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] = []

Reply via email to