You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by be...@apache.org on 2026/01/16 20:23:19 UTC
(superset) 01/01: feat: DND
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 <ro...@dealmeida.net>
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] = []