This is an automated email from the ASF dual-hosted git repository. jli pushed a commit to branch alert-report-test-gaps in repository https://gitbox.apache.org/repos/asf/superset.git
commit 7136c04c3fdc3064183c30e65d00579cf1e892f2 Merge: 1d762b94529 a6c0d6321f6 Author: Joe Li <[email protected]> AuthorDate: Fri Mar 6 13:44:32 2026 -0800 Merge remote-tracking branch 'origin/master' into alert-report-test-gaps # Conflicts: # superset-frontend/src/features/alerts/AlertReportModal.test.tsx .devcontainer/devcontainer.json | 2 +- .github/workflows/docker.yml | 2 +- .gitignore | 2 +- RESOURCES/INTHEWILD.yaml | 3 + UPDATING.md | 8 +- docs/.gitignore | 8 +- docs/.nvmrc | 2 +- docs/admin_docs/configuration/alerts-reports.mdx | 14 + docs/admin_docs/configuration/cache.mdx | 16 +- docs/babel.config.js | 32 - docs/developer_docs/api.mdx | 36 +- docs/developer_docs/extensions/architecture.md | 66 +- .../developer_docs/extensions/components/alert.mdx | 6 +- .../developer_docs/extensions/components/index.mdx | 8 +- .../extensions/contribution-types.md | 255 ++- docs/developer_docs/extensions/dependencies.md | 6 +- docs/developer_docs/extensions/development.md | 210 +- .../extensions/extension-points/editors.md | 49 +- .../extensions/extension-points/sqllab.md | 173 +- docs/developer_docs/extensions/mcp-server.md | 679 ++++++ docs/developer_docs/extensions/mcp.md | 6 +- docs/developer_docs/extensions/overview.md | 6 +- docs/developer_docs/extensions/quick-start.md | 164 +- docs/developer_docs/extensions/registry.md | 14 +- docs/developer_docs/extensions/tasks.md | 14 +- docs/developer_docs/sidebars.js | 1 + docs/docusaurus.config.ts | 28 + docs/package.json | 17 +- docs/scripts/generate-database-docs.mjs | 8 +- docs/scripts/generate-extension-components.mjs | 676 ------ docs/scripts/generate-if-changed.mjs | 307 +++ docs/scripts/generate-superset-components.mjs | 241 ++- docs/sidebarTutorials.js | 11 - docs/src/components/StorybookWrapper.jsx | 4 +- docs/src/components/databases/DatabaseIndex.tsx | 1 + .../components/ui-components/ComponentIndex.tsx | 262 +++ .../src/components/ui-components}/index.ts | 6 +- .../src/components/ui-components/types.ts | 36 +- docs/src/data/databases.json | 12 +- docs/src/shims/superset-ui-core.ts | 57 + docs/src/theme/Playground/Preview/index.tsx | 2 +- docs/src/theme/ReactLiveScope/index.tsx | 2 +- docs/src/types/apache-superset-core/index.d.ts | 6 +- docs/src/webpack.extend.ts | 19 +- .../{sql-snippets.png => editor-snippets.png} | Bin docs/tsconfig.json | 7 +- docs/yarn.lock | 1288 ++++++++--- pyproject.toml | 8 +- requirements/base.in | 4 +- requirements/base.txt | 16 +- requirements/development.txt | 26 +- superset-core/README.md | 4 +- superset-core/src/superset_core/api/rest_api.py | 72 - .../src/superset_core/{api => common}/__init__.py | 0 .../src/superset_core/{api => common}/daos.py | 82 +- .../src/superset_core/{api => common}/models.py | 192 +- .../src/superset_core/extensions/types.py | 109 +- superset-core/src/superset_core/mcp/__init__.py | 145 -- .../mcp/{__init__.py => decorators.py} | 2 +- .../src/superset_core/{api => queries}/__init__.py | 0 superset-core/src/superset_core/queries/daos.py | 63 + superset-core/src/superset_core/queries/models.py | 79 + .../src/superset_core/{api => queries}/query.py | 4 +- .../src/superset_core/{api => queries}/types.py | 0 .../superset_core/{api => rest_api}/__init__.py | 0 .../{api/__init__.py => rest_api/api.py} | 16 + .../src/superset_core/rest_api/decorators.py | 103 + .../src/superset_core/{api => tasks}/__init__.py | 0 superset-core/src/superset_core/tasks/daos.py | 76 + .../src/superset_core/tasks/decorators.py | 152 ++ superset-core/src/superset_core/tasks/models.py | 169 ++ .../superset_core/{api/tasks.py => tasks/types.py} | 128 +- superset-embedded-sdk/.nvmrc | 2 +- .../src/superset_extensions_cli/cli.py | 144 +- .../templates/backend/pyproject.toml.j2 | 7 + .../templates/backend/src/package/__init__.py.j2 | 0 .../templates/extension.json.j2 | 20 - .../templates/frontend/src/index.tsx.j2 | 17 +- .../templates/frontend/webpack.config.js.j2 | 3 +- .../src/superset_extensions_cli/types.py | 4 +- .../src/superset_extensions_cli/utils.py | 2 +- superset-extensions-cli/tests/conftest.py | 9 +- superset-extensions-cli/tests/test_cli_build.py | 237 +- superset-extensions-cli/tests/test_cli_bundle.py | 5 +- superset-extensions-cli/tests/test_cli_init.py | 32 +- superset-extensions-cli/tests/test_cli_validate.py | 14 +- .../tests/test_name_transformations.py | 11 +- superset-extensions-cli/tests/test_templates.py | 55 +- superset-frontend/.eslintrc.js | 41 - superset-frontend/.nvmrc | 2 +- superset-frontend/.storybook/preview.jsx | 2 +- .../.storybook/shared/ResizableChartDemo.tsx | 2 +- .../.storybook/shared/ResizablePanel.tsx | 2 +- superset-frontend/.storybook/shared/VerifyCORS.tsx | 2 +- .../e2e/dashboard/_skip.nativeFilters.test.ts | 4 +- .../cypress/e2e/dashboard_list/filter.test.ts | 47 - .../cypress/e2e/dashboard_list/list.test.ts | 279 --- superset-frontend/oxlint.json | 4 +- superset-frontend/package-lock.json | 2282 ++++++++------------ superset-frontend/package.json | 24 +- .../packages/generator-superset/package.json | 2 +- .../packages/superset-core/package.json | 62 + .../superset-core/src/api/contributions.ts | 147 -- .../packages/superset-core/src/api/index.ts | 42 - .../authentication.ts => authentication/index.ts} | 0 .../src/{api/commands.ts => commands/index.ts} | 51 +- .../src/{api/core.ts => common/index.ts} | 74 +- .../{ui => }/components/Alert/Alert.stories.tsx | 0 .../src/{ui => }/components/Alert/Alert.test.tsx | 0 .../src/{ui => }/components/Alert/index.tsx | 0 .../superset-core/src/{ui => }/components/index.ts | 0 .../superset-core/src/contributions/index.ts | 85 + .../src/{api/editors.ts => editors/index.ts} | 105 +- .../src/{api/extensions.ts => extensions/index.ts} | 2 +- .../packages/superset-core/src/index.ts | 16 +- .../packages/superset-core/src/menus/index.ts | 104 + .../src/{api/sqlLab.ts => sqlLab/index.ts} | 4 +- .../superset-core/src/{ui => }/testing.tsx | 0 .../src/{ui => }/theme/GlobalStyles.tsx | 11 + .../src/{ui => }/theme/Theme.test.tsx | 73 + .../superset-core/src/{ui => }/theme/Theme.tsx | 14 + .../src/{ui => }/theme/exampleThemes.ts | 0 .../superset-core/src/{ui => }/theme/index.tsx | 0 .../superset-core/src/{ui => }/theme/types.ts | 0 .../src/{ui => }/theme/utils/index.ts | 0 .../src/{ui => }/theme/utils/themeUtils.test.ts | 0 .../src/{ui => }/theme/utils/themeUtils.ts | 0 .../src/{ui => }/theme/utils/utils.test.ts | 0 .../src/{ui => }/translation/README.md | 0 .../src/{ui => }/translation/Translator.ts | 2 +- .../{ui => }/translation/TranslatorSingleton.ts | 0 .../src/{ui => }/translation/index.ts | 0 .../src/{ui => }/translation/types/index.ts | 0 .../src/{ui => }/translation/types/jed.ts | 0 .../src/{spec => }/utils/logging.test.ts | 4 +- .../packages/superset-core/src/views/index.ts | 90 + .../src/components/CertifiedIconWithTooltip.tsx | 4 +- .../src/components/ColumnOption.tsx | 2 +- .../components/ColumnTypeLabel/ColumnTypeLabel.tsx | 6 +- .../src/components/ControlHeader.tsx | 4 +- .../src/components/ControlSubSectionHeader.tsx | 2 +- .../src/components/MetricOption.tsx | 2 +- .../src/components/SQLPopover.tsx | 4 +- .../src/components/labelUtils.tsx | 4 +- .../superset-ui-chart-controls/src/constants.ts | 4 +- .../superset-ui-chart-controls/src/fixtures.ts | 2 +- .../src/sections/advancedAnalytics.tsx | 2 +- .../src/sections/annotationsAndLayers.tsx | 2 +- .../src/sections/chartTitle.tsx | 2 +- .../src/sections/echartsTimeSeriesQuery.tsx | 2 +- .../src/sections/forecastInterval.tsx | 2 +- .../src/sections/matrixify.tsx | 2 +- .../src/sections/sections.tsx | 2 +- .../src/sections/timeComparison.tsx | 2 +- .../components/RadioButtonControl.tsx | 2 +- .../src/shared-controls/customControls.tsx | 4 +- .../src/shared-controls/dndControls.tsx | 4 +- .../src/shared-controls/matrixifyControls.tsx | 2 +- .../src/shared-controls/mixins.tsx | 2 +- .../src/shared-controls/sharedControls.tsx | 17 +- .../superset-ui-chart-controls/src/types.ts | 3 +- .../src/utils/D3Formatting.ts | 2 +- .../src/utils/checkColumnType.ts | 2 +- .../src/utils/columnChoices.ts | 2 +- .../src/utils/getColorFormatters.ts | 2 +- .../src/utils/isSortable.ts | 2 +- .../src/utils/metricColumnFilter.test.ts | 8 + .../test/components/ColumnOption.test.tsx | 2 +- .../test/components/ColumnTypeLabel.test.tsx | 2 +- .../test/shared-controls/customControls.test.tsx | 2 +- .../test/utils/checkColumnType.test.ts | 2 +- .../test/utils/columnChoices.test.tsx | 2 +- .../test/utils/getColorFormatters.test.ts | 43 +- .../test/utils/getTemporalColumns.test.ts | 2 +- .../test/utils/isSortable.test.ts | 2 +- .../packages/superset-ui-core/package.json | 2 +- .../src/chart/components/FallbackComponent.tsx | 4 +- .../Matrixify/MatrixifyGridCell.test.tsx | 2 +- .../components/Matrixify/MatrixifyGridCell.tsx | 4 +- .../Matrixify/MatrixifyGridGenerator.test.ts | 120 + .../components/Matrixify/MatrixifyGridGenerator.ts | 8 +- .../Matrixify/MatrixifyGridRenderer.test.tsx | 4 +- .../components/Matrixify/MatrixifyGridRenderer.tsx | 2 +- .../src/chart/components/NoResultsComponent.tsx | 3 +- .../src/chart/components/StatefulChart.tsx | 2 +- .../src/chart/components/SuperChartCore.tsx | 2 +- .../src/chart/models/ChartProps.ts | 2 +- .../src/chart/types/matrixify.mocks.test.ts | 11 +- .../src/chart/types/matrixify.test.ts | 82 + .../superset-ui-core/src/chart/types/matrixify.ts | 36 +- .../src/components/ActionButton/index.tsx | 2 +- .../src/components/AsyncAceEditor/index.tsx | 8 +- .../AsyncAceEditor/useJsonValidation.test.ts | 25 + .../src/components/Badge/index.tsx | 2 +- .../src/components/Button/index.tsx | 2 +- .../src/components/CachedLabel/TooltipContent.tsx | 2 +- .../src/components/CachedLabel/index.tsx | 2 +- .../superset-ui-core/src/components/Card/index.tsx | 2 +- .../CertifiedBadge/CertifiedBadge.stories.tsx | 2 +- .../src/components/CertifiedBadge/index.tsx | 4 +- .../src/components/Checkbox/CheckboxIcons.tsx | 2 +- .../src/components/CodeSyntaxHighlighter/index.tsx | 2 +- .../src/components/Collapse/Collapse.tsx | 2 +- .../components/Collapse/CollapseLabelInModal.tsx | 2 +- .../components/ConfirmModal/ConfirmModal.test.tsx | 2 +- .../src/components/ConfirmModal/index.tsx | 4 +- .../src/components/CronPicker/index.tsx | 4 +- .../src/components/DatePicker/index.tsx | 2 +- .../src/components/DeleteModal/index.tsx | 4 +- .../src/components/Divider/index.tsx | 2 +- .../src/components/Dropdown/index.tsx | 2 +- .../src/components/DropdownButton/index.tsx | 2 +- .../DropdownContainer.stories.tsx | 2 +- .../DropdownContainer/DropdownContainer.tsx | 4 +- .../src/components/DynamicEditableTitle/index.tsx | 4 +- .../src/components/EditableTitle/index.tsx | 4 +- .../src/components/EmptyState/index.tsx | 4 +- .../src/components/FaveStar/index.tsx | 4 +- .../src/components/Flex/Flex.stories.tsx | 2 +- .../src/components/Form/FormItem.tsx | 2 +- .../src/components/Form/FormLabel.tsx | 2 +- .../src/components/Form/LabeledErrorBoundInput.tsx | 4 +- .../src/components/IconButton/index.tsx | 2 +- .../components/IconTooltip/IconTooltip.stories.tsx | 2 +- .../src/components/Icons/BaseIcon.tsx | 2 +- .../src/components/Icons/Icons.stories.tsx | 2 +- .../src/components/InfoTooltip/index.tsx | 4 +- .../src/components/Label/index.tsx | 2 +- .../components/Label/reusable/DatasetTypeLabel.tsx | 4 +- .../components/Label/reusable/PublishedLabel.tsx | 4 +- .../src/components/LastUpdated/index.tsx | 4 +- .../src/components/List/List.test.tsx | 10 + .../superset-ui-core/src/components/List/index.ts | 2 +- .../src/components/ListViewCard/ImageLoader.tsx | 4 +- .../src/components/ListViewCard/index.tsx | 2 +- .../src/components/Loading/index.test.tsx | 2 +- .../src/components/Loading/index.tsx | 4 +- .../superset-ui-core/src/components/Menu/index.tsx | 2 +- .../src/components/Metadata/index.tsx | 2 +- .../src/components/MetadataBar/ContentConfig.tsx | 4 +- .../components/MetadataBar/MetadataBar.stories.tsx | 2 +- .../components/MetadataBar/MetadataBar.test.tsx | 2 +- .../src/components/MetadataBar/MetadataBar.tsx | 2 +- .../src/components/Modal/FormModal.tsx | 2 +- .../src/components/Modal/Modal.tsx | 4 +- .../src/components/PageHeaderWithActions/index.tsx | 4 +- .../src/components/PopoverDropdown/index.tsx | 2 +- .../src/components/PopoverSection/index.tsx | 2 +- .../src/components/ProgressBar/index.tsx | 2 +- .../src/components/Radio/Radio.stories.tsx | 2 +- .../src/components/RefreshLabel/index.tsx | 2 +- .../src/components/Select/AsyncSelect.tsx | 2 +- .../src/components/Select/Select.tsx | 2 +- .../src/components/Select/constants.test.ts | 49 + .../src/components/Select/constants.ts | 2 +- .../src/components/Select/styles.tsx | 5 +- .../src/components/Select/utils.tsx | 2 +- .../src/components/Table/VirtualTable.tsx | 2 +- .../Table/cell-renderers/ActionCell/index.tsx | 2 +- .../Table/cell-renderers/NullCell/index.tsx | 2 +- .../Table/cell-renderers/NumericCell/index.tsx | 2 +- .../header-renderers/HeaderWithRadioGroup.tsx | 2 +- .../src/components/Table/index.tsx | 6 +- .../Table/utils/InteractiveTableUtils.test.ts | 574 +++++ .../src/components/TableCollection/index.tsx | 2 +- .../src/components/TableView/TableView.tsx | 2 +- .../superset-ui-core/src/components/Tabs/Tabs.tsx | 2 +- .../ThemedAgGridReact/ThemedAgGridReact.test.tsx | 8 +- .../src/components/ThemedAgGridReact/index.tsx | 2 +- .../src/components/Timer/index.tsx | 2 +- .../src/components/TimezoneSelector/index.tsx | 2 +- .../src/components/TruncatedList/index.tsx | 4 +- .../src/components/Typography/index.tsx | 2 +- .../src/components/UnsavedChangesModal/index.tsx | 2 +- .../components/WarningIconWithTooltip/index.tsx | 2 +- .../superset-ui-core/src/components/constants.ts | 2 +- .../superset-ui-core/src/components/index.ts | 1 + .../src/query/extractQueryFields.ts | 2 +- .../src/query/getClientErrorObject.ts | 2 +- .../superset-ui-core/src/query/types/Column.ts | 2 +- .../superset-ui-core/src/query/types/Query.ts | 2 +- .../src/query/types/QueryResponse.ts | 2 +- .../packages/superset-ui-core/src/spec/index.tsx | 2 +- .../src/style/stories/Theme.stories.tsx | 2 +- .../src/types/react-syntax-highlighter.d.ts | 7 - .../superset-ui-core/src/utils/featureFlags.ts | 2 +- .../src/utils/rankedSearchCompare.test.ts | 20 + .../packages/superset-ui-core/src/utils/tooltip.ts | 2 +- .../List/List.test.tsx => utils/withLabel.test.ts} | 33 +- .../src/validator/legacyValidateInteger.ts | 2 +- .../src/validator/legacyValidateNumber.ts | 2 +- .../src/validator/validateInteger.ts | 2 +- .../src/validator/validateMapboxStylesUrl.ts | 2 +- .../src/validator/validateMaxValue.ts | 2 +- .../src/validator/validateNonEmpty.ts | 2 +- .../src/validator/validateNumber.ts | 2 +- .../src/validator/validateServerPagination.ts | 2 +- .../validator/validateTimeComparisonRangeValues.ts | 2 +- .../test/chart/clients/ChartClient.test.ts | 2 +- .../test/chart/components/MockChartPlugins.tsx | 2 +- .../test/chart/components/SuperChartCore.test.tsx | 2 +- .../test/chart/models/ChartPlugin.test.tsx | 2 +- .../test/chart/models/ChartProps.test.ts | 2 +- .../Icons/AsyncIcon.integration.test.tsx | 2 +- .../test/connection/SupersetClient.test.ts | 6 + .../test/connection/SupersetClientClass.test.ts | 33 + .../test/currency-format/CurrencyFormatter.test.ts | 78 + .../test/currency-format/utils.test.ts | 193 ++ .../packages/superset-ui-core/test/fixtures.ts | 2 +- .../test/query/extractQueryFields.test.ts | 2 +- .../test/query/types/Column.test.ts | 14 + .../test/query/types/Dashboard.test.ts | 33 + .../test/utils/featureFlag.test.ts | 4 +- .../superset-ui-core/test/validator/setup.ts | 2 +- .../playwright/helpers/api/dashboard.ts | 170 ++ .../playwright/helpers/fixtures/testAssets.ts | 24 +- .../playwright/pages/DashboardListPage.ts | 139 ++ .../experimental/dashboard/dashboard-list.spec.ts | 403 ++++ .../dashboard/dashboard-test-helpers.ts | 74 + .../legacy-plugin-chart-calendar/src/Calendar.ts | 3 +- .../src/ReactCalendar.tsx | 2 +- .../src/controlPanel.ts | 2 +- .../legacy-plugin-chart-calendar/src/index.ts | 2 +- .../src/vendor/cal-heatmap.ts | 2 +- .../legacy-plugin-chart-chord/src/ReactChord.tsx | 2 +- .../legacy-plugin-chart-chord/src/controlPanel.ts | 2 +- .../plugins/legacy-plugin-chart-chord/src/index.ts | 2 +- .../src/ReactCountryMap.tsx | 2 +- .../src/controlPanel.ts | 2 +- .../legacy-plugin-chart-country-map/src/index.ts | 2 +- .../src/stories/CountryMap.stories.tsx | 2 +- .../src/HorizonChart.tsx | 2 +- .../src/controlPanel.ts | 2 +- .../legacy-plugin-chart-horizon/src/index.ts | 2 +- .../src/controlPanel.ts | 2 +- .../legacy-plugin-chart-map-box/src/index.ts | 2 +- .../src/stories/MapBox.stories.tsx | 2 +- .../src/stories/data.ts | 2 +- .../src/PairedTTest.tsx | 2 +- .../src/controlPanel.ts | 2 +- .../legacy-plugin-chart-paired-t-test/src/index.ts | 2 +- .../src/ReactParallelCoordinates.tsx | 2 +- .../src/controlPanel.ts | 2 +- .../src/index.ts | 2 +- .../src/ReactPartition.tsx | 2 +- .../src/controlPanel.tsx | 2 +- .../legacy-plugin-chart-partition/src/index.ts | 2 +- .../legacy-plugin-chart-rose/src/ReactRose.tsx | 2 +- .../legacy-plugin-chart-rose/src/controlPanel.tsx | 2 +- .../plugins/legacy-plugin-chart-rose/src/index.ts | 2 +- .../src/ReactWorldMap.tsx | 2 +- .../src/controlPanel.ts | 2 +- .../legacy-plugin-chart-world-map/src/index.ts | 2 +- .../src/DeckGLContainer.tsx | 2 +- .../src/Multi/Multi.test.tsx | 2 +- .../legacy-preset-chart-deckgl/src/Multi/Multi.tsx | 2 +- .../src/Multi/controlPanel.ts | 2 +- .../legacy-preset-chart-deckgl/src/Multi/index.ts | 2 +- .../src/components/Legend.tsx | 2 +- .../src/components/Tooltip.tsx | 2 +- .../src/layers/Arc/controlPanel.ts | 2 +- .../src/layers/Arc/index.ts | 2 +- .../src/layers/Contour/Contour.tsx | 2 +- .../src/layers/Contour/controlPanel.ts | 2 +- .../src/layers/Contour/index.ts | 2 +- .../src/layers/Geojson/controlPanel.ts | 2 +- .../src/layers/Geojson/index.ts | 2 +- .../src/layers/Grid/controlPanel.ts | 2 +- .../src/layers/Grid/index.ts | 2 +- .../src/layers/Heatmap/Heatmap.tsx | 2 +- .../src/layers/Heatmap/controlPanel.ts | 2 +- .../src/layers/Heatmap/index.ts | 2 +- .../src/layers/Hex/controlPanel.ts | 2 +- .../src/layers/Hex/index.ts | 2 +- .../src/layers/Path/controlPanel.ts | 2 +- .../src/layers/Path/index.ts | 2 +- .../src/layers/Path/stories/Path.stories.tsx | 2 +- .../src/layers/Polygon/Polygon.test.tsx | 2 +- .../src/layers/Polygon/Polygon.tsx | 2 +- .../src/layers/Polygon/controlPanel.ts | 2 +- .../src/layers/Polygon/index.ts | 2 +- .../src/layers/Scatter/Scatter.tsx | 2 +- .../src/layers/Scatter/controlPanel.ts | 2 +- .../src/layers/Scatter/index.ts | 2 +- .../src/layers/Screengrid/Screengrid.tsx | 4 +- .../src/layers/Screengrid/controlPanel.ts | 2 +- .../src/layers/Screengrid/index.ts | 2 +- .../src/layers/spatialUtils.test.ts | 2 +- .../src/utilities/HandlebarsRenderer.tsx | 4 +- .../src/utilities/Shared_DeckGL.tsx | 2 +- .../src/utilities/TooltipTemplateControl.tsx | 4 +- .../src/utilities/TooltipTemplateEditor.tsx | 2 +- .../src/utilities/sharedDndControls.tsx | 2 +- .../src/utilities/tooltipUtils.tsx | 2 +- .../src/Bubble/controlPanel.ts | 2 +- .../legacy-preset-chart-nvd3/src/Bubble/index.ts | 2 +- .../src/Bullet/controlPanel.ts | 2 +- .../legacy-preset-chart-nvd3/src/Bullet/index.ts | 2 +- .../src/Compare/controlPanel.ts | 2 +- .../legacy-preset-chart-nvd3/src/Compare/index.ts | 2 +- .../legacy-preset-chart-nvd3/src/NVD3Controls.tsx | 2 +- .../legacy-preset-chart-nvd3/src/NVD3Vis.ts | 2 +- .../legacy-preset-chart-nvd3/src/ReactNVD3.tsx | 2 +- .../src/TimePivot/controlPanel.ts | 2 +- .../src/TimePivot/index.ts | 2 +- .../src/vendor/superset/AnnotationTypes.ts | 2 +- .../src/AgGridTable/components/CustomHeader.tsx | 2 +- .../src/AgGridTable/components/Pagination.tsx | 2 +- .../components/TimeComparisonVisibility.tsx | 2 +- .../src/AgGridTable/index.tsx | 2 +- .../src/AgGridTableChart.tsx | 4 +- .../src/controlPanel.tsx | 4 +- .../plugin-chart-ag-grid-table/src/index.ts | 2 +- .../src/renderers/NumericCellRenderer.tsx | 6 +- .../src/renderers/TextCellRenderer.tsx | 2 +- .../plugin-chart-ag-grid-table/src/stories/data.ts | 2 +- .../src/styles/index.tsx | 2 +- .../src/transformProps.ts | 4 +- .../src/utils/formatValue.ts | 2 +- .../src/utils/useColDefs.ts | 2 +- .../src/utils/useTableTheme.ts | 2 +- .../test/controlPanel.test.tsx | 2 +- .../test/utils/useColDefs.test.ts | 2 +- .../src/CartodiagramPlugin.tsx | 2 +- .../src/components/ChartLayer.tsx | 2 +- .../src/components/ChartWrapper.tsx | 2 +- .../src/plugin/controlPanel.ts | 2 +- .../plugin-chart-cartodiagram/src/plugin/index.ts | 2 +- .../plugins/plugin-chart-cartodiagram/src/types.ts | 2 +- .../src/util/chartUtil.tsx | 2 +- .../src/util/controlPanelUtil.tsx | 2 +- .../test/plugin/transformProps.test.ts | 2 +- .../plugins/plugin-chart-echarts/package.json | 4 +- .../BigNumber/BigNumberPeriodOverPeriod/PopKPI.tsx | 4 +- .../BigNumberPeriodOverPeriod/controlPanel.ts | 4 +- .../BigNumber/BigNumberPeriodOverPeriod/index.ts | 2 +- .../src/BigNumber/BigNumberTotal/controlPanel.ts | 4 +- .../src/BigNumber/BigNumberTotal/index.ts | 2 +- .../BigNumberTotal/transformProps.test.ts | 2 +- .../src/BigNumber/BigNumberTotal/transformProps.ts | 2 +- .../src/BigNumber/BigNumberViz.tsx | 4 +- .../BigNumberWithTrendline/buildQuery.test.ts | 4 +- .../BigNumber/BigNumberWithTrendline/buildQuery.ts | 2 +- .../BigNumberWithTrendline/controlPanel.tsx | 2 +- .../src/BigNumber/BigNumberWithTrendline/index.ts | 2 +- .../BigNumberWithTrendline/transformProps.test.ts | 2 +- .../BigNumberWithTrendline/transformProps.ts | 4 +- .../src/BigNumber/sharedControls.ts | 2 +- .../src/BoxPlot/controlPanel.ts | 2 +- .../plugin-chart-echarts/src/BoxPlot/index.ts | 2 +- .../src/Bubble/controlPanel.tsx | 2 +- .../plugin-chart-echarts/src/Bubble/index.ts | 2 +- .../src/Funnel/controlPanel.tsx | 2 +- .../plugin-chart-echarts/src/Funnel/index.ts | 2 +- .../src/Gantt/EchartsGantt.tsx | 2 +- .../src/Gantt/controlPanel.tsx | 4 +- .../plugin-chart-echarts/src/Gantt/index.ts | 2 +- .../src/Gantt/transformProps.ts | 4 +- .../plugin-chart-echarts/src/Gauge/constants.ts | 2 +- .../src/Gauge/controlPanel.tsx | 2 +- .../plugin-chart-echarts/src/Gauge/index.ts | 2 +- .../src/Graph/controlPanel.tsx | 2 +- .../plugin-chart-echarts/src/Graph/index.ts | 2 +- .../src/Heatmap/controlPanel.tsx | 2 +- .../plugin-chart-echarts/src/Heatmap/index.ts | 2 +- .../src/Heatmap/transformProps.ts | 4 +- .../src/Histogram/controlPanel.tsx | 4 +- .../plugin-chart-echarts/src/Histogram/index.ts | 2 +- .../src/MixedTimeseries/buildQuery.ts | 2 +- .../src/MixedTimeseries/controlPanel.tsx | 3 +- .../src/MixedTimeseries/index.ts | 2 +- .../src/MixedTimeseries/transformProps.ts | 22 +- .../plugin-chart-echarts/src/Pie/controlPanel.tsx | 2 +- .../plugins/plugin-chart-echarts/src/Pie/index.ts | 2 +- .../plugin-chart-echarts/src/Pie/transformProps.ts | 2 +- .../src/Radar/controlPanel.tsx | 4 +- .../plugin-chart-echarts/src/Radar/index.ts | 2 +- .../src/Sankey/controlPanel.tsx | 2 +- .../plugin-chart-echarts/src/Sankey/index.ts | 2 +- .../src/Sunburst/controlPanel.tsx | 2 +- .../plugin-chart-echarts/src/Sunburst/index.ts | 2 +- .../src/Sunburst/transformProps.ts | 2 +- .../src/Timeseries/Area/controlPanel.tsx | 3 +- .../src/Timeseries/Area/index.ts | 2 +- .../src/Timeseries/Regular/Bar/controlPanel.tsx | 5 +- .../src/Timeseries/Regular/Bar/index.ts | 2 +- .../src/Timeseries/Regular/Line/controlPanel.tsx | 3 +- .../src/Timeseries/Regular/Line/index.ts | 2 +- .../Timeseries/Regular/Scatter/controlPanel.tsx | 2 +- .../src/Timeseries/Regular/Scatter/index.ts | 2 +- .../Timeseries/Regular/SmoothLine/controlPanel.tsx | 3 +- .../src/Timeseries/Regular/SmoothLine/index.ts | 2 +- .../src/Timeseries/Step/controlPanel.tsx | 2 +- .../src/Timeseries/Step/index.ts | 2 +- .../src/Timeseries/buildQuery.ts | 6 +- .../src/Timeseries/constants.ts | 2 +- .../plugin-chart-echarts/src/Timeseries/index.ts | 2 +- .../src/Timeseries/transformProps.ts | 152 +- .../src/Timeseries/transformers.ts | 51 +- .../plugin-chart-echarts/src/Timeseries/types.ts | 1 + .../plugin-chart-echarts/src/Tree/controlPanel.tsx | 2 +- .../plugins/plugin-chart-echarts/src/Tree/index.ts | 2 +- .../src/Treemap/controlPanel.tsx | 2 +- .../plugin-chart-echarts/src/Treemap/index.ts | 2 +- .../src/Waterfall/constants.ts | 2 +- .../src/Waterfall/controlPanel.tsx | 2 +- .../plugin-chart-echarts/src/Waterfall/index.ts | 2 +- .../src/Waterfall/transformProps.ts | 2 +- .../plugin-chart-echarts/src/components/Echart.tsx | 12 +- .../src/components/ExtraControls.tsx | 2 +- .../plugins/plugin-chart-echarts/src/constants.ts | 2 +- .../plugins/plugin-chart-echarts/src/controls.tsx | 30 +- .../plugins/plugin-chart-echarts/src/index.ts | 3 + .../src/utils/eChartOptionsSchema.ts | 827 +++++++ .../plugin-chart-echarts/src/utils/formatters.ts | 91 +- .../src/utils/mergeCustomEChartOptions.test.ts | 163 ++ .../src/utils/mergeCustomEChartOptions.ts | 79 + .../src/utils/safeEChartOptionsParser.test.ts | 525 +++++ .../src/utils/safeEChartOptionsParser.ts | 477 ++++ .../plugin-chart-echarts/src/utils/series.ts | 4 +- .../src/utils/themeOverrides.test.ts | 623 ++++-- .../src/utils/themeOverrides.ts | 89 + .../test/BigNumber/transformProps.test.ts | 2 +- .../test/BoxPlot/transformProps.test.ts | 2 +- .../test/Bubble/transformProps.test.ts | 2 +- .../test/Funnel/transformProps.test.ts | 2 +- .../test/Gantt/transformProps.test.ts | 2 +- .../test/Gauge/transformProps.test.ts | 2 +- .../test/Graph/transformProps.test.ts | 2 +- .../test/Heatmap/transformProps.test.ts | 2 +- .../test/MixedTimeseries/transformProps.test.ts | 2 +- .../test/Pie/transformProps.test.ts | 2 +- .../test/Radar/transformProps.test.ts | 2 +- .../test/Timeseries/Bar/transformProps.test.ts | 285 ++- .../test/Timeseries/Scatter/transformProps.test.ts | 12 +- .../test/Timeseries/transformProps.test.ts | 6 +- .../test/Timeseries/transformers.test.ts | 4 +- .../test/Tree/transformProps.test.ts | 2 +- .../test/Treemap/transformProps.test.ts | 2 +- .../test/Waterfall/transformProps.test.ts | 2 +- .../plugins/plugin-chart-echarts/test/helpers.ts | 2 +- .../test/utils/formatters.test.ts | 174 +- .../plugin-chart-echarts/test/utils/series.test.ts | 4 +- .../test/utils/transformers.test.ts | 2 +- .../plugin-chart-handlebars/src/Handlebars.tsx | 2 +- .../src/components/Handlebars/HandlebarsViewer.tsx | 4 +- .../src/plugin/controlPanel.tsx | 2 +- .../src/plugin/controls/columns.tsx | 2 +- .../src/plugin/controls/handlebarTemplate.tsx | 4 +- .../src/plugin/controls/includeTime.ts | 2 +- .../src/plugin/controls/metrics.tsx | 2 +- .../src/plugin/controls/orderBy.tsx | 2 +- .../src/plugin/controls/queryMode.tsx | 2 +- .../src/plugin/controls/shared.ts | 2 +- .../src/plugin/controls/style.tsx | 4 +- .../plugin-chart-handlebars/src/plugin/index.ts | 2 +- .../test/plugin/transformProps.test.ts | 2 +- .../src/PivotTableChart.tsx | 4 +- .../src/plugin/controlPanel.tsx | 2 +- .../plugin-chart-pivot-table/src/plugin/index.ts | 2 +- .../src/plugin/transformProps.ts | 2 +- .../src/react-pivottable/Styles.ts | 2 +- .../src/react-pivottable/TableRenderers.tsx | 2 +- .../src/react-pivottable/utilities.ts | 2 +- .../test/plugin/transformProps.test.ts | 2 +- .../plugin-chart-table/src/DataTable/DataTable.tsx | 2 +- .../src/DataTable/components/GlobalFilter.tsx | 2 +- .../DataTable/components/SearchSelectDropdown.tsx | 2 +- .../src/DataTable/components/SelectPageSize.tsx | 4 +- .../src/DataTable/hooks/useSticky.tsx | 2 +- .../plugins/plugin-chart-table/src/Styles.tsx | 2 +- .../plugins/plugin-chart-table/src/TableChart.tsx | 7 +- .../plugins/plugin-chart-table/src/consts.ts | 2 +- .../plugin-chart-table/src/controlPanel.tsx | 4 +- .../plugins/plugin-chart-table/src/index.ts | 2 +- .../plugin-chart-table/src/stories/testData.ts | 2 +- .../plugin-chart-table/src/transformProps.ts | 4 +- .../plugin-chart-table/src/utils/formatValue.ts | 2 +- .../plugin-chart-table/test/TableChart.test.tsx | 2 +- .../plugin-chart-table/test/controlPanel.test.tsx | 2 +- .../plugins/plugin-chart-table/test/testData.ts | 4 +- .../plugin-chart-table/test/testHelpers.tsx | 2 +- .../test/utils/formatValue.test.ts | 2 +- .../plugins/plugin-chart-word-cloud/package.json | 2 +- .../src/chart/WordCloud.tsx | 2 +- .../src/plugin/controlPanel.tsx | 2 +- .../ColorSchemeControl/ColorSchemeLabel.tsx | 2 +- .../plugin/controls/ColorSchemeControl/index.tsx | 4 +- .../src/plugin/controls/RotationControl.tsx | 2 +- .../plugin-chart-word-cloud/src/plugin/index.ts | 2 +- .../scripts/eslint-metrics-uploader.js | 144 -- .../scripts/oxlint-metrics-uploader.js | 7 +- superset-frontend/spec/helpers/ProviderWrapper.tsx | 2 +- superset-frontend/spec/helpers/shim.tsx | 2 +- superset-frontend/spec/helpers/testing-library.tsx | 7 +- .../src/SqlLab/SqlLabGlobalStyles.tsx | 2 +- superset-frontend/src/SqlLab/actions/sqlLab.ts | 2 +- .../src/SqlLab/components/App/index.tsx | 4 +- .../SqlLab/components/AppLayout/AppLayout.test.tsx | 79 +- .../src/SqlLab/components/AppLayout/index.tsx | 11 +- .../src/SqlLab/components/ColumnElement/index.tsx | 4 +- .../EditorAutoSync/EditorAutoSync.test.tsx | 6 +- .../src/SqlLab/components/EditorAutoSync/index.tsx | 2 +- .../src/SqlLab/components/EditorWrapper/index.tsx | 2 +- .../components/EditorWrapper/useAnnotations.ts | 2 +- .../SqlLab/components/EditorWrapper/useKeywords.ts | 2 +- .../components/EstimateQueryCostButton/index.tsx | 5 +- .../components/ExploreCtasResultsButton/index.tsx | 2 +- .../components/ExploreResultsButton/index.tsx | 2 +- .../src/SqlLab/components/HighlightedSql/index.tsx | 2 +- .../components/KeyboardShortcutButton/index.tsx | 4 +- .../src/SqlLab/components/QueryHistory/index.tsx | 4 +- .../SqlLab/components/QueryLimitSelect/index.tsx | 2 +- .../SqlLab/components/QueryStateLabel/index.tsx | 2 +- .../src/SqlLab/components/QueryStatusBar/index.tsx | 3 +- .../src/SqlLab/components/QueryTable/index.tsx | 4 +- .../src/SqlLab/components/QueryTable/styles.ts | 2 +- .../src/SqlLab/components/ResultSet/index.tsx | 7 +- .../components/RunQueryActionButton/index.tsx | 4 +- .../components/SaveDatasetActionButton/index.tsx | 2 +- .../SqlLab/components/SaveDatasetModal/index.tsx | 4 +- .../src/SqlLab/components/SaveQuery/index.tsx | 4 +- .../components/ScheduleQueryButton/index.tsx | 4 +- .../SqlLab/components/ShareSqlLabQuery/index.tsx | 4 +- .../src/SqlLab/components/SouthPane/Results.tsx | 5 +- .../src/SqlLab/components/SouthPane/index.tsx | 24 +- .../src/SqlLab/components/SqlEditor/index.tsx | 5 +- .../SqlLab/components/SqlEditorLeftBar/index.tsx | 4 +- .../SqlLab/components/SqlEditorTabHeader/index.tsx | 9 +- .../SqlLab/components/SqlEditorTopBar/index.tsx | 2 +- .../SqlLab/components/StatusBar/StatusBar.test.tsx | 14 +- .../src/SqlLab/components/StatusBar/index.tsx | 11 +- .../SqlLab/components/TabbedSqlEditors/index.tsx | 4 +- .../src/SqlLab/components/TableElement/index.tsx | 4 +- .../TableExploreTree/TreeNodeRenderer.tsx | 3 +- .../SqlLab/components/TableExploreTree/index.tsx | 3 +- .../components/TableExploreTree/useTreeData.ts | 2 +- .../src/SqlLab/components/TablePreview/index.tsx | 5 +- .../components/TemplateParamsEditor/index.tsx | 4 +- superset-frontend/src/SqlLab/constants.ts | 2 +- superset-frontend/src/SqlLab/fixtures.ts | 2 +- .../src/SqlLab/reducers/getInitialState.ts | 2 +- superset-frontend/src/SqlLab/reducers/sqlLab.ts | 2 +- .../src/SqlLab/utils/newQueryTabName.ts | 2 +- .../DeckglLayerVisibilityCustomizationPlugin.tsx | 4 +- .../components/DeckglLayerVisibility/index.ts | 2 +- .../DynamicGroupBy/DynamicGroupByPlugin.tsx | 2 +- .../components/DynamicGroupBy/controlPanel.ts | 2 +- .../components/DynamicGroupBy/index.ts | 2 +- .../TimeColumn/TimeColumnFilterPlugin.tsx | 4 +- .../components/TimeColumn/controlPanel.ts | 2 +- .../components/TimeColumn/index.ts | 2 +- .../components/TimeGrain/TimeGrainFilterPlugin.tsx | 2 +- .../components/TimeGrain/controlPanel.ts | 2 +- .../components/TimeGrain/index.ts | 2 +- .../src/chartCustomizations/components/common.ts | 2 +- .../src/components/AlteredSliceTag/index.tsx | 2 +- .../src/components/AuditInfo/index.tsx | 2 +- superset-frontend/src/components/Chart/Chart.tsx | 7 +- .../Chart/ChartContextMenu/ChartContextMenu.tsx | 4 +- .../src/components/Chart/ChartRenderer.tsx | 4 +- .../components/Chart/DisabledMenuItemTooltip.tsx | 2 +- .../src/components/Chart/DrillBy/DrillByChart.tsx | 2 +- .../src/components/Chart/DrillBy/DrillByModal.tsx | 5 +- .../components/Chart/DrillBy/DrillBySubmenu.tsx | 4 +- .../Chart/DrillBy/useDisplayModeToggle.tsx | 4 +- .../Chart/DrillBy/useResultsTableView.tsx | 3 +- .../Chart/DrillDetail/DrillDetailModal.tsx | 4 +- .../Chart/DrillDetail/DrillDetailPane.tsx | 6 +- .../Chart/DrillDetail/DrillDetailTableControls.tsx | 4 +- .../src/components/Chart/DrillDetail/types.ts | 2 +- .../components/Chart/MenuItemWithTruncation.tsx | 2 +- .../src/components/Chart/chartAction.ts | 2 +- .../src/components/Chart/chartReducer.ts | 2 +- .../Chart/useDrillDetailMenuItems/index.tsx | 4 +- .../src/components/CopyToClipboard/index.tsx | 4 +- .../src/components/CrudThemeProvider.tsx | 2 +- .../src/components/DatabaseSelector/index.tsx | 4 +- .../src/components/DatabaseSelector/styles.ts | 2 +- .../Datasource/ChangeDatasourceModal/index.tsx | 5 +- .../Datasource/DatasourceModal/index.tsx | 5 +- .../Datasource/FoldersEditor/TreeItem.styles.ts | 2 +- .../Datasource/FoldersEditor/TreeItem.tsx | 5 +- .../components/DragOverlayContent.tsx | 2 +- .../components/FoldersToolbarComponent.tsx | 2 +- .../FoldersEditor/components/ResetConfirmModal.tsx | 2 +- .../Datasource/FoldersEditor/folderOperations.ts | 2 +- .../Datasource/FoldersEditor/folderValidation.ts | 2 +- .../FoldersEditor/hooks/useDragHandlers.ts | 2 +- .../FoldersEditor/hooks/useItemHeights.ts | 2 +- .../components/Datasource/FoldersEditor/styles.tsx | 2 +- .../components/CollectionTable/index.tsx | 4 +- .../DatasourceEditor/DatasourceEditor.tsx | 10 +- .../components/DashboardLinksExternal/index.tsx | 2 +- .../components/DatasetUsageTab/index.tsx | 4 +- .../tests/DatasourceEditorCurrency.test.tsx | 2 +- .../Datasource/components/Field/index.tsx | 2 +- .../Datasource/components/Fieldset/index.tsx | 2 +- .../src/components/Datasource/utils/index.ts | 2 +- .../src/components/Datasource/utils/utils.test.tsx | 2 +- .../src/components/DynamicPlugins/index.tsx | 2 +- .../src/components/ErrorBoundary/index.tsx | 2 +- .../ErrorMessage/BasicErrorAlert.test.tsx | 2 +- .../components/ErrorMessage/BasicErrorAlert.tsx | 6 +- .../ErrorMessage/DatabaseErrorMessage.tsx | 4 +- .../ErrorMessage/DatasetNotFoundErrorMessage.tsx | 2 +- .../src/components/ErrorMessage/ErrorAlert.tsx | 5 +- .../ErrorMessage/ErrorMessageWithStackTrace.tsx | 2 +- .../ErrorMessage/FrontendNetworkErrorMessage.tsx | 2 +- .../ErrorMessage/InvalidSQLErrorMessage.tsx | 2 +- .../src/components/ErrorMessage/IssueCode.tsx | 4 +- .../ErrorMessage/MarshmallowErrorMessage.tsx | 2 +- .../ErrorMessage/OAuth2RedirectMessage.tsx | 2 +- .../ErrorMessage/ParameterErrorMessage.tsx | 4 +- .../ErrorMessage/TimeoutErrorMessage.tsx | 4 +- .../src/components/FilterableTable/utils.tsx | 2 +- .../src/components/GridTable/Header.tsx | 4 +- .../src/components/GridTable/HeaderMenu.tsx | 4 +- .../src/components/GridTable/index.tsx | 2 +- .../src/components/ImportModal/ErrorAlert.tsx | 5 +- .../components/ImportModal/ImportErrorAlert.tsx | 2 +- .../src/components/ImportModal/index.tsx | 4 +- .../src/components/ImportModal/styles.ts | 2 +- .../src/components/JsonModal/index.tsx | 2 +- .../src/components/LastQueriedLabel/index.tsx | 4 +- .../src/components/ListView/ActionsBar.tsx | 2 +- .../src/components/ListView/CardCollection.tsx | 2 +- .../src/components/ListView/CardSortSelect.tsx | 4 +- .../src/components/ListView/CrossLinks.tsx | 2 +- .../src/components/ListView/CrossLinksTooltip.tsx | 4 +- .../src/components/ListView/Filters/Base.ts | 2 +- .../src/components/ListView/Filters/DateRange.tsx | 2 +- .../components/ListView/Filters/NumericalRange.tsx | 4 +- .../src/components/ListView/Filters/Search.tsx | 4 +- .../src/components/ListView/Filters/Select.tsx | 2 +- .../src/components/ListView/Filters/index.tsx | 2 +- .../src/components/ListView/ListView.tsx | 5 +- .../src/components/MessageToasts/Toast.tsx | 9 +- .../components/MessageToasts/ToastPresenter.tsx | 2 +- .../components/Modal/CollapsibleModalSection.tsx | 2 +- .../src/components/Modal/ModalFormField.tsx | 2 +- .../src/components/Modal/StandardModal.tsx | 4 +- .../src/components/Modal/useModalValidation.tsx | 4 +- .../src/components/ModalTitleWithIcon/index.tsx | 2 +- .../src/components/PanelToolbar/index.tsx | 42 +- .../src/components/ResizableSidebar/index.tsx | 2 +- .../src/components/RowCountLabel/index.tsx | 2 +- .../components/SQLEditorWithValidation/index.tsx | 4 +- .../StreamingExportModal/StreamingExportModal.tsx | 4 +- .../src/components/TableSelector/index.tsx | 4 +- superset-frontend/src/components/Tag/index.tsx | 2 +- superset-frontend/src/components/Tag/utils.tsx | 2 +- .../src/components/TagsList/index.tsx | 2 +- .../ViewListExtension/ViewListExtension.test.tsx | 228 +- .../src/components/ViewListExtension/index.tsx | 24 +- superset-frontend/src/core/commands/index.ts | 44 +- .../src/core/editors/AceEditorProvider.test.tsx | 2 +- .../src/core/editors/AceEditorProvider.tsx | 7 + superset-frontend/src/core/editors/EditorHost.tsx | 10 +- .../src/core/editors/EditorProviders.test.ts | 88 +- .../src/core/editors/EditorProviders.ts | 37 +- superset-frontend/src/core/editors/index.ts | 69 +- superset-frontend/src/core/extensions/index.ts | 6 +- superset-frontend/src/core/index.ts | 16 +- superset-frontend/src/core/menus/index.test.ts | 124 ++ superset-frontend/src/core/menus/index.ts | 83 + superset-frontend/src/core/models.ts | 6 +- superset-frontend/src/core/sqlLab/models.ts | 5 +- superset-frontend/src/core/utils.ts | 2 +- superset-frontend/src/core/views/index.test.ts | 112 + superset-frontend/src/core/views/index.ts | 83 + .../dashboard/actions/chartCustomizationActions.ts | 2 +- .../src/dashboard/actions/dashboardInfo.ts | 2 +- .../src/dashboard/actions/dashboardLayout.ts | 2 +- .../src/dashboard/actions/dashboardState.ts | 4 +- .../src/dashboard/actions/sliceEntities.ts | 2 +- .../components/AddSliceCard/AddSliceCard.tsx | 4 +- .../src/dashboard/components/AnchorLink/index.tsx | 2 +- .../components/AutoRefreshIndicator/index.tsx | 3 +- .../AutoRefreshStatus/StatusIndicatorDot.tsx | 2 +- .../AutoRefreshStatus/StatusTooltipContent.tsx | 2 +- .../components/BuilderComponentPane/index.tsx | 4 +- .../components/ColorSchemeControlWrapper.tsx | 2 +- .../src/dashboard/components/ColorSchemeSelect.tsx | 4 +- .../components/CustomizationsBadge/index.tsx | 4 +- .../src/dashboard/components/Dashboard.tsx | 2 +- .../DashboardBuilder/DashboardBuilder.tsx | 22 +- .../DashboardBuilder/DashboardWrapper.tsx | 2 +- .../src/dashboard/components/DashboardGrid.tsx | 3 +- .../dashboard/components/EmbeddedModal/index.tsx | 5 +- .../components/FiltersBadge/DetailsPanel/index.tsx | 4 +- .../FiltersBadge/FilterIndicator/index.tsx | 2 +- .../dashboard/components/FiltersBadge/Styles.tsx | 2 +- .../dashboard/components/FiltersBadge/index.tsx | 4 +- .../src/dashboard/components/Header/index.tsx | 3 +- .../components/Header/useDashboardMetadataBar.tsx | 2 +- .../Header/useHeaderActionsDropdownMenu.tsx | 2 +- .../src/dashboard/components/IconButton.tsx | 2 +- .../src/dashboard/components/MissingChart.tsx | 2 +- .../OverwriteConfirm/OverwriteConfirmModal.tsx | 4 +- .../PropertiesModal/PropertiesModal.test.tsx | 2 +- .../dashboard/components/PropertiesModal/index.tsx | 2 +- .../PropertiesModal/sections/AccessSection.tsx | 2 +- .../PropertiesModal/sections/AdvancedSection.tsx | 4 +- .../PropertiesModal/sections/BasicInfoSection.tsx | 2 +- .../sections/CertificationSection.tsx | 2 +- .../PropertiesModal/sections/RefreshSection.tsx | 2 +- .../PropertiesModal/sections/StylingSection.tsx | 5 +- .../dashboard/components/PublishedStatus/index.tsx | 2 +- .../dashboard/components/RefreshButton/index.tsx | 3 +- .../RefreshFrequency/RefreshFrequencySelect.tsx | 4 +- .../dashboard/components/RefreshIntervalModal.tsx | 4 +- .../src/dashboard/components/SaveModal.tsx | 4 +- .../src/dashboard/components/SliceAdder.tsx | 4 +- .../src/dashboard/components/SliceHeader/index.tsx | 9 +- .../ViewResultsModalTrigger.tsx | 4 +- .../components/SliceHeaderControls/index.tsx | 4 +- .../components/URLShortLinkButton/index.tsx | 4 +- .../src/dashboard/components/dnd/DragDroppable.tsx | 11 +- .../src/dashboard/components/dnd/DragHandle.tsx | 2 +- .../components/filterscope/FilterScope.test.tsx | 2 +- .../components/filterscope/FilterScopeModal.tsx | 2 +- .../components/filterscope/FilterScopeSelector.tsx | 3 +- .../filterscope/renderFilterScopeTreeNodes.tsx | 2 +- .../dashboard/components/filterscope/treeIcons.tsx | 2 +- .../components/gridComponents/Chart/Chart.test.tsx | 30 + .../components/gridComponents/Chart/Chart.tsx | 4 +- .../gridComponents/ChartHolder/ChartHolder.tsx | 2 +- .../components/gridComponents/Column/Column.tsx | 5 +- .../components/gridComponents/Divider/Divider.tsx | 2 +- .../DynamicComponent/DynamicComponent.tsx | 2 +- .../components/gridComponents/Header/Header.tsx | 2 +- .../gridComponents/Markdown/Markdown.test.tsx | 26 +- .../gridComponents/Markdown/Markdown.tsx | 3 +- .../components/gridComponents/Row/Row.tsx | 29 +- .../components/gridComponents/Tab/Tab.tsx | 3 +- .../components/gridComponents/Tabs/Tabs.tsx | 3 +- .../gridComponents/TabsRenderer/TabsRenderer.tsx | 2 +- .../gridComponents/new/DraggableNewComponent.tsx | 2 +- .../components/gridComponents/new/NewColumn.tsx | 2 +- .../components/gridComponents/new/NewDivider.tsx | 2 +- .../components/gridComponents/new/NewHeader.tsx | 2 +- .../components/gridComponents/new/NewMarkdown.tsx | 2 +- .../components/gridComponents/new/NewRow.tsx | 2 +- .../components/gridComponents/new/NewTabs.tsx | 2 +- .../components/menu/BackgroundStyleDropdown.tsx | 4 +- .../components/menu/DownloadMenuItems/index.tsx | 3 +- .../src/dashboard/components/menu/HoverMenu.tsx | 2 +- .../components/menu/MarkdownModeDropdown.tsx | 2 +- .../components/menu/ShareMenuItems/index.tsx | 3 +- .../dashboard/components/menu/WithPopoverMenu.tsx | 17 +- .../nativeFilters/ConfigModal/ModalFooter.tsx | 5 +- .../nativeFilters/ConfigModal/SharedStyles.tsx | 2 +- .../FilterBar/ActionButtons/index.tsx | 4 +- .../FilterBar/CrossFilters/CrossFilter.tsx | 2 +- .../FilterBar/CrossFilters/CrossFilterTag.tsx | 2 +- .../FilterBar/CrossFilters/CrossFilterTitle.tsx | 4 +- .../ScopingModal/ChartsScopingListPanel.tsx | 4 +- .../CrossFilters/ScopingModal/ScopingModal.tsx | 2 +- .../ScopingModal/ScopingModalContent.tsx | 2 +- .../CrossFilters/ScopingModal/ScopingTreePanel.tsx | 5 +- .../FilterBar/CrossFilters/VerticalCollapse.tsx | 4 +- .../nativeFilters/FilterBar/CrossFilters/styles.ts | 2 +- .../CustomizationsOutOfScopeCollapsible/index.tsx | 4 +- .../FilterBar/FilterBarSettings/index.tsx | 4 +- .../FilterControls/FilterControlShared.tsx | 2 +- .../FilterBar/FilterControls/FilterControls.tsx | 9 +- .../FilterControls/FilterDivider.stories.tsx | 2 +- .../FilterBar/FilterControls/FilterDivider.tsx | 2 +- .../FilterBar/FilterControls/FilterValue.tsx | 4 +- .../FilterBar/FilterControls/GroupByFilterCard.tsx | 9 +- .../FilterBar/FiltersDropdownContent/index.tsx | 2 +- .../FiltersOutOfScopeCollapsible/index.tsx | 4 +- .../nativeFilters/FilterBar/Header/index.tsx | 4 +- .../nativeFilters/FilterBar/Horizontal.tsx | 4 +- .../nativeFilters/FilterBar/Vertical.tsx | 4 +- .../components/nativeFilters/FilterBar/index.tsx | 2 +- .../nativeFilters/FilterBar/keyValue.tsx | 2 +- .../nativeFilters/FilterCard/DependenciesRow.tsx | 4 +- .../nativeFilters/FilterCard/NameRow.tsx | 2 +- .../nativeFilters/FilterCard/ScopeRow.tsx | 4 +- .../components/nativeFilters/FilterCard/Styles.ts | 2 +- .../nativeFilters/FilterCard/TypeRow.tsx | 2 +- .../nativeFilters/FilterCard/useFilterScope.ts | 2 +- .../ConfigModalContent/ConfigModalContent.tsx | 2 +- .../ConfigModalSidebar/ConfigModalSidebar.tsx | 240 +- .../FiltersConfigModal/DividerConfigForm.tsx | 4 +- .../FiltersConfigModal/DraggableFilter.test.tsx | 109 +- .../FiltersConfigModal/DraggableFilter.tsx | 124 +- .../FiltersConfigModal/FilterConfigPane.test.tsx | 26 +- .../FiltersConfigModal/FilterConfigurePane.tsx | 2 +- .../FiltersConfigModal/FilterTitleContainer.tsx | 106 +- .../FiltersConfigModal/FilterTitlePane.tsx | 4 +- .../FiltersConfigForm/CollapsibleControl.tsx | 2 +- .../FiltersConfigForm/ColumnSelect.tsx | 2 +- .../FiltersConfigForm/DatasetSelect.tsx | 2 +- .../FiltersConfigForm/DefaultValue.tsx | 2 +- .../FiltersConfigForm/DependencyList.tsx | 4 +- .../FiltersConfigForm/FilterScope/FilterScope.tsx | 2 +- .../FiltersConfigForm/FilterScope/ScopingTree.tsx | 2 +- .../FiltersConfigForm/FilterScope/state.ts | 2 +- .../FiltersConfigForm/FilterScope/utils.ts | 4 +- .../FiltersConfigForm/FiltersConfigForm.tsx | 6 +- .../FiltersConfigForm/RemovedFilter.tsx | 4 +- .../FiltersConfigForm/constants.ts | 2 +- .../FiltersConfigForm/getControlItemsMap.tsx | 4 +- .../FiltersConfigModal/FiltersConfigForm/state.ts | 2 +- .../FiltersConfigForm/utils.test.ts | 2 +- .../FiltersConfigModal/FiltersConfigForm/utils.ts | 2 +- .../FiltersConfigModal/FiltersConfigModal.test.tsx | 180 +- .../FiltersConfigModal/FiltersConfigModal.tsx | 17 +- .../Footer/CancelConfirmationAlert.tsx | 4 +- .../FiltersConfigModal/Footer/Footer.tsx | 2 +- .../FiltersConfigModal/ItemTitleContainer.tsx | 71 +- .../FiltersConfigModal/ItemTitlePane.tsx | 2 +- .../FiltersConfigModal/NewItemDropdown.tsx | 4 +- .../hooks/useFilterOperations.ts | 2 +- .../FiltersConfigModal/hooks/useModalSaveLogic.ts | 2 +- .../nativeFilters/FiltersConfigModal/utils.ts | 2 +- .../dashboard/components/nativeFilters/utils.ts | 2 +- .../components/resizable/ResizableContainer.tsx | 2 +- .../src/dashboard/containers/DashboardPage.tsx | 4 +- .../src/dashboard/hooks/useDownloadScreenshot.ts | 4 +- .../src/dashboard/reducers/sliceEntities.ts | 2 +- superset-frontend/src/dashboard/styles.ts | 2 +- superset-frontend/src/dashboard/types.ts | 2 +- .../src/dashboard/util/backgroundStyleOptions.ts | 2 +- .../src/dashboard/util/getFilterFieldNodesTree.ts | 2 +- .../src/dashboard/util/getFilterScopeNodesTree.ts | 2 +- .../src/dashboard/util/getSliceHeaderTooltip.tsx | 2 +- .../src/dashboard/util/headerStyleOptions.ts | 2 +- .../src/dashboard/util/newComponentFactory.ts | 2 +- .../dashboard/util/updateComponentParentsList.ts | 2 +- .../util/useFilterFocusHighlightStyles.ts | 2 +- .../src/embedded/EmbeddedContextProviders.tsx | 2 +- superset-frontend/src/embedded/index.tsx | 9 +- superset-frontend/src/embedded/utils.ts | 2 +- .../src/explore/actions/exploreActions.ts | 2 +- .../src/explore/actions/saveModalActions.ts | 2 +- .../src/explore/components/ChartPills.tsx | 2 +- .../src/explore/components/Control.tsx | 2 +- .../src/explore/components/ControlHeader.tsx | 4 +- .../components/ControlPanelsContainer.test.tsx | 2 +- .../explore/components/ControlPanelsContainer.tsx | 9 +- .../explore/components/DataTableControl/index.tsx | 8 +- .../DataTableControl/useTableColumns.test.ts | 2 +- .../components/DataTablesPane/DataTablesPane.tsx | 4 +- .../components/DataTableControls.tsx | 4 +- .../components/ResultsPaneOnDashboard.tsx | 4 +- .../DataTablesPane/components/SamplesPane.tsx | 6 +- .../components/SingleQueryResultPane.tsx | 2 +- .../DataTablesPane/components/useResultsPane.tsx | 4 +- .../src/explore/components/DataTablesPane/types.ts | 2 +- .../DatasourcePanelDragOption/index.tsx | 2 +- .../DatasourcePanel/DatasourcePanelItem.tsx | 4 +- .../components/DatasourcePanel/fixtures.tsx | 2 +- .../explore/components/DatasourcePanel/index.tsx | 5 +- .../DatasourcePanel/transformDatasourceFolders.ts | 2 +- .../src/explore/components/EmbedCodeContent.tsx | 3 +- .../src/explore/components/ExploreAlert.tsx | 2 +- .../components/ExploreChartHeader/index.tsx | 5 +- .../ExploreChartHeader/useExploreMetadataBar.tsx | 4 +- .../explore/components/ExploreChartPanel/index.tsx | 5 +- .../explore/components/ExploreContainer/index.tsx | 2 +- .../explore/components/ExploreContentPopover.tsx | 2 +- .../components/ExploreViewContainer/index.tsx | 5 +- .../components/ExportToCSVDropdown/index.tsx | 4 +- .../explore/components/PropertiesModal/index.tsx | 2 +- .../explore/components/RunQueryButton/index.tsx | 4 +- .../src/explore/components/SaveModal.tsx | 7 +- .../AnnotationLayerControl/AnnotationLayer.tsx | 4 +- .../AnnotationLayerControl/AnnotationTypes.ts | 2 +- .../controls/AnnotationLayerControl/index.tsx | 4 +- .../explore/components/controls/BoundsControl.tsx | 4 +- .../components/controls/CheckboxControl.tsx | 2 +- .../controls/CollectionControl/index.tsx | 4 +- .../ColorBreakpointOption.tsx | 2 +- .../ColorBreakpointPopoverControl.tsx | 4 +- .../controls/ColorBreakpointsControl/index.tsx | 4 +- .../ColorSchemeControl/ColorSchemeLabel.tsx | 2 +- .../controls/ColorSchemeControl/index.tsx | 4 +- .../ColumnConfigControl/ColumnConfigControl.tsx | 6 +- .../ColumnConfigControl/ColumnConfigItem.tsx | 2 +- .../ColumnConfigControl/ColumnConfigPopover.tsx | 2 +- .../ControlForm/ControlFormItem.tsx | 2 +- .../ColumnConfigControl/ControlForm/index.tsx | 2 +- .../controls/ColumnConfigControl/constants.tsx | 4 +- .../controls/ColumnConfigControl/types.ts | 2 +- .../components/controls/ComparisonRangeLabel.tsx | 4 +- .../ConditionalFormattingControl.tsx | 4 +- .../FormattingPopoverContent.test.tsx | 2 +- .../FormattingPopoverContent.tsx | 6 +- .../ConditionalFormattingControl/constants.ts | 2 +- .../controls/ConditionalFormattingControl/types.ts | 2 +- .../controls/ContourControl/ContourOption.tsx | 4 +- .../ContourControl/ContourPopoverControl.tsx | 4 +- .../components/controls/ContourControl/index.tsx | 4 +- .../controls/CurrencyControl/CurrencyControl.tsx | 4 +- .../components/controls/CustomListItem/index.tsx | 2 +- .../DatasourceControl/DatasourceControl.test.tsx | 268 +-- .../controls/DatasourceControl/index.tsx | 4 +- .../controls/DateFilterControl/DateFilterLabel.tsx | 9 +- .../DateFilterControl/components/AdvancedFrame.tsx | 2 +- .../DateFilterControl/components/CalendarFrame.tsx | 2 +- .../DateFilterControl/components/CommonFrame.tsx | 2 +- .../components/CurrentCalendarFrame.tsx | 2 +- .../DateFilterControl/components/CustomFrame.tsx | 2 +- .../components/DateFunctionTooltip.tsx | 4 +- .../DateFilterControl/components/DateLabel.tsx | 4 +- .../controls/DateFilterControl/utils/constants.ts | 2 +- .../DndColumnSelectControl/ColumnSelectPopover.tsx | 5 +- .../ColumnSelectPopoverTrigger.tsx | 2 +- .../DndAdhocFilterOption.tsx | 2 +- .../DndColumnMetricSelect.tsx | 4 +- .../DndColumnSelectControl/DndColumnSelect.tsx | 4 +- .../DndColumnSelectPopoverTitle.tsx | 4 +- .../DndFilterSelect.test.tsx | 2 +- .../DndColumnSelectControl/DndFilterSelect.tsx | 3 +- .../DndColumnSelectControl/DndMetricSelect.tsx | 6 +- .../DndColumnSelectControl/DndSelectLabel.tsx | 2 +- .../controls/DndColumnSelectControl/Option.tsx | 4 +- .../DndColumnSelectControl/OptionWrapper.tsx | 2 +- .../DndColumnSelectControl/useResizeButton.tsx | 2 +- .../DndColumnSelectControl/utils/optionSelector.ts | 2 +- .../FilterControl/AdhocFilterControl/index.tsx | 6 +- .../FilterControl/AdhocFilterEditPopover/index.tsx | 6 +- .../index.tsx | 4 +- .../useAdvancedDataTypes.ts | 2 +- .../AdhocFilterEditPopoverSqlTabContent.test.tsx | 84 +- .../AdhocFilterEditPopoverSqlTabContent/index.tsx | 12 +- .../utils/useDatePickerInAdhocFilter.tsx | 2 +- .../controls/FixedOrMetricControl/index.tsx | 2 +- .../components/controls/JSEditorControl.test.tsx | 125 ++ .../components/controls/JSEditorControl.tsx | 105 + .../controls/LayerConfigsControl/FlatLayerTree.tsx | 4 +- .../LayerConfigsControl/LayerConfigsControl.tsx | 4 +- .../LayerConfigsPopoverContent.tsx | 4 +- .../controls/MapViewControl/ExtentTag.tsx | 2 +- .../controls/MapViewControl/MapViewControl.tsx | 4 +- .../MapViewControl/MapViewPopoverContent.tsx | 4 +- .../controls/MatrixifyDimensionControl.tsx | 2 +- .../MetricControl/AdhocMetricEditPopover/index.tsx | 25 +- .../MetricControl/AdhocMetricEditPopoverTitle.tsx | 4 +- .../MetricControl/AdhocMetricPopoverTrigger.tsx | 2 +- .../controls/MetricControl/MetricsControl.tsx | 2 +- .../components/controls/NumberControl/index.tsx | 2 +- .../components/controls/OptionControls/index.tsx | 4 +- .../controls/SelectAsyncControl/index.tsx | 2 +- .../explore/components/controls/SelectControl.tsx | 4 +- .../explore/components/controls/SpatialControl.tsx | 2 +- .../components/controls/TextAreaControl.tsx | 4 +- .../controls/TimeSeriesColumnControl/index.tsx | 4 +- .../src/explore/components/controls/ViewQuery.tsx | 4 +- .../explore/components/controls/ViewQueryModal.tsx | 5 +- .../components/controls/ViewQueryModalFooter.tsx | 2 +- .../components/controls/ViewportControl.tsx | 2 +- .../controls/VizTypeControl/FastVizSwitcher.tsx | 4 +- .../components/controls/VizTypeControl/VizTile.tsx | 4 +- .../controls/VizTypeControl/VizTypeGallery.tsx | 4 +- .../controls/VizTypeControl/constants.tsx | 2 +- .../components/controls/VizTypeControl/index.tsx | 4 +- .../ZoomConfigControl/ZoomConfigControl.tsx | 4 +- .../ZoomConfigControl/ZoomConfigsChart.tsx | 2 +- .../src/explore/components/controls/index.ts | 2 + .../components/controls/withAsyncVerification.tsx | 2 +- .../src/explore/components/optionRenderers.tsx | 2 +- .../DashboardsSubMenu.tsx | 4 +- .../useExploreAdditionalActionsMenu/index.tsx | 3 +- superset-frontend/src/explore/constants.ts | 2 +- .../src/explore/controlPanels/Separator.ts | 2 +- .../src/explore/controlPanels/sections.tsx | 2 +- .../src/explore/controlUtils/controlUtils.test.tsx | 2 +- .../src/explore/controlUtils/getColumnKeywords.tsx | 2 +- superset-frontend/src/explore/controls.tsx | 2 +- superset-frontend/src/explore/fixtures.tsx | 2 +- .../src/extensions/ExtensionPlaceholder.tsx | 2 +- .../src/extensions/ExtensionsContext.test.tsx | 150 -- .../src/extensions/ExtensionsContext.tsx | 93 - .../src/extensions/ExtensionsContextUtils.test.ts | 74 - .../src/extensions/ExtensionsContextUtils.ts | 32 - .../src/extensions/ExtensionsList.test.tsx | 17 - .../src/extensions/ExtensionsList.tsx | 31 +- .../src/extensions/ExtensionsLoader.test.ts | 113 + .../src/extensions/ExtensionsLoader.ts | 161 ++ .../src/extensions/ExtensionsManager.test.ts | 572 ----- .../src/extensions/ExtensionsManager.ts | 355 --- .../src/extensions/ExtensionsStartup.test.tsx | 74 +- .../src/extensions/ExtensionsStartup.tsx | 56 +- .../src/features/alerts/AlertReportModal.test.tsx | 463 +++- .../src/features/alerts/AlertReportModal.tsx | 49 +- .../features/alerts/buildErrorTooltipMessage.tsx | 2 +- .../alerts/components/AlertReportCronScheduler.tsx | 4 +- .../features/alerts/components/AlertStatusIcon.tsx | 4 +- .../alerts/components/NotificationMethod.tsx | 4 +- .../features/alerts/components/RecipientIcon.tsx | 2 +- superset-frontend/src/features/alerts/types.ts | 7 + .../src/features/allEntities/AllEntitiesTable.tsx | 4 +- .../annotationLayers/AnnotationLayerModal.tsx | 4 +- .../src/features/annotations/AnnotationModal.tsx | 4 +- .../src/features/charts/ChartCard.tsx | 4 +- .../src/features/cssTemplates/CssTemplateModal.tsx | 4 +- .../src/features/dashboards/DashboardCard.tsx | 2 +- .../DatabaseConnectionForm/CommonParameters.tsx | 4 +- .../DatabaseConnectionForm/EncryptedField.tsx | 4 +- .../DatabaseConnectionForm/OAuth2ClientField.tsx | 2 +- .../DatabaseConnectionForm/TableCatalog.tsx | 4 +- .../DatabaseConnectionForm/ValidatedInputField.tsx | 2 +- .../DatabaseModal/DatabaseConnectionForm/index.tsx | 2 +- .../databases/DatabaseModal/ExtraOptions.test.tsx | 2 +- .../databases/DatabaseModal/ExtraOptions.tsx | 4 +- .../databases/DatabaseModal/ModalHeader.tsx | 10 +- .../databases/DatabaseModal/SSHTunnelForm.tsx | 4 +- .../databases/DatabaseModal/SSHTunnelSwitch.tsx | 4 +- .../databases/DatabaseModal/SqlAlchemyForm.tsx | 4 +- .../src/features/databases/DatabaseModal/index.tsx | 5 +- .../src/features/databases/DatabaseModal/styles.ts | 2 +- .../databases/UploadDataModel/ColumnsPreview.tsx | 4 +- .../features/databases/UploadDataModel/index.tsx | 4 +- .../features/databases/UploadDataModel/styles.ts | 2 +- .../AddDataset/DatasetPanel/DatasetPanel.tsx | 5 +- .../AddDataset/DatasetPanel/MessageContent.tsx | 4 +- .../datasets/AddDataset/DatasetPanel/index.tsx | 4 +- .../datasets/AddDataset/EditDataset/index.tsx | 4 +- .../features/datasets/AddDataset/Footer/index.tsx | 4 +- .../features/datasets/AddDataset/Header/index.tsx | 2 +- .../datasets/AddDataset/LeftPanel/index.tsx | 4 +- .../datasets/AddDataset/RightPanel/index.tsx | 2 +- .../src/features/datasets/DatasetLayout/index.tsx | 2 +- .../features/datasets/DatasetSelectLabel/index.tsx | 4 +- .../datasets/DuplicateDatasetModal.test.tsx | 2 +- .../features/datasets/DuplicateDatasetModal.tsx | 2 +- .../src/features/datasets/constants.ts | 2 +- .../src/features/datasets/hooks/useDatasetLists.ts | 4 +- .../datasets/hooks/useGetDatasetRelatedCounts.ts | 4 +- .../DatasetMetadataBar.skipped-stories.tsx | 2 +- .../datasets/metadataBar/useDatasetMetadataBar.tsx | 4 +- superset-frontend/src/features/datasets/styles.ts | 2 +- .../src/features/groups/GroupListModal.tsx | 2 +- superset-frontend/src/features/groups/utils.ts | 2 +- .../src/features/home/ActivityTable.tsx | 4 +- superset-frontend/src/features/home/ChartTable.tsx | 2 +- .../src/features/home/DashboardTable.tsx | 2 +- superset-frontend/src/features/home/EmptyState.tsx | 4 +- .../src/features/home/LanguagePicker.tsx | 4 +- superset-frontend/src/features/home/Menu.test.tsx | 115 +- superset-frontend/src/features/home/Menu.tsx | 2 +- superset-frontend/src/features/home/RightMenu.tsx | 9 +- .../src/features/home/SavedQueries.tsx | 4 +- superset-frontend/src/features/home/SubMenu.tsx | 9 +- .../src/features/home/commonMenuData.ts | 2 +- .../src/features/owners/OwnerSelectLabel/index.tsx | 2 +- .../src/features/queries/QueryPreviewModal.tsx | 4 +- .../features/queries/SavedQueryPreviewModal.tsx | 4 +- .../src/features/queries/SyntaxHighlighterCopy.tsx | 4 +- .../ReportModal/HeaderReportDropdown/index.tsx | 4 +- .../src/features/reports/ReportModal/actions.ts | 2 +- .../src/features/reports/ReportModal/index.tsx | 5 +- .../src/features/reports/ReportModal/styles.tsx | 2 +- .../src/features/rls/RowLevelSecurityModal.tsx | 4 +- superset-frontend/src/features/rls/constants.ts | 2 +- .../src/features/roles/RoleFormItems.tsx | 53 +- .../src/features/roles/RoleListAddModal.test.tsx | 45 +- .../src/features/roles/RoleListAddModal.tsx | 23 +- .../src/features/roles/RoleListDuplicateModal.tsx | 2 +- .../src/features/roles/RoleListEditModal.test.tsx | 328 ++- .../src/features/roles/RoleListEditModal.tsx | 179 +- superset-frontend/src/features/roles/types.ts | 12 +- superset-frontend/src/features/roles/utils.test.ts | 547 +++++ superset-frontend/src/features/roles/utils.ts | 165 ++ .../src/features/tags/BulkTagModal.tsx | 4 +- superset-frontend/src/features/tags/TagCard.tsx | 2 +- superset-frontend/src/features/tags/TagModal.tsx | 4 +- .../src/features/tasks/TaskPayloadPopover.tsx | 2 +- .../src/features/tasks/TaskStackTracePopover.tsx | 4 +- .../src/features/tasks/TaskStatusIcon.tsx | 3 +- .../src/features/themes/ThemeModal.tsx | 5 +- .../src/features/userInfo/UserInfoModal.tsx | 2 +- .../src/features/users/UserListModal.tsx | 2 +- superset-frontend/src/features/users/utils.ts | 2 +- .../components/Range/RangeFilterPlugin.stories.tsx | 2 +- .../components/Range/RangeFilterPlugin.test.tsx | 2 +- .../filters/components/Range/RangeFilterPlugin.tsx | 4 +- .../src/filters/components/Range/buildQuery.ts | 2 +- .../src/filters/components/Range/controlPanel.ts | 2 +- .../src/filters/components/Range/index.ts | 2 +- .../components/Select/SelectFilterPlugin.tsx | 8 +- .../filters/components/Select/buildQuery.test.ts | 2 +- .../src/filters/components/Select/buildQuery.ts | 2 +- .../src/filters/components/Select/controlPanel.ts | 2 +- .../src/filters/components/Select/index.ts | 2 +- .../filters/components/Select/transformProps.ts | 2 +- .../src/filters/components/Select/types.ts | 2 +- .../filters/components/Time/TimeFilterPlugin.tsx | 2 +- .../src/filters/components/Time/controlPanel.ts | 2 +- .../src/filters/components/Time/index.ts | 2 +- .../TimeColumn/TimeColumnFilterPlugin.tsx | 4 +- .../filters/components/TimeColumn/controlPanel.ts | 2 +- .../src/filters/components/TimeColumn/index.ts | 2 +- .../components/TimeGrain/TimeGrainFilterPlugin.tsx | 4 +- .../filters/components/TimeGrain/controlPanel.ts | 2 +- .../src/filters/components/TimeGrain/index.ts | 2 +- superset-frontend/src/filters/components/common.ts | 2 +- superset-frontend/src/filters/utils.test.ts | 2 +- superset-frontend/src/filters/utils.ts | 2 +- .../src/hooks/apiResources/datasets.ts | 2 +- .../hooks/useConfirmModal/useConfirmModal.test.tsx | 2 +- superset-frontend/src/hooks/useJsonTreeTheme.ts | 2 +- .../src/hooks/useThemeMenuItems.test.tsx | 2 +- superset-frontend/src/hooks/useThemeMenuItems.tsx | 4 +- .../src/hooks/useUnsavedChangesPrompt/index.ts | 2 +- superset-frontend/src/middleware/asyncEvent.ts | 2 +- superset-frontend/src/pages/ActionLog/index.tsx | 4 +- .../src/pages/AlertReportList/index.tsx | 4 +- superset-frontend/src/pages/AllEntities/index.tsx | 4 +- .../src/pages/AnnotationLayerList/index.tsx | 2 +- .../src/pages/AnnotationList/index.tsx | 4 +- superset-frontend/src/pages/Chart/index.tsx | 2 +- .../src/pages/ChartCreation/ChartCreation.test.tsx | 2 +- .../src/pages/ChartCreation/index.tsx | 4 +- superset-frontend/src/pages/ChartList/index.tsx | 5 +- .../src/pages/CssTemplateList/index.tsx | 2 +- .../DashboardList/DashboardList.behavior.test.tsx | 394 ++++ .../DashboardList/DashboardList.cardview.test.tsx | 417 ++++ .../DashboardList/DashboardList.listview.test.tsx | 402 ++++ .../DashboardList.permissions.test.tsx | 340 +++ .../src/pages/DashboardList/DashboardList.test.tsx | 391 ++-- .../DashboardList/DashboardList.testHelpers.tsx | 360 +++ .../src/pages/DashboardList/index.tsx | 4 +- superset-frontend/src/pages/DatabaseList/index.tsx | 4 +- superset-frontend/src/pages/DatasetList/index.tsx | 4 +- .../src/pages/ExecutionLogList/index.tsx | 4 +- .../src/pages/FileHandler/index.test.tsx | 22 +- superset-frontend/src/pages/FileHandler/index.tsx | 2 +- superset-frontend/src/pages/GroupsList/index.tsx | 2 +- superset-frontend/src/pages/Home/index.tsx | 4 +- superset-frontend/src/pages/Login/index.tsx | 4 +- .../src/pages/QueryHistoryList/index.tsx | 4 +- .../src/pages/RedirectWarning/index.tsx | 175 ++ .../src/pages/RedirectWarning/utils.test.ts | 124 ++ .../src/pages/RedirectWarning/utils.ts | 96 + superset-frontend/src/pages/Register/index.tsx | 4 +- .../src/pages/RolesList/RolesList.test.tsx | 27 +- superset-frontend/src/pages/RolesList/index.tsx | 75 +- .../src/pages/RowLevelSecurityList/index.tsx | 2 +- .../src/pages/SavedQueryList/index.tsx | 4 +- superset-frontend/src/pages/SqlLab/index.tsx | 2 +- superset-frontend/src/pages/Tags/index.tsx | 2 +- superset-frontend/src/pages/TaskList/index.tsx | 3 +- superset-frontend/src/pages/ThemeList/index.tsx | 5 +- superset-frontend/src/pages/UserInfo/index.tsx | 4 +- .../src/pages/UserRegistrations/index.tsx | 2 +- superset-frontend/src/pages/UsersList/index.tsx | 2 +- superset-frontend/src/preamble.ts | 4 +- superset-frontend/src/setup/setupClient.ts | 2 +- superset-frontend/src/theme/ThemeController.ts | 4 +- superset-frontend/src/theme/ThemeProvider.tsx | 2 +- .../src/theme/hooks/useThemeValidation.ts | 2 +- .../src/theme/tests/ThemeController.test.ts | 2 +- .../src/theme/tests/ThemeProvider.test.tsx | 2 +- .../theme/utils/themeStructureValidation.test.ts | 2 +- .../src/theme/utils/themeStructureValidation.ts | 2 +- superset-frontend/src/types/bootstrapTypes.ts | 9 +- superset-frontend/src/utils/downloadAsImage.tsx | 4 +- superset-frontend/src/utils/downloadAsPdf.ts | 4 +- superset-frontend/src/utils/export.test.ts | 4 +- superset-frontend/src/utils/export.ts | 2 +- superset-frontend/src/utils/fetchOptions.ts | 2 +- .../utils/getChartRequiredFieldsMissingMessage.ts | 2 +- superset-frontend/src/utils/pathUtils.test.ts | 71 + superset-frontend/src/utils/pathUtils.ts | 37 +- superset-frontend/src/views/App.tsx | 47 +- superset-frontend/src/views/CRUD/hooks.ts | 2 +- superset-frontend/src/views/CRUD/utils.tsx | 5 +- .../src/views/RootContextProviders.tsx | 17 +- superset-frontend/src/views/index.tsx | 2 +- superset-frontend/src/views/menu.tsx | 3 +- superset-frontend/src/views/routes.tsx | 11 + .../src/visualizations/TimeTable/TimeTable.tsx | 4 +- .../components/SparklineCell/SparklineCell.tsx | 2 +- .../TimeTable/config/controlPanel/controlPanel.ts | 2 +- .../config/transformProps/transformProps.test.ts | 2 +- .../src/visualizations/TimeTable/constants.ts | 2 +- .../src/visualizations/TimeTable/index.ts | 2 +- .../ExampleComponent/ExampleComponent.tsx | 2 +- superset-frontend/webpack.config.js | 7 +- superset-websocket/package-lock.json | 72 +- superset-websocket/package.json | 8 +- superset-websocket/utils/client-ws-app/.nvmrc | 2 +- superset/charts/schemas.py | 12 +- superset/commands/database/export.py | 9 + superset/commands/database/importers/v1/utils.py | 26 +- superset/commands/distributed_lock/acquire.py | 2 +- superset/commands/distributed_lock/base.py | 6 +- superset/commands/distributed_lock/release.py | 2 +- superset/commands/importers/v1/__init__.py | 4 + superset/commands/importers/v1/assets.py | 4 + superset/commands/importers/v1/utils.py | 19 + superset/commands/tasks/cancel.py | 2 +- superset/commands/tasks/internal_update.py | 2 +- superset/commands/tasks/prune.py | 2 +- superset/commands/tasks/submit.py | 2 +- superset/commands/tasks/update.py | 2 +- superset/config.py | 22 +- superset/connectors/sqla/models.py | 2 +- superset/core/api/core_api_injection.py | 159 +- superset/core/mcp/core_mcp_injection.py | 74 +- superset/daos/base.py | 4 +- superset/daos/tag.py | 7 + superset/daos/tasks.py | 2 +- superset/dashboards/api.py | 34 +- superset/databases/api.py | 14 + superset/databases/schemas.py | 62 + superset/db_engine_specs/base.py | 6 + superset/db_engine_specs/databend.py | 21 +- superset/distributed_lock/__init__.py | 2 +- superset/extensions/context.py | 90 + superset/extensions/contributions.py | 94 + superset/extensions/utils.py | 7 +- superset/importexport/api.py | 13 + superset/initialization/__init__.py | 18 +- superset/key_value/models.py | 2 +- superset/mcp_service/CLAUDE.md | 16 +- superset/mcp_service/app.py | 40 +- superset/mcp_service/chart/chart_utils.py | 289 ++- .../chart/prompts/create_chart_guided.py | 2 +- superset/mcp_service/chart/schemas.py | 188 +- superset/mcp_service/chart/tool/generate_chart.py | 196 +- superset/mcp_service/chart/tool/get_chart_data.py | 2 +- superset/mcp_service/chart/tool/get_chart_info.py | 2 +- .../mcp_service/chart/tool/get_chart_preview.py | 2 +- superset/mcp_service/chart/tool/list_charts.py | 2 +- superset/mcp_service/chart/tool/update_chart.py | 2 +- .../mcp_service/chart/tool/update_chart_preview.py | 2 +- .../chart/validation/schema_validator.py | 169 +- .../tool/add_chart_to_existing_dashboard.py | 2 +- .../dashboard/tool/generate_dashboard.py | 2 +- .../dashboard/tool/get_dashboard_info.py | 2 +- .../mcp_service/dashboard/tool/list_dashboards.py | 2 +- .../mcp_service/dataset/tool/get_dataset_info.py | 2 +- superset/mcp_service/dataset/tool/list_datasets.py | 2 +- .../explore/tool/generate_explore_link.py | 2 +- superset/mcp_service/server.py | 27 + superset/mcp_service/sql_lab/tool/execute_sql.py | 9 +- .../sql_lab/tool/open_sql_lab_with_context.py | 2 +- superset/mcp_service/system/prompts/quickstart.py | 2 +- superset/mcp_service/system/schemas.py | 7 + .../mcp_service/system/tool/get_instance_info.py | 13 +- superset/mcp_service/system/tool/get_schema.py | 2 +- superset/mcp_service/system/tool/health_check.py | 2 +- superset/models/core.py | 9 +- superset/models/dashboard.py | 2 +- superset/models/helpers.py | 2 +- superset/models/slice.py | 2 +- superset/models/sql_lab.py | 5 +- superset/models/task_subscribers.py | 2 +- superset/models/tasks.py | 4 +- superset/reports/notifications/email.py | 5 + superset/sql/execution/executor.py | 24 +- superset/tags/models.py | 2 +- superset/tasks/constants.py | 2 +- superset/tasks/context.py | 2 +- superset/tasks/decorators.py | 17 +- superset/tasks/locks.py | 4 +- superset/tasks/manager.py | 10 +- superset/tasks/scheduler.py | 2 +- superset/tasks/utils.py | 2 +- superset/templates/superset/spa.html | 5 + superset/utils/cache_manager.py | 34 +- superset/utils/json.py | 43 + superset/utils/link_redirect.py | 149 ++ superset/utils/logging_configurator.py | 7 + superset/utils/oauth2.py | 4 + superset/utils/pandas_postprocessing/geography.py | 6 +- superset/utils/pandas_postprocessing/resample.py | 2 +- superset/utils/webdriver.py | 34 +- superset/views/redirect.py | 76 + superset/viz.py | 36 +- tests/integration_tests/databases/api_tests.py | 91 + tests/integration_tests/fixtures/importexport.py | 26 + tests/integration_tests/security_tests.py | 1 + tests/integration_tests/tasks/api_tests.py | 4 +- .../tasks/commands/test_cancel.py | 2 +- .../tasks/commands/test_internal_update.py | 2 +- .../integration_tests/tasks/commands/test_prune.py | 2 +- .../tasks/commands/test_submit.py | 2 +- .../tasks/commands/test_update.py | 2 +- .../integration_tests/tasks/test_event_handlers.py | 2 +- .../tasks/test_subscription_visibility.py | 2 +- .../integration_tests/tasks/test_sync_join_wait.py | 6 +- tests/integration_tests/tasks/test_throttling.py | 2 +- tests/integration_tests/tasks/test_timeout.py | 2 +- .../integration_tests/views/test_redirect_view.py | 66 + tests/unit_tests/dao/base_dao_test.py | 2 +- tests/unit_tests/daos/test_tasks.py | 2 +- tests/unit_tests/databases/api_test.py | 70 + .../databases/commands/importers/v1/import_test.py | 123 ++ tests/unit_tests/databases/schema_tests.py | 106 + tests/unit_tests/db_engine_specs/test_base.py | 91 + tests/unit_tests/extension_tests.py | 21 +- tests/unit_tests/extensions/test_types.py | 84 +- tests/unit_tests/importexport/api_test.py | 52 + .../mcp_service/chart/test_new_chart_types.py | 929 ++++++++ .../mcp_service/chart/tool/test_generate_chart.py | 74 + .../mcp_service/sql_lab/tool/test_execute_sql.py | 2 +- .../system/tool/test_get_current_user.py | 5 + tests/unit_tests/mcp_service/test_mcp_server.py | 34 + tests/unit_tests/sql/execution/test_executor.py | 19 +- tests/unit_tests/tags/commands/update_test.py | 60 + tests/unit_tests/tasks/test_decorators.py | 2 +- tests/unit_tests/tasks/test_handlers.py | 6 +- tests/unit_tests/tasks/test_manager.py | 34 +- tests/unit_tests/tasks/test_timeout.py | 34 +- tests/unit_tests/tasks/test_utils.py | 2 +- tests/unit_tests/utils/json_tests.py | 139 ++ tests/unit_tests/utils/oauth2_tests.py | 56 + tests/unit_tests/utils/test_link_redirect.py | 143 ++ 1416 files changed, 23388 insertions(+), 9895 deletions(-) diff --cc superset-frontend/src/features/alerts/AlertReportModal.test.tsx index 1bf64bcf8a4,b025a27c018..a29a6d1322b --- a/superset-frontend/src/features/alerts/AlertReportModal.test.tsx +++ b/superset-frontend/src/features/alerts/AlertReportModal.test.tsx @@@ -913,760 -867,502 +921,1211 @@@ test('dashboard with no tabs disables t userEvent.click(screen.getByTestId('contents-panel')); await screen.findByText(/test dashboard/i); - const filterDropdown = screen.getByRole('combobox', { - name: /select filter/i, - }); - expect(filterDropdown).toBeInTheDocument(); + const tabSelector = document.querySelector('.ant-select-disabled'); + expect(tabSelector).toBeInTheDocument(); +}); - userEvent.click(filterDropdown); +test('dashboard with no tabs and no filters hides filter add link', async () => { + fetchMock.removeRoute(tabsEndpoint); + fetchMock.get(tabsEndpoint, noTabsResponse, { name: tabsEndpoint }); - const filterOption = await waitFor(() => { - const virtualList = document.querySelector('.rc-virtual-list'); - return within(virtualList as HTMLElement).getByText('Test Filter 1'); + render(<AlertReportModal {...generateMockedProps(true, true)} />, { + useRedux: true, }); - userEvent.click(filterOption); + userEvent.click(screen.getByTestId('contents-panel')); + await screen.findByText(/test dashboard/i); + // Wait for tabs fetch to complete await waitFor(() => { - const selectionItem = document.querySelector( - '.ant-select-selection-item[title="Test Filter 1"]', - ); - expect(selectionItem).toBeInTheDocument(); + expect( + fetchMock.callHistory.calls(tabsEndpoint).length, + ).toBeGreaterThan(0); }); - const selectContainer = filterDropdown.closest('.ant-select'); + // Tab selector should be disabled (no tabs) + const disabledSelects = document.querySelectorAll('.ant-select-disabled'); + expect(disabledSelects.length).toBeGreaterThanOrEqual(1); - const clearIcon = selectContainer?.querySelector( - '.ant-select-clear [aria-label="close-circle"]', - ); - expect(clearIcon).toBeInTheDocument(); - userEvent.click(clearIcon as Element); + // Filter Select should also be disabled (no filter options available) + expect(disabledSelects.length).toBeGreaterThanOrEqual(2); + + // "Apply another dashboard filter" link should NOT appear + // because noTabsResponse has empty native_filters ({}) + expect( + screen.queryByText(/apply another dashboard filter/i), + ).not.toBeInTheDocument(); +}); +test('dashboard switching resets tab and filter selections', async () => { + // Return dashboard options so user can switch + const dashboardOptions = { + result: [ + { text: 'Test Dashboard', value: 1 }, + { text: 'Other Dashboard', value: 99 }, + ], + count: 2, + }; + fetchMock.removeRoute(dashboardEndpoint); + fetchMock.get(dashboardEndpoint, dashboardOptions); + fetchMock.removeRoute(reportDashboardEndpoint); + fetchMock.get(reportDashboardEndpoint, dashboardOptions); + + // Dashboard 1 has tabs and filters + fetchMock.removeRoute(tabsEndpoint); + fetchMock.get(tabsEndpoint, tabsWithFilters, { name: tabsEndpoint }); + + // Dashboard 99 has no tabs + const tabs99 = 'glob:*/api/v1/dashboard/99/tabs'; + fetchMock.get(tabs99, noTabsResponse, { name: tabs99 }); + + render(<AlertReportModal {...generateMockedProps(true, true)} />, { + useRedux: true, + }); + + userEvent.click(screen.getByTestId('contents-panel')); + await screen.findByText(/test dashboard/i); + + // Wait for tabs to load from dashboard 1 await waitFor(() => { - const selectionItem = document.querySelector( - '.ant-select-selection-item[title="Test Filter 1"]', - ); - expect(selectionItem).not.toBeInTheDocument(); + expect(screen.getAllByText(/select tab/i)).toHaveLength(1); }); - userEvent.click(filterDropdown); + // Verify filters are available await waitFor(() => { - const virtualList = document.querySelector('.rc-virtual-list'); expect( - within(virtualList as HTMLElement).getByText('Test Filter 1'), + screen.getByRole('combobox', { name: /select filter/i }), ).toBeInTheDocument(); }); + + // Switch to "Other Dashboard" + const dashboardSelect = screen.getByRole('combobox', { + name: /dashboard/i, + }); + userEvent.clear(dashboardSelect); + userEvent.type(dashboardSelect, 'Other Dashboard{enter}'); + + // Tab selector should reset: "Other Dashboard" has no tabs, so disabled with placeholder + await waitFor(() => { + expect(screen.getByText(/select a tab/i)).toBeInTheDocument(); + }); + + // Filter row should reset to empty (no filter selected) + await waitFor(() => { + const filterSelects = screen.getAllByRole('combobox', { + name: /select filter/i, + }); + // Filter select should have no selected value (placeholder state) + filterSelects.forEach(select => { + const container = select.closest('.ant-select'); + expect( + container?.querySelector('.ant-select-selection-item'), + ).not.toBeInTheDocument(); + }); + }); + + // Restore dashboard endpoints + fetchMock.removeRoute(dashboardEndpoint); + fetchMock.get(dashboardEndpoint, { result: [] }); + fetchMock.removeRoute(reportDashboardEndpoint); + fetchMock.get(reportDashboardEndpoint, { result: [] }); + fetchMock.removeRoute(tabs99); }); -const setupAnchorMocks = ( - nativeFilters: Record<string, unknown>, - anchor = 'TAB-abc', - tabsOverride?: { - all_tabs: Record<string, string>; - tab_tree: { title: string; value: string }[]; - }, -) => { - const payloadWithAnchor = { - ...generateMockPayload(true), - extra: { dashboard: { anchor } }, - }; +test('different dashboard populates its own tabs and filters', async () => { + // Set up a report (id:99) that uses dashboard 99 instead of dashboard 1. + // This tests that the component correctly loads tabs and filters for a + // different dashboard (the "dashboard B has its own data" case). + const FETCH_REPORT_DASH99_ENDPOINT = 'glob:*/api/v1/report/99'; + fetchMock.get(FETCH_REPORT_DASH99_ENDPOINT, { + result: { + ...generateMockPayload(true), + id: 99, + type: 'Report', + dashboard: { id: 99, dashboard_title: 'Other Dashboard' }, + }, + }); - const defaultTabs = { - all_tabs: { [anchor]: `Tab ${anchor}` }, - tab_tree: [{ title: `Tab ${anchor}`, value: anchor }], + // Dashboard 99 has its own tabs (Tab Alpha, Tab Beta) and a Region Filter + const tabs99Endpoint = 'glob:*/api/v1/dashboard/99/tabs'; + const dash99Tabs = { + result: { + all_tabs: { TAB_A: 'Tab Alpha', TAB_B: 'Tab Beta' }, + tab_tree: [ + { title: 'Tab Alpha', value: 'TAB_A' }, + { title: 'Tab Beta', value: 'TAB_B' }, + ], + native_filters: { + all: [ + { + id: 'NATIVE_FILTER-R1', + name: 'Region Filter', + filterType: 'filter_select', + targets: [{ column: { name: 'region' }, datasetId: 3 }], + adhoc_filters: [], + }, + ], + TAB_A: [ + { + id: 'NATIVE_FILTER-R1', + name: 'Region Filter', + filterType: 'filter_select', + targets: [{ column: { name: 'region' }, datasetId: 3 }], + adhoc_filters: [], + }, + ], + TAB_B: [], + }, + }, }; - const tabs = tabsOverride ?? defaultTabs; + fetchMock.get(tabs99Endpoint, dash99Tabs, { name: 'tabs-99' }); - // Clear call history so waitFor assertions don't match calls from prior tests. - fetchMock.callHistory.clear(); + const props = generateMockedProps(true, true); + const dash99Props = { ...props, alert: { ...validAlert, id: 99 } }; - // Only replace the named routes that need anchor-specific overrides; - // unnamed related-endpoint routes (owners, database, etc.) stay intact. - fetchMock.removeRoute(FETCH_DASHBOARD_ENDPOINT); - fetchMock.removeRoute(FETCH_CHART_ENDPOINT); - fetchMock.removeRoute(tabsEndpoint); + render(<AlertReportModal {...dash99Props} />, { useRedux: true }); - fetchMock.get( - FETCH_DASHBOARD_ENDPOINT, - { result: payloadWithAnchor }, - { name: FETCH_DASHBOARD_ENDPOINT }, - ); - fetchMock.get( - FETCH_CHART_ENDPOINT, - { result: generateMockPayload(false) }, - { name: FETCH_CHART_ENDPOINT }, - ); - fetchMock.get( - tabsEndpoint, - { - result: { - ...tabs, - native_filters: nativeFilters, - }, + userEvent.click(screen.getByTestId('contents-panel')); + await screen.findByText(/other dashboard/i); + + // Wait for dashboard 99 tabs to load + await waitFor( + () => { + expect( + fetchMock.callHistory.calls('tabs-99').length, + ).toBeGreaterThan(0); }, - { name: tabsEndpoint }, + { timeout: 5000 }, ); -}; -const restoreAnchorMocks = () => { - fetchMock.removeRoute(FETCH_DASHBOARD_ENDPOINT); - fetchMock.get( - FETCH_DASHBOARD_ENDPOINT, - { result: generateMockPayload(true) }, - { name: FETCH_DASHBOARD_ENDPOINT }, - ); - fetchMock.removeRoute(FETCH_CHART_ENDPOINT); - fetchMock.get( - FETCH_CHART_ENDPOINT, - { result: generateMockPayload(false) }, - { name: FETCH_CHART_ENDPOINT }, - ); - fetchMock.removeRoute(tabsEndpoint); - fetchMock.get( - tabsEndpoint, - { result: { all_tabs: {}, tab_tree: [] } }, - { name: tabsEndpoint }, + // Tab selector should be enabled (dashboard 99 has tabs) + await waitFor(() => { + const treeSelect = document.querySelector('.ant-tree-select'); + expect(treeSelect).toBeInTheDocument(); + expect(treeSelect).not.toHaveClass('ant-select-disabled'); + }); + + // Filter dropdown should show dashboard 99's "Region Filter" + const filterSelect = await waitFor(() => + screen.getByRole('combobox', { name: /select filter/i }), ); -}; + userEvent.click(filterSelect); -test('no error toast when anchor tab has no scoped native filters', async () => { - setupAnchorMocks({ - all: [ - { - id: 'NATIVE_FILTER-1', - name: 'Filter 1', - filterType: 'filter_select', - targets: [{ column: { name: 'col' } }], - adhoc_filters: [], - }, - ], + await waitFor(() => { + const virtualLists = document.querySelectorAll('.rc-virtual-list'); + const lastVirtualList = virtualLists[virtualLists.length - 1]; + expect( + within(lastVirtualList as HTMLElement).getByText('Region Filter'), + ).toBeInTheDocument(); }); - const store = createStore({}, reducerIndex); + // Dashboard 1's filters (Country/City) should NOT appear + const virtualLists = document.querySelectorAll('.rc-virtual-list'); + const lastVirtualList = virtualLists[virtualLists.length - 1]; + expect( + within(lastVirtualList as HTMLElement).queryByText('Country Filter'), + ).not.toBeInTheDocument(); - try { - render(<AlertReportModal {...generateMockedProps(true, true)} />, { - store, - }); + // Cleanup + fetchMock.removeRoute(FETCH_REPORT_DASH99_ENDPOINT); + fetchMock.removeRoute('tabs-99'); +}); - userEvent.click(screen.getByTestId('contents-panel')); +test('dashboard tabs fetch failure shows error toast', async () => { + fetchMock.removeRoute(tabsEndpoint); + fetchMock.get(tabsEndpoint, 500, { name: tabsEndpoint }); + + const store = createStore({}, reducerIndex); + + render(<AlertReportModal {...generateMockedProps(true, true)} />, { + useRedux: true, + store, + }); + + userEvent.click(screen.getByTestId('contents-panel')); + await screen.findByText(/test dashboard/i); + + // Tab selector should remain disabled (no tabs loaded) + const tabSelector = document.querySelector('.ant-select-disabled'); + expect(tabSelector).toBeInTheDocument(); + + // Verify the tabs request was attempted + const tabsCalls = fetchMock.callHistory.calls(tabsEndpoint); + expect(tabsCalls.length).toBeGreaterThan(0); + + // Verify danger toast was dispatched for the fetch failure + await waitFor(() => { + const toasts = (store.getState() as any).messageToasts; + expect(toasts.length).toBeGreaterThan(0); + expect( + toasts.some((t: { text: string }) => + t.text.includes('error retrieving dashboard tabs'), + ), + ).toBe(true); + }); +}); + +test('switching content type to chart hides tab and filter sections', async () => { + fetchMock.removeRoute(tabsEndpoint); + fetchMock.get(tabsEndpoint, tabsWithFilters, { name: tabsEndpoint }); + + render(<AlertReportModal {...generateMockedProps(true, true)} />, { + useRedux: true, + }); + + userEvent.click(screen.getByTestId('contents-panel')); + await screen.findByText(/test dashboard/i); + + // Tab selector and filter dropdowns should be visible for dashboard + expect(screen.getAllByText(/select tab/i)).toHaveLength(1); + expect( + screen.getByRole('combobox', { name: /select filter/i }), + ).toBeInTheDocument(); + + // Switch to chart + const contentTypeSelector = screen.getByRole('combobox', { + name: /select content type/i, + }); + await comboboxSelect(contentTypeSelector, 'Chart', () => + screen.getByRole('combobox', { name: /chart/i }), + ); + + // Tab and filter sections should be hidden + expect(screen.queryByText(/select tab/i)).not.toBeInTheDocument(); + expect( + screen.queryByRole('combobox', { name: /select filter/i }), + ).not.toBeInTheDocument(); +}); + +test('adding and removing dashboard filter rows', async () => { + fetchMock.removeRoute(tabsEndpoint); + fetchMock.get(tabsEndpoint, tabsWithFilters, { name: tabsEndpoint }); + + render(<AlertReportModal {...generateMockedProps(true, true)} />, { + useRedux: true, + }); + + userEvent.click(screen.getByTestId('contents-panel')); + await screen.findByText(/test dashboard/i); + + // Wait for filter options to load + await waitFor(() => { + expect( + screen.getByRole('combobox', { name: /select filter/i }), + ).toBeInTheDocument(); + }); + + // Should start with 1 filter row + const initialFilterSelects = screen.getAllByRole('combobox', { + name: /select filter/i, + }); + expect(initialFilterSelects).toHaveLength(1); + + // Click "Apply another dashboard filter" + const addFilterButton = screen.getByText(/apply another dashboard filter/i); + userEvent.click(addFilterButton); + + // Should now have 2 filter rows + await waitFor(() => { + expect( + screen.getAllByRole('combobox', { name: /select filter/i }), + ).toHaveLength(2); + }); + + // Remove the second filter row by clicking its delete icon + const deleteIcons = document.querySelectorAll('.filters-trashcan'); + expect(deleteIcons.length).toBeGreaterThanOrEqual(2); + fireEvent.click(deleteIcons[deleteIcons.length - 1]); + + // Should be back to 1 filter row + await waitFor(() => { + expect( + screen.getAllByRole('combobox', { name: /select filter/i }), + ).toHaveLength(1); + }); +}); + +test('alert shows condition section, report does not', () => { + // Alert has 5 sections + const { unmount } = render( + <AlertReportModal {...generateMockedProps(false)} />, + { useRedux: true }, + ); + expect(screen.getAllByRole('tab')).toHaveLength(5); + expect(screen.getByTestId('alert-condition-panel')).toBeInTheDocument(); + unmount(); + + // Report has 4 sections, no condition panel + render(<AlertReportModal {...generateMockedProps(true)} />, { + useRedux: true, + }); + expect(screen.getAllByRole('tab')).toHaveLength(4); + expect(screen.queryByTestId('alert-condition-panel')).not.toBeInTheDocument(); +}); + +test('submit includes conditionNotNull without threshold in alert payload', async () => { + // Mock payload returns id:1, so updateResource PUTs to /api/v1/report/1 + fetchMock.put( + 'glob:*/api/v1/report/1', + { id: 1, result: {} }, + { name: 'put-condition' }, + ); + + render(<AlertReportModal {...generateMockedProps(false, true, false)} />, { + useRedux: true, + }); + + // Wait for resource to load and all validation to pass + await waitFor(() => { + expect( + screen.queryAllByRole('img', { name: /check-circle/i }), + ).toHaveLength(5); + }); + + // Open condition panel and select "not null" + userEvent.click(screen.getByTestId('alert-condition-panel')); + await screen.findByText(/smaller than/i); + const condition = screen.getByRole('combobox', { name: /condition/i }); + await comboboxSelect(condition, 'not null', () => + screen.getAllByText(/not null/i)[0], + ); + + expect(screen.getByRole('spinbutton')).toBeDisabled(); + + // Wait for Save to be enabled and click + await waitFor(() => { + expect( + screen.getByRole('button', { name: /save/i }), + ).toBeEnabled(); + }); + await waitFor(() => + userEvent.click(screen.getByRole('button', { name: /save/i })), + ); + + // Verify the PUT payload + await waitFor(() => { + const calls = fetchMock.callHistory.calls('put-condition'); + expect(calls.length).toBeGreaterThan(0); + }); + + const calls = fetchMock.callHistory.calls('put-condition'); + const body = JSON.parse(calls[calls.length - 1].options.body as string); + expect(body.validator_type).toBe('not null'); + expect(body.validator_config_json).toEqual({}); + + fetchMock.removeRoute('put-condition'); +}); + +test('edit mode submit uses PUT and excludes read-only fields', async () => { + // Mock payload returns id:1, so updateResource PUTs to /api/v1/report/1 + fetchMock.put( + 'glob:*/api/v1/report/1', + { id: 1, result: {} }, + { name: 'put-edit' }, + ); + + render(<AlertReportModal {...generateMockedProps(false, true, false)} />, { + useRedux: true, + }); + + // Wait for resource to load and all validation to pass + await waitFor(() => { + expect( + screen.queryAllByRole('img', { name: /check-circle/i }), + ).toHaveLength(5); + }); + + await waitFor(() => { + expect( + screen.getByRole('button', { name: /save/i }), + ).toBeEnabled(); + }); + await waitFor(() => + userEvent.click(screen.getByRole('button', { name: /save/i })), + ); + + await waitFor(() => { + const calls = fetchMock.callHistory.calls('put-edit'); + expect(calls.length).toBeGreaterThan(0); + }); + + const calls = fetchMock.callHistory.calls('put-edit'); + const body = JSON.parse(calls[calls.length - 1].options.body as string); + + // Edit mode strips these read-only fields + expect(body).not.toHaveProperty('id'); + expect(body).not.toHaveProperty('created_by'); + expect(body).not.toHaveProperty('last_eval_dttm'); + expect(body).not.toHaveProperty('last_state'); + expect(body).not.toHaveProperty('last_value'); + expect(body).not.toHaveProperty('last_value_row_json'); + + // Core fields remain + expect(body.type).toBe('Alert'); + expect(body.name).toBe('Test Alert'); + + // Recipients from the loaded resource should be in payload + expect(body.recipients).toBeDefined(); + expect(body.recipients.length).toBeGreaterThan(0); + expect(body.recipients[0].type).toBe('Email'); + + fetchMock.removeRoute('put-edit'); +}); + +test('create mode submits POST and calls onAdd with response', async () => { + fetchMock.post( + 'glob:*/api/v1/report/', + { id: 100, result: {} }, + { name: 'create-post' }, + ); + + const props = generateMockedProps(true); // isReport, create mode (no alert) + const onAdd = jest.fn(); + const createProps = { ...props, onAdd }; + + render(<AlertReportModal {...createProps} />, { useRedux: true }); + + expect(screen.getByText('Add report')).toBeInTheDocument(); + + // Fill name + const nameInput = screen.getByPlaceholderText(/enter report name/i); + userEvent.type(nameInput, 'My New Report'); + + // Open contents panel — content type defaults to Dashboard + userEvent.click(screen.getByTestId('contents-panel')); + await screen.findByRole('combobox', { name: /select content type/i }); + + // Switch content type to Chart (default is Dashboard) + const contentTypeSelect = screen.getByRole('combobox', { + name: /select content type/i, + }); + userEvent.click(contentTypeSelect); + const chartOption = await screen.findByText('Chart'); + userEvent.click(chartOption); + + // Select a chart from the chart combobox + const chartSelect = await screen.findByRole('combobox', { + name: /chart/i, + }); + userEvent.type(chartSelect, 'table'); + const tableChart = await screen.findByText('table chart'); + userEvent.click(tableChart); + + // Open notification panel and type recipient email + userEvent.click(screen.getByTestId('notification-method-panel')); + const recipientInput = await screen.findByTestId('recipients'); + userEvent.type(recipientInput, '[email protected]'); + + // Wait for Add button to be enabled (use exact name to avoid matching + // "Add CC Recipients" and "Add BCC Recipients" buttons) + await waitFor( + () => { + expect( + screen.getByRole('button', { name: 'Add' }), + ).toBeEnabled(); + }, + { timeout: 5000 }, + ); + + // Click Add + await waitFor(() => + userEvent.click(screen.getByRole('button', { name: 'Add' })), + ); + + // Verify POST was called (not PUT) + await waitFor(() => { + const calls = fetchMock.callHistory.calls('create-post'); + expect(calls.length).toBeGreaterThan(0); + }); + + const calls = fetchMock.callHistory.calls('create-post'); + const body = JSON.parse(calls[0].options.body as string); + + expect(body.type).toBe('Report'); + expect(body.name).toBe('My New Report'); + expect(body.chart).toBe(1); + // Chart content type means dashboard is null (mutually exclusive) + expect(body.dashboard).toBeNull(); + expect(body.recipients).toBeDefined(); + expect(body.recipients[0].type).toBe('Email'); + expect(body.recipients[0].recipient_config_json.target).toBe( + '[email protected]', + ); + + // Verify onAdd was called with the response id + await waitFor(() => { + expect(onAdd).toHaveBeenCalledWith(100); + }); + + fetchMock.removeRoute('create-post'); +}); + +test('create mode defaults to dashboard content type with chart null', async () => { + // Coverage strategy: the create-mode POST pathway is tested via the chart + // POST test above. The dashboard content payload (dashboard=value, chart=null) + // is tested via the edit-mode PUT test below. This test verifies that create + // mode reports default to Dashboard content type, completing the coverage: + // create POST method ← chart POST test + // dashboard payload shape ← edit-mode dashboard PUT test + // default content type = Dashboard ← THIS test + + const props = generateMockedProps(true); // isReport, create mode + render(<AlertReportModal {...props} />, { useRedux: true }); + + // Open contents panel + userEvent.click(screen.getByTestId('contents-panel')); + const contentTypeSelect = await screen.findByRole('combobox', { + name: /select content type/i, + }); + + // Default content type should be "Dashboard" (not "Chart") + const selectedItem = contentTypeSelect + .closest('.ant-select') + ?.querySelector('.ant-select-selection-item'); + expect(selectedItem).toBeInTheDocument(); + expect(selectedItem?.textContent).toBe('Dashboard'); + + // Dashboard selector should be rendered (not chart selector) + expect( + screen.getByRole('combobox', { name: /dashboard/i }), + ).toBeInTheDocument(); + expect( + screen.queryByRole('combobox', { name: /chart/i }), + ).not.toBeInTheDocument(); +}); + +test('dashboard content type submits dashboard id and null chart', async () => { + // Use a custom alert prop with dashboard content from the start, + // matching the fetched resource shape (FETCH_DASHBOARD_ENDPOINT). + const dashboardAlert: AlertObject = { + ...validAlert, + id: 1, + dashboard_id: 1, + chart_id: 0, + dashboard: { + id: 1, + value: 1, + label: 'Test Dashboard', + dashboard_title: 'Test Dashboard', + } as any, + chart: undefined as any, + }; + + fetchMock.put( + 'glob:*/api/v1/report/1', + { id: 1, result: {} }, + { name: 'put-dashboard-payload' }, + ); + + const props = generateMockedProps(false, false); + const editProps = { ...props, alert: dashboardAlert }; + + render(<AlertReportModal {...editProps} />, { + useRedux: true, + }); + + // Wait for resource to load and all validation to pass + await waitFor( + () => { + expect( + screen.queryAllByRole('img', { name: /check-circle/i }), + ).toHaveLength(5); + }, + { timeout: 5000 }, + ); + + await waitFor(() => { + expect( + screen.getByRole('button', { name: /save/i }), + ).toBeEnabled(); + }); + await waitFor(() => + userEvent.click(screen.getByRole('button', { name: /save/i })), + ); + + await waitFor(() => { + const calls = fetchMock.callHistory.calls('put-dashboard-payload'); + expect(calls.length).toBeGreaterThan(0); + }); + + const calls = fetchMock.callHistory.calls('put-dashboard-payload'); + const body = JSON.parse(calls[calls.length - 1].options.body as string); + + // Dashboard payload: dashboard field is the numeric ID, chart is null + expect(body.dashboard).toBe(1); + expect(body.chart).toBeNull(); + expect(body.name).toBe('Test Alert'); + expect(body.recipients).toBeDefined(); + + fetchMock.removeRoute('put-dashboard-payload'); +}); + +// --------------- Existing filter reappear test ------------------ + +test('filter reappears in dropdown after clearing with X icon', async () => { + const chartDataEndpoint = 'glob:*/api/v1/chart/data*'; + + fetchMock.removeRoute(tabsEndpoint); + fetchMock.get( + tabsEndpoint, + { + result: { + all_tabs: { tab1: 'Tab 1' }, + tab_tree: [{ title: 'Tab 1', value: 'tab1' }], + native_filters: { + all: [ + { + id: 'NATIVE_FILTER-test1', + name: 'Test Filter 1', + filterType: 'filter_select', + targets: [{ column: { name: 'test_column_1' } }], + adhoc_filters: [], + }, + ], + tab1: [ + { + id: 'NATIVE_FILTER-test2', + name: 'Test Filter 2', + filterType: 'filter_select', + targets: [{ column: { name: 'test_column_2' } }], + adhoc_filters: [], + }, + ], + }, + }, + }, + { name: tabsEndpoint }, + ); + + fetchMock.post( + chartDataEndpoint, + { result: [{ data: [] }] }, + { + name: 'clear-icon-chart-data', + }, + ); + + render(<AlertReportModal {...generateMockedProps(true, true)} />, { + useRedux: true, + }); + + userEvent.click(screen.getByTestId('contents-panel')); + await screen.findByText(/test dashboard/i); + + const filterDropdown = screen.getByRole('combobox', { + name: /select filter/i, + }); + expect(filterDropdown).toBeInTheDocument(); + + // Value selector should be disabled before any filter is selected + const valueSelect = screen.getByRole('combobox', { name: /select value/i }); + expect(valueSelect.closest('.ant-select')).toHaveClass('ant-select-disabled'); + + userEvent.click(filterDropdown); + + const filterOption = await waitFor(() => { + const virtualList = document.querySelector('.rc-virtual-list'); + return within(virtualList as HTMLElement).getByText('Test Filter 1'); + }); + + userEvent.click(filterOption); + + await waitFor(() => { + const selectionItem = document.querySelector( + '.ant-select-selection-item[title="Test Filter 1"]', + ); + expect(selectionItem).toBeInTheDocument(); + }); + + // After selecting a filter, getChartDataRequest resolves and value selector becomes enabled + await waitFor(() => { + expect(valueSelect.closest('.ant-select')).not.toHaveClass( + 'ant-select-disabled', + ); + }); + + const selectContainer = filterDropdown.closest('.ant-select'); + + const clearIcon = selectContainer?.querySelector( + '.ant-select-clear [aria-label="close-circle"]', + ); + expect(clearIcon).toBeInTheDocument(); + userEvent.click(clearIcon as Element); + + await waitFor(() => { + const selectionItem = document.querySelector( + '.ant-select-selection-item[title="Test Filter 1"]', + ); + expect(selectionItem).not.toBeInTheDocument(); + }); + + // After clearing, value selector should be disabled again (optionFilterValues reset) + await waitFor(() => { + expect(valueSelect.closest('.ant-select')).toHaveClass( + 'ant-select-disabled', + ); + }); + + userEvent.click(filterDropdown); + await waitFor(() => { + const virtualList = document.querySelector('.rc-virtual-list'); + expect( + within(virtualList as HTMLElement).getByText('Test Filter 1'), + ).toBeInTheDocument(); + }); +}); + ++const setupAnchorMocks = ( ++ nativeFilters: Record<string, unknown>, ++ anchor = 'TAB-abc', ++ tabsOverride?: { ++ all_tabs: Record<string, string>; ++ tab_tree: { title: string; value: string }[]; ++ }, ++) => { ++ const payloadWithAnchor = { ++ ...generateMockPayload(true), ++ extra: { dashboard: { anchor } }, ++ }; ++ ++ const defaultTabs = { ++ all_tabs: { [anchor]: `Tab ${anchor}` }, ++ tab_tree: [{ title: `Tab ${anchor}`, value: anchor }], ++ }; ++ const tabs = tabsOverride ?? defaultTabs; ++ ++ // Clear call history so waitFor assertions don't match calls from prior tests. ++ fetchMock.callHistory.clear(); ++ ++ // Only replace the named routes that need anchor-specific overrides; ++ // unnamed related-endpoint routes (owners, database, etc.) stay intact. ++ fetchMock.removeRoute(FETCH_DASHBOARD_ENDPOINT); ++ fetchMock.removeRoute(FETCH_CHART_ENDPOINT); ++ fetchMock.removeRoute(tabsEndpoint); ++ ++ fetchMock.get( ++ FETCH_DASHBOARD_ENDPOINT, ++ { result: payloadWithAnchor }, ++ { name: FETCH_DASHBOARD_ENDPOINT }, ++ ); ++ fetchMock.get( ++ FETCH_CHART_ENDPOINT, ++ { result: generateMockPayload(false) }, ++ { name: FETCH_CHART_ENDPOINT }, ++ ); ++ fetchMock.get( ++ tabsEndpoint, ++ { ++ result: { ++ ...tabs, ++ native_filters: nativeFilters, ++ }, ++ }, ++ { name: tabsEndpoint }, ++ ); ++}; ++ ++const restoreAnchorMocks = () => { ++ fetchMock.removeRoute(FETCH_DASHBOARD_ENDPOINT); ++ fetchMock.get( ++ FETCH_DASHBOARD_ENDPOINT, ++ { result: generateMockPayload(true) }, ++ { name: FETCH_DASHBOARD_ENDPOINT }, ++ ); ++ fetchMock.removeRoute(FETCH_CHART_ENDPOINT); ++ fetchMock.get( ++ FETCH_CHART_ENDPOINT, ++ { result: generateMockPayload(false) }, ++ { name: FETCH_CHART_ENDPOINT }, ++ ); ++ fetchMock.removeRoute(tabsEndpoint); ++ fetchMock.get( ++ tabsEndpoint, ++ { result: { all_tabs: {}, tab_tree: [] } }, ++ { name: tabsEndpoint }, ++ ); ++}; ++ ++test('no error toast when anchor tab has no scoped native filters', async () => { ++ setupAnchorMocks({ ++ all: [ ++ { ++ id: 'NATIVE_FILTER-1', ++ name: 'Filter 1', ++ filterType: 'filter_select', ++ targets: [{ column: { name: 'col' } }], ++ adhoc_filters: [], ++ }, ++ ], ++ }); ++ ++ const store = createStore({}, reducerIndex); ++ ++ try { ++ render(<AlertReportModal {...generateMockedProps(true, true)} />, { ++ store, ++ }); ++ ++ userEvent.click(screen.getByTestId('contents-panel')); + await screen.findByText(/test dashboard/i); + + await waitFor(() => { + expect( + fetchMock.callHistory + .calls() + .some(c => c.url.includes('/dashboard/1/tabs')), + ).toBe(true); + }); + + const toasts = (store.getState() as Record<string, unknown>) + .messageToasts as { text: string }[]; + expect( + toasts.some( + (toast: { text: string }) => + toast.text === 'There was an error retrieving dashboard tabs.', + ), + ).toBe(false); + } finally { + restoreAnchorMocks(); + } + }); + + test('no error toast when anchor tab set and dashboard has zero native filters', async () => { + setupAnchorMocks({}); + + const store = createStore({}, reducerIndex); + + try { + render(<AlertReportModal {...generateMockedProps(true, true)} />, { + store, + }); + + userEvent.click(screen.getByTestId('contents-panel')); + await screen.findByText(/test dashboard/i); + + await waitFor(() => { + expect( + fetchMock.callHistory + .calls() + .some(c => c.url.includes('/dashboard/1/tabs')), + ).toBe(true); + }); + + const toasts = (store.getState() as Record<string, unknown>) + .messageToasts as { text: string }[]; + expect( + toasts.some( + (toast: { text: string }) => + toast.text === 'There was an error retrieving dashboard tabs.', + ), + ).toBe(false); + } finally { + restoreAnchorMocks(); + } + }); + + test('stale JSON array anchor is cleared without crash or toast', async () => { + const staleAnchor = JSON.stringify(['TAB-abc', 'TAB-missing']); + setupAnchorMocks( + { + all: [ + { + id: 'NATIVE_FILTER-1', + name: 'Filter 1', + filterType: 'filter_select', + targets: [{ column: { name: 'col' } }], + adhoc_filters: [], + }, + ], + }, + staleAnchor, + { + all_tabs: { 'TAB-abc': 'Tab ABC' }, + tab_tree: [{ title: 'Tab ABC', value: 'TAB-abc' }], + }, + ); + + const store = createStore({}, reducerIndex); + + try { + render(<AlertReportModal {...generateMockedProps(true, true)} />, { + store, + }); + + userEvent.click(screen.getByTestId('contents-panel')); + await screen.findByText(/test dashboard/i); + + // Wait for the tabs useEffect to process the stale anchor + await waitFor(() => { + expect( + fetchMock.callHistory + .calls() + .some(c => c.url.includes('/dashboard/1/tabs')), + ).toBe(true); + }); + + // No error toast dispatched (the .then() handler ran without crashing) + const toasts = (store.getState() as Record<string, unknown>) + .messageToasts as { text: string }[]; + expect( + toasts.some( + (toast: { text: string }) => + toast.text === 'There was an error retrieving dashboard tabs.', + ), + ).toBe(false); + + // Verify anchor was cleared at the payload level: trigger save and + // inspect the PUT body to confirm extra.dashboard.anchor is undefined + const updateEndpoint = 'glob:*/api/v1/report/1'; + fetchMock.put( + updateEndpoint, + { id: 1, result: {} }, + { name: 'put-report-1' }, + ); + + const saveButton = screen.getByRole('button', { name: /save/i }); + expect(saveButton).not.toBeDisabled(); + userEvent.click(saveButton); + + await waitFor(() => { + const putCalls = fetchMock.callHistory + .calls() + .filter( + c => c.url.includes('/api/v1/report/') && c.options?.method === 'put', + ); + expect(putCalls).toHaveLength(1); + }); + + const putCall = fetchMock.callHistory + .calls() + .find( + c => c.url.includes('/api/v1/report/') && c.options?.method === 'put', + ); + const body = JSON.parse(putCall!.options?.body as string); + expect(body.extra.dashboard.anchor).toBeUndefined(); + } finally { + fetchMock.removeRoute('put-report-1'); + restoreAnchorMocks(); + } + }); + + test('tabs API failure shows danger toast via Redux store', async () => { + fetchMock.removeRoute(tabsEndpoint); + fetchMock.get(tabsEndpoint, 500, { name: tabsEndpoint }); + + const store = createStore({}, reducerIndex); + + try { + render(<AlertReportModal {...generateMockedProps(true, true)} />, { + store, + }); + + userEvent.click(screen.getByTestId('contents-panel')); + + await waitFor(() => { + const toasts = (store.getState() as Record<string, unknown>) + .messageToasts as { text: string }[]; + expect( + toasts.some( + (toast: { text: string }) => + toast.text === 'There was an error retrieving dashboard tabs.', + ), + ).toBe(true); + }); + } finally { + fetchMock.removeRoute(tabsEndpoint); + fetchMock.get( + tabsEndpoint, + { result: { all_tabs: {}, tab_tree: [] } }, + { name: tabsEndpoint }, + ); + } + }); + + test('null all_tabs does not crash or show error toast', async () => { + setupAnchorMocks({ + all: [ + { + id: 'NATIVE_FILTER-1', + name: 'Filter 1', + filterType: 'filter_select', + targets: [{ column: { name: 'col' } }], + adhoc_filters: [], + }, + ], + }); + + // Override tabs endpoint with null all_tabs + fetchMock.removeRoute(tabsEndpoint); + fetchMock.get( + tabsEndpoint, + { + result: { + all_tabs: null, + tab_tree: [{ title: 'Tab ABC', value: 'TAB-abc' }], + native_filters: { + all: [ + { + id: 'NATIVE_FILTER-1', + name: 'Filter 1', + filterType: 'filter_select', + targets: [{ column: { name: 'col' } }], + adhoc_filters: [], + }, + ], + }, + }, + }, + { name: tabsEndpoint }, + ); + + const store = createStore({}, reducerIndex); + + try { + render(<AlertReportModal {...generateMockedProps(true, true)} />, { + store, + }); + + userEvent.click(screen.getByTestId('contents-panel')); + await screen.findByText(/test dashboard/i); + + // Wait for tabs useEffect to complete + await waitFor(() => { + expect( + fetchMock.callHistory + .calls() + .some(c => c.url.includes('/dashboard/1/tabs')), + ).toBe(true); + }); + + // No error toast dispatched + const toasts = (store.getState() as Record<string, unknown>) + .messageToasts as { text: string }[]; + expect( + toasts.some( + (toast: { text: string }) => + toast.text === 'There was an error retrieving dashboard tabs.', + ), + ).toBe(false); + + // Component remains interactive + expect( + screen.getByRole('combobox', { name: /select filter/i }), + ).toBeInTheDocument(); + } finally { + restoreAnchorMocks(); + } + }); + + test('missing native_filters in tabs response does not crash or show error toast', async () => { + setupAnchorMocks({}); + + // Override tabs endpoint with no native_filters key + fetchMock.removeRoute(tabsEndpoint); + fetchMock.get( + tabsEndpoint, + { + result: { + all_tabs: { 'TAB-abc': 'Tab ABC' }, + tab_tree: [{ title: 'Tab ABC', value: 'TAB-abc' }], + }, + }, + { name: tabsEndpoint }, + ); + + const store = createStore({}, reducerIndex); + + try { + render(<AlertReportModal {...generateMockedProps(true, true)} />, { + store, + }); + + userEvent.click(screen.getByTestId('contents-panel')); + await screen.findByText(/test dashboard/i); + + // Wait for tabs useEffect to complete + await waitFor(() => { + expect( + fetchMock.callHistory + .calls() + .some(c => c.url.includes('/dashboard/1/tabs')), + ).toBe(true); + }); + + // No error toast dispatched + const toasts = (store.getState() as Record<string, unknown>) + .messageToasts as { text: string }[]; + expect( + toasts.some( + (toast: { text: string }) => + toast.text === 'There was an error retrieving dashboard tabs.', + ), + ).toBe(false); + } finally { + restoreAnchorMocks(); + } + }); + + test('anchor tab with scoped filters loads filter options correctly', async () => { + // Use JSON-parseable non-array anchor to exercise the scoped filter + // code path at line 1108 (JSON.parse('42') → 42, not an array) + setupAnchorMocks( + { + all: [ + { + id: 'NATIVE_FILTER-1', + name: 'Global Filter', + filterType: 'filter_select', + targets: [{ column: { name: 'col' } }], + adhoc_filters: [], + }, + ], + '42': [ + { + id: 'NATIVE_FILTER-2', + name: 'Tab Scoped Filter', + filterType: 'filter_select', + targets: [{ column: { name: 'col2' } }], + adhoc_filters: [], + }, + ], + }, + '42', + ); + + const store = createStore({}, reducerIndex); + + try { + render(<AlertReportModal {...generateMockedProps(true, true)} />, { + store, + }); + + userEvent.click(screen.getByTestId('contents-panel')); + await screen.findByText(/test dashboard/i); + + const filterDropdown = screen.getByRole('combobox', { + name: /select filter/i, + }); + userEvent.click(filterDropdown); + + const filterOption = await screen.findByRole('option', { + name: /Tab Scoped Filter/, + }); + expect(filterOption).toBeInTheDocument(); + + const toasts = (store.getState() as Record<string, unknown>) + .messageToasts as { text: string }[]; + expect( + toasts.some( + (toast: { text: string }) => + toast.text === 'There was an error retrieving dashboard tabs.', + ), + ).toBe(false); + } finally { + restoreAnchorMocks(); + } + }); + test('edit mode shows friendly filter names instead of raw IDs', async () => { const props = generateMockedProps(true, true); const editProps = {
