Compare commits

...

4 Commits

Author SHA1 Message Date
Beto Dealmeida
4342ff4df6 feat: DND 2026-01-16 13:49:11 -05:00
Beto Dealmeida
9042601f5c Line chart 2026-01-15 22:34:38 -05:00
Beto Dealmeida
1bebbcf067 Another chart 2026-01-15 19:23:54 -05:00
Beto Dealmeida
58ac91c059 feat: test Glyph chart 2026-01-15 18:52:49 -05:00
14 changed files with 474 additions and 2898 deletions

View File

@@ -112,6 +112,8 @@ services:
superset-init-light:
condition: service_completed_successfully
volumes: *superset-volumes
ports:
- "${SUPERSET_PORT:-8088}:8088"
environment:
DATABASE_HOST: db-light
DATABASE_DB: superset_light
@@ -157,7 +159,7 @@ services:
environment:
# set this to false if you have perf issues running the npm i; npm run dev in-docker
# if you do so, you have to run this manually on the host, which should perform better!
BUILD_SUPERSET_FRONTEND_IN_DOCKER: true
BUILD_SUPERSET_FRONTEND_IN_DOCKER: false
NPM_RUN_PRUNE: false
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
# configuring the dev-server to use the host.docker.internal to connect to the backend

File diff suppressed because it is too large Load Diff

View File

@@ -102,6 +102,9 @@
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource/fira-code": "^5.2.7",
"@fontsource/inter": "^5.2.8",
"@react-spring/web": "^10.0.3",
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/core": "^5.24.13",
"@rjsf/utils": "^5.24.3",
@@ -144,6 +147,7 @@
"chrono-node": "^2.7.8",
"classnames": "^2.2.5",
"content-disposition": "^1.0.1",
"currencyformatter.js": "^2.2.0",
"d3-color": "^3.1.0",
"d3-scale": "^2.1.2",
"dayjs": "^1.11.19",
@@ -160,6 +164,7 @@
"geostyler-openlayers-parser": "^4.3.0",
"geostyler-style": "7.5.0",
"geostyler-wfs-parser": "^2.0.3",
"global-box": "^2.0.2",
"googleapis": "^169.0.0",
"immer": "^11.0.1",
"interweave": "^13.1.1",
@@ -178,6 +183,7 @@
"nanoid": "^5.1.6",
"ol": "^7.5.2",
"prop-types": "^15.8.1",
"query-string": "^6.14.1",
"re-resizable": "^6.11.2",
"react": "^17.0.2",
"react-checkbox-tree": "^1.8.0",

View File

@@ -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 {

View File

@@ -31,13 +31,14 @@
"@deck.gl/layers": "~9.2.5",
"@deck.gl/mesh-layers": "~9.2.2",
"@deck.gl/react": "~9.2.5",
"@deck.gl/widgets": "~9.2.2",
"@luma.gl/constants": "~9.2.4",
"@luma.gl/core": "~9.2.2",
"@luma.gl/engine": "~9.2.4",
"@luma.gl/shadertools": "~9.2.2",
"@luma.gl/webgl": "~9.2.2",
"@mapbox/tiny-sdf": "^2.0.7",
"@mapbox/geojson-extent": "^1.0.1",
"@mapbox/tiny-sdf": "^2.0.7",
"@math.gl/web-mercator": "^4.1.0",
"@types/d3-array": "^3.2.2",
"@types/geojson": "^7946.0.16",

View File

@@ -0,0 +1,14 @@
{
"name": "@superset-ui/plugin-chart-glyph",
"version": "0.0.1",
"description": "Glyph semantic charts for Superset",
"main": "src/index.ts",
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^17 || ^18 || ^19"
},
"dependencies": {
"glyph": "file:../../../../glyph"
}
}

View File

@@ -0,0 +1,21 @@
import { ChartPlugin, ChartMetadata, buildQueryContext } from '@superset-ui/core';
import { BigNumber, FancyBigNumber, LineChart, makeChartPlugin } from 'glyph';
import thumbnail from './thumbnail.png';
export const GlyphBigNumberChartPlugin = makeChartPlugin(
BigNumber,
{ ChartPlugin, ChartMetadata, buildQueryContext },
{ thumbnail },
);
export const GlyphFancyBigNumberChartPlugin = makeChartPlugin(
FancyBigNumber,
{ ChartPlugin, ChartMetadata, buildQueryContext },
{ thumbnail },
);
export const GlyphLineChartPlugin = makeChartPlugin(
LineChart,
{ ChartPlugin, ChartMetadata, buildQueryContext },
{ thumbnail },
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -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 (

View File

@@ -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() {

View File

@@ -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 => {

View File

@@ -88,6 +88,11 @@ import { HandlebarsChartPlugin } from '@superset-ui/plugin-chart-handlebars';
import { ChartCustomizationPlugins, FilterPlugins } from 'src/constants';
import AgGridTableChartPlugin from '@superset-ui/plugin-chart-ag-grid-table';
import TimeTableChartPlugin from '../TimeTable';
import {
GlyphBigNumberChartPlugin,
GlyphFancyBigNumberChartPlugin,
GlyphLineChartPlugin,
} from '@superset-ui/plugin-chart-glyph';
export default class MainPreset extends Preset {
constructor() {
@@ -211,6 +216,12 @@ export default class MainPreset extends Preset {
}).configure({ key: VizType.Cartodiagram }),
...experimentalPlugins,
...agGridTablePlugin,
// Glyph semantic charts
new GlyphBigNumberChartPlugin().configure({ key: 'glyph_big_number' }),
new GlyphFancyBigNumberChartPlugin().configure({
key: 'glyph_fancy_big_number',
}),
new GlyphLineChartPlugin().configure({ key: 'glyph_line_chart' }),
],
});
}

View File

@@ -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):

View File

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