This is an automated email from the ASF dual-hosted git repository. beto pushed a commit to branch dnd in repository https://gitbox.apache.org/repos/asf/superset.git
commit 4342ff4df6d4fe8631f32dedaa75d78b3b639363 Author: Beto Dealmeida <[email protected]> AuthorDate: Fri Jan 16 13:49:11 2026 -0500 feat: DND --- .../superset-ui-core/src/chart/types/Base.ts | 8 +++++++ .../src/components/Chart/ChartRenderer.jsx | 10 +++++++-- .../explore/components/ExploreChartPanel/index.tsx | 22 +++++++++++++++++-- .../components/ExploreViewContainer/index.jsx | 14 +++++++++++- superset/charts/schemas.py | 9 ++++++++ superset/models/helpers.py | 25 +++++++++++++++++++++- 6 files changed, 82 insertions(+), 6 deletions(-) diff --git a/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts b/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts index 15459d1114..4a99bfa34a 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts +++ b/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts @@ -33,6 +33,14 @@ export enum Behavior { */ DrillToDetail = 'DRILL_TO_DETAIL', DrillBy = 'DRILL_BY', + + /** + * Include `ALLOWS_EMPTY_RESULTS` behavior if the chart handles empty/no data + * gracefully (e.g., showing a drop zone for drag-and-drop configuration). + * Charts with this behavior will receive empty data instead of seeing + * the "No results" message. + */ + AllowsEmptyResults = 'ALLOWS_EMPTY_RESULTS', } export interface ContextMenuFilters { diff --git a/superset-frontend/src/components/Chart/ChartRenderer.jsx b/superset-frontend/src/components/Chart/ChartRenderer.jsx index decee4d3b4..c6a9d1c5bf 100644 --- a/superset-frontend/src/components/Chart/ChartRenderer.jsx +++ b/superset-frontend/src/components/Chart/ChartRenderer.jsx @@ -365,9 +365,15 @@ class ChartRenderer extends Component { ownState?.agGridFilterModel && Object.keys(ownState.agGridFilterModel).length > 0; + // Check if chart allows empty results (e.g., for drag-and-drop configuration) + const chartMetadata = getChartMetadataRegistry().get(vizType); + const allowsEmptyResults = chartMetadata?.behaviors?.includes( + Behavior.AllowsEmptyResults, + ); + const bypassNoResult = !( - formData?.server_pagination && - (hasSearchText || hasAgGridFilters) + (formData?.server_pagination && (hasSearchText || hasAgGridFilters)) || + allowsEmptyResults ); return ( diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx index c55fe306bf..9e9c0b843f 100644 --- a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx +++ b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx @@ -20,6 +20,7 @@ import { useState, useEffect, useCallback, useMemo, ReactNode } from 'react'; import Split from 'react-split'; import { t } from '@apache-superset/core'; import { + Behavior, DatasourceType, ensureIsArray, isFeatureEnabled, @@ -168,16 +169,33 @@ const ExploreChartPanel = ({ const [showDatasetModal, setShowDatasetModal] = useState(false); const metaDataRegistry = getChartMetadataRegistry(); - const { useLegacyApi } = metaDataRegistry.get(vizType) ?? {}; + const chartMetadata = metaDataRegistry.get(vizType); + const { useLegacyApi } = chartMetadata ?? {}; const vizTypeNeedsDataset = useLegacyApi && datasource.type !== DatasourceType.Table; + + // Check if chart allows empty results (for drag-and-drop configuration) + const allowsEmptyResults = chartMetadata?.behaviors?.includes( + Behavior.AllowsEmptyResults, + ); + // Check if query returned no actual data rows + const hasNoDataRows = + ensureIsArray(chart.queriesResponse).length > 0 && + chart.queriesResponse?.every( + response => !response?.data || response.data.length === 0, + ); + // Suppress stale warning for AllowsEmptyResults charts with no data + // (they're in initial unconfigured state) + const isUnconfiguredEmptyChart = allowsEmptyResults && hasNoDataRows; + // added boolean column to below show boolean so that the errors aren't overlapping const showAlertBanner = !chartAlert && chartIsStale && !vizTypeNeedsDataset && chart.chartStatus !== 'failed' && - ensureIsArray(chart.queriesResponse).length > 0; + ensureIsArray(chart.queriesResponse).length > 0 && + !isUnconfiguredEmptyChart; const updateQueryContext = useCallback( async function fetchChartData() { diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx index f8556a19ce..7d6a5e9a1a 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx @@ -456,15 +456,27 @@ function ExploreViewContainer(props) { } }, [isDynamicPluginLoading]); + // Track if we've already triggered initial query + const [hasTriggeredInitialQuery, setHasTriggeredInitialQuery] = + useState(false); + + // Auto-trigger query when there are no validation errors + // This effect runs on mount and when controls change (e.g., after dynamic plugin loads) useEffect(() => { + // Skip if already triggered or still loading dynamic plugin + if (hasTriggeredInitialQuery || isDynamicPluginLoading) { + return; + } + const hasError = Object.values(props.controls).some( control => control.validationErrors && control.validationErrors.length > 0, ); if (!hasError) { props.actions.triggerQuery(true, props.chart.id); + setHasTriggeredInitialQuery(true); } - }, []); + }, [props.controls, isDynamicPluginLoading, hasTriggeredInitialQuery]); const reRenderChart = useCallback( controlsChanged => { diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py index 2ed6446cee..fa010e6a4b 100644 --- a/superset/charts/schemas.py +++ b/superset/charts/schemas.py @@ -1029,6 +1029,15 @@ class ChartDataExtrasSchema(Schema): }, allow_none=True, ) + allow_empty_query = fields.Boolean( + metadata={ + "description": ( + "Allow queries with no metrics, columns, or groupby. " + "Used by charts that support drag-and-drop configuration." + ) + }, + load_default=False, + ) class AnnotationLayerSchema(Schema): diff --git a/superset/models/helpers.py b/superset/models/helpers.py index 754956cbc2..6081168b9a 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -1149,6 +1149,26 @@ class ExploreMixin: # pylint: disable=too-many-public-methods datasource types (Query, SqlaTable, etc.). """ qry_start_dttm = datetime.now() + + # Check if this is an empty query (for drag-and-drop configured charts) + extras = query_obj.get("extras", {}) or {} + metrics = query_obj.get("metrics") or [] + columns = query_obj.get("columns") or [] + groupby = query_obj.get("groupby") or [] + if extras.get("allow_empty_query") and not metrics and not columns and not groupby: + # Return empty result without executing any SQL + return QueryResult( + applied_template_filters=[], + applied_filter_columns=[], + rejected_filter_columns=[], + status=QueryStatus.SUCCESS, + df=pd.DataFrame(), + duration=datetime.now() - qry_start_dttm, + query="", + errors=None, + error_message=None, + ) + query_str_ext = self.get_query_str_extended(query_obj) sql = query_str_ext.sql status = QueryStatus.SUCCESS @@ -2727,7 +2747,10 @@ class ExploreMixin: # pylint: disable=too-many-public-methods "and is required by this type of chart" ) ) - if not metrics and not columns and not groupby: + # Allow charts to opt-in to empty queries (for drag-and-drop configuration) + # Note: The actual empty query handling is done in the query() method + allow_empty = extras.get("allow_empty_query", False) + if not metrics and not columns and not groupby and not allow_empty: raise QueryObjectValidationError(_("Empty query?")) metrics_exprs: list[ColumnElement] = []
