mirror of
https://github.com/apache/superset.git
synced 2026-07-26 16:42:32 +00:00
Compare commits
8 Commits
research-s
...
fix/backpo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ee8fd1d3b | ||
|
|
b7765ca291 | ||
|
|
d96cacfb63 | ||
|
|
0877e66610 | ||
|
|
7c00f5567f | ||
|
|
76c5b40c13 | ||
|
|
9617b8b392 | ||
|
|
a7b180d8dd |
@@ -368,9 +368,13 @@ const CustomModal = ({
|
||||
};
|
||||
CustomModal.displayName = 'Modal';
|
||||
|
||||
// Theme-aware confirmation modal - now inherits theme through App wrapper in SupersetThemeProvider
|
||||
const themedConfirm = (props: Parameters<typeof AntdModal.confirm>[0]) =>
|
||||
AntdModal.confirm(props);
|
||||
|
||||
export const Modal = Object.assign(CustomModal, {
|
||||
error: AntdModal.error,
|
||||
warning: AntdModal.warning,
|
||||
confirm: AntdModal.confirm,
|
||||
confirm: themedConfirm,
|
||||
useModal: AntdModal.useModal,
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
/* eslint-disable react-prefer-function-component/react-prefer-function-component */
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
import React from 'react';
|
||||
import { theme as antdThemeImport, ConfigProvider } from 'antd';
|
||||
import { theme as antdThemeImport, ConfigProvider, App } from 'antd';
|
||||
|
||||
// @fontsource/* v5.1+ doesn't play nice with eslint-import plugin v2.31+
|
||||
/* eslint-disable import/extensions */
|
||||
@@ -243,7 +243,7 @@ export class Theme {
|
||||
<ThemeProvider theme={themeState.theme}>
|
||||
<GlobalStyles />
|
||||
<ConfigProvider theme={themeState.antdConfig}>
|
||||
{children}
|
||||
<App>{children}</App>
|
||||
</ConfigProvider>
|
||||
</ThemeProvider>
|
||||
</EmotionCacheProvider>
|
||||
|
||||
@@ -32,7 +32,7 @@ import crossFiltersSelector from './CrossFilters/selectors';
|
||||
|
||||
const HorizontalBar = styled.div`
|
||||
${({ theme }) => `
|
||||
padding: ${theme.sizeUnit * 3}px ${theme.sizeUnit * 2}px ${
|
||||
padding: 0 ${theme.sizeUnit * 2}px ${
|
||||
theme.sizeUnit * 3
|
||||
}px ${theme.sizeUnit * 4}px;
|
||||
background: ${theme.colorBgBase};
|
||||
|
||||
@@ -317,13 +317,20 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||
|
||||
const sortComparator = useCallback(
|
||||
(a: LabeledValue, b: LabeledValue) => {
|
||||
// When sortMetric is specified, the backend already sorted the data correctly
|
||||
// Don't override the backend's metric-based sorting with frontend alphabetical sorting
|
||||
if (formData.sortMetric) {
|
||||
return 0; // Preserve the original order from the backend
|
||||
}
|
||||
|
||||
// Only apply alphabetical sorting when no sortMetric is specified
|
||||
const labelComparator = propertyComparator('label');
|
||||
if (formData.sortAscending) {
|
||||
return labelComparator(a, b);
|
||||
}
|
||||
return labelComparator(b, a);
|
||||
},
|
||||
[formData.sortAscending],
|
||||
[formData.sortAscending, formData.sortMetric],
|
||||
);
|
||||
|
||||
// Use effect for initialisation for filter plugin
|
||||
|
||||
@@ -1380,6 +1380,22 @@ class ChartDataQueryObjectSchema(Schema):
|
||||
allow_none=True,
|
||||
)
|
||||
|
||||
@post_load
|
||||
def rename_deprecated_fields(
|
||||
self, data: dict[str, Any], **kwargs: Any
|
||||
) -> dict[str, Any]:
|
||||
_renames = (
|
||||
("groupby", "columns"),
|
||||
("granularity_sqla", "granularity"),
|
||||
("timeseries_limit", "series_limit"),
|
||||
("timeseries_limit_metric", "series_limit_metric"),
|
||||
)
|
||||
for old, new in _renames:
|
||||
value = data.pop(old, None)
|
||||
if value or value == 0:
|
||||
data[new] = value
|
||||
return data
|
||||
|
||||
|
||||
class ChartDataQueryContextSchema(Schema):
|
||||
query_context_factory: QueryContextFactory | None = None
|
||||
|
||||
@@ -19,7 +19,7 @@ from __future__ import annotations
|
||||
from typing import Any, cast, TYPE_CHECKING
|
||||
|
||||
from superset.common.chart_data import ChartDataResultType
|
||||
from superset.common.query_object import QueryObject
|
||||
from superset.common.query_object import DEPRECATED_FIELDS, QueryObject
|
||||
from superset.common.utils.time_range_utils import get_since_until_from_time_range
|
||||
from superset.constants import NO_TIME_RANGE
|
||||
from superset.superset_typing import Column
|
||||
@@ -66,6 +66,14 @@ class QueryObjectFactory: # pylint: disable=too-few-public-methods
|
||||
processed_extras = self._process_extras(extras)
|
||||
result_type = kwargs.setdefault("result_type", parent_result_type)
|
||||
|
||||
# Rename deprecated kwargs before any processing so that downstream code
|
||||
# (time-range resolution, QueryObject) only ever sees the canonical names.
|
||||
# A truthy deprecated value always overrides the canonical field, matching
|
||||
# the historical QueryObject._rename_deprecated_fields precedence.
|
||||
for field in DEPRECATED_FIELDS:
|
||||
if old_val := kwargs.pop(field.old_name, None):
|
||||
kwargs[field.new_name] = old_val
|
||||
|
||||
# Process row limit taking server pagination into account
|
||||
row_limit = self._process_row_limit(
|
||||
row_limit, result_type, server_pagination=server_pagination
|
||||
|
||||
@@ -24,7 +24,6 @@ from flask_wtf.csrf import same_origin
|
||||
from superset import event_logger, is_feature_enabled
|
||||
from superset.daos.dashboard import EmbeddedDashboardDAO
|
||||
from superset.superset_typing import FlaskResponse
|
||||
from superset.utils import json
|
||||
from superset.views.base import BaseSupersetView, common_bootstrap_payload
|
||||
|
||||
|
||||
@@ -76,7 +75,7 @@ class EmbeddedView(BaseSupersetView):
|
||||
dashboard_version="v2",
|
||||
)
|
||||
|
||||
bootstrap_data = {
|
||||
extra_bootstrap_data = {
|
||||
"config": {
|
||||
"GUEST_TOKEN_HEADER_NAME": current_app.config["GUEST_TOKEN_HEADER_NAME"]
|
||||
},
|
||||
@@ -86,10 +85,7 @@ class EmbeddedView(BaseSupersetView):
|
||||
},
|
||||
}
|
||||
|
||||
return self.render_template(
|
||||
"superset/spa.html",
|
||||
return self.render_app_template(
|
||||
extra_bootstrap_data=extra_bootstrap_data,
|
||||
entry="embedded",
|
||||
bootstrap_data=json.dumps(
|
||||
bootstrap_data, default=json.pessimistic_json_iso_dttm_ser
|
||||
),
|
||||
)
|
||||
|
||||
@@ -69,11 +69,11 @@
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body {% if standalone_mode %}class="standalone"{% endif %}>
|
||||
{% set tokens = theme_tokens | default({}) %}
|
||||
<body {% if standalone_mode %}class="standalone"{% endif %} style="margin: 0; padding: 0; background-color: {{ tokens.get('colorBgBase', '#ffffff') }};">
|
||||
|
||||
{% block body %}
|
||||
<div id="app" data-bootstrap="{{ bootstrap_data }}">
|
||||
{% set tokens = theme_tokens | default({}) %}
|
||||
{% set spinner_style = "width: 70px; height: auto; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);" %}
|
||||
|
||||
{% if spinner_svg %}
|
||||
@@ -85,14 +85,10 @@
|
||||
<!-- Custom URL from theme -->
|
||||
<img
|
||||
src="{{ tokens.brandSpinnerUrl }}"
|
||||
alt="Loading..."
|
||||
alt=""
|
||||
style="{{ spinner_style }}"
|
||||
/>
|
||||
{% else %}
|
||||
<!-- Fallback: This should rarely happen with new logic -->
|
||||
<div style="{{ spinner_style }}">
|
||||
Loading...
|
||||
</div>
|
||||
{# Remove fallback text - let React app handle loading state #}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -135,6 +135,53 @@ def test_chart_data_query_object_schema_time_grain_sqla_validation(
|
||||
assert result["extras"]["time_grain_sqla"] is None
|
||||
|
||||
|
||||
def test_chart_data_query_object_schema_deprecated_fields_renamed(
|
||||
app_context: None,
|
||||
) -> None:
|
||||
"""Deprecated query object fields are renamed to their canonical names."""
|
||||
schema = ChartDataQueryObjectSchema()
|
||||
|
||||
# groupby alone → becomes columns
|
||||
result = schema.load({"groupby": ["country_name"]})
|
||||
assert result.get("columns") == ["country_name"]
|
||||
assert "groupby" not in result
|
||||
|
||||
# groupby overwrites columns when both are provided
|
||||
result = schema.load({"groupby": ["region"], "columns": ["country_name"]})
|
||||
assert result.get("columns") == ["region"]
|
||||
assert "groupby" not in result
|
||||
|
||||
# empty groupby is discarded; existing columns is preserved
|
||||
result = schema.load({"groupby": [], "columns": ["country_name"]})
|
||||
assert result.get("columns") == ["country_name"]
|
||||
assert "groupby" not in result
|
||||
|
||||
# null groupby is discarded; existing columns is preserved (allow_none=True)
|
||||
result = schema.load({"groupby": None, "columns": ["country_name"]})
|
||||
assert result.get("columns") == ["country_name"]
|
||||
assert "groupby" not in result
|
||||
|
||||
# no groupby → columns passes through unchanged
|
||||
result = schema.load({"columns": ["country_name"]})
|
||||
assert result.get("columns") == ["country_name"]
|
||||
assert "groupby" not in result
|
||||
|
||||
# granularity_sqla → granularity
|
||||
result = schema.load({"granularity_sqla": "ds"})
|
||||
assert result.get("granularity") == "ds"
|
||||
assert "granularity_sqla" not in result
|
||||
|
||||
# timeseries_limit → series_limit
|
||||
result = schema.load({"timeseries_limit": 5})
|
||||
assert result.get("series_limit") == 5
|
||||
assert "timeseries_limit" not in result
|
||||
|
||||
# timeseries_limit_metric → series_limit_metric
|
||||
result = schema.load({"timeseries_limit_metric": "count"})
|
||||
assert result.get("series_limit_metric") == "count"
|
||||
assert "timeseries_limit_metric" not in result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"app",
|
||||
[{"TIME_GRAIN_ADDONS": {"PT10M": "10 minutes"}}],
|
||||
|
||||
@@ -138,3 +138,91 @@ class TestQueryObjectFactory:
|
||||
**raw_query_object,
|
||||
)
|
||||
assert query_object.metric_names == ["SUM", "num_girls", "num_boys"]
|
||||
|
||||
def test_deprecated_groupby_renamed_to_columns(
|
||||
self,
|
||||
query_object_factory: QueryObjectFactory,
|
||||
raw_query_context: dict[str, Any],
|
||||
):
|
||||
"""groupby in stored query_context should be silently renamed to columns."""
|
||||
raw_query_object = raw_query_context["queries"][0]
|
||||
raw_query_object.pop("columns", None)
|
||||
raw_query_object["groupby"] = ["name", "gender"]
|
||||
query_object = query_object_factory.create(
|
||||
raw_query_context["result_type"], **raw_query_object
|
||||
)
|
||||
assert query_object.columns == ["name", "gender"]
|
||||
assert not hasattr(query_object, "groupby")
|
||||
|
||||
def test_deprecated_groupby_overwrites_columns(
|
||||
self,
|
||||
query_object_factory: QueryObjectFactory,
|
||||
raw_query_context: dict[str, Any],
|
||||
):
|
||||
"""A truthy deprecated groupby overrides an already-present columns,
|
||||
matching the historical QueryObject._rename_deprecated_fields precedence."""
|
||||
raw_query_object = raw_query_context["queries"][0]
|
||||
raw_query_object["columns"] = ["state"]
|
||||
raw_query_object["groupby"] = ["name", "gender"]
|
||||
query_object = query_object_factory.create(
|
||||
raw_query_context["result_type"], **raw_query_object
|
||||
)
|
||||
assert query_object.columns == ["name", "gender"]
|
||||
|
||||
def test_deprecated_groupby_empty_list_is_ignored(
|
||||
self,
|
||||
query_object_factory: QueryObjectFactory,
|
||||
raw_query_context: dict[str, Any],
|
||||
):
|
||||
"""groupby=[] is falsy — popped but columns should default to []."""
|
||||
raw_query_object = raw_query_context["queries"][0]
|
||||
raw_query_object.pop("columns", None)
|
||||
raw_query_object["groupby"] = []
|
||||
query_object = query_object_factory.create(
|
||||
raw_query_context["result_type"], **raw_query_object
|
||||
)
|
||||
assert query_object.columns == []
|
||||
assert not hasattr(query_object, "groupby")
|
||||
|
||||
def test_deprecated_granularity_sqla_renamed_to_granularity(
|
||||
self,
|
||||
query_object_factory: QueryObjectFactory,
|
||||
raw_query_context: dict[str, Any],
|
||||
):
|
||||
"""granularity_sqla in stored query_context should be renamed to granularity."""
|
||||
raw_query_object = raw_query_context["queries"][0]
|
||||
raw_query_object.pop("granularity", None)
|
||||
raw_query_object["granularity_sqla"] = "ds"
|
||||
query_object = query_object_factory.create(
|
||||
raw_query_context["result_type"], **raw_query_object
|
||||
)
|
||||
assert query_object.granularity == "ds"
|
||||
|
||||
def test_deprecated_timeseries_limit_renamed_to_series_limit(
|
||||
self,
|
||||
query_object_factory: QueryObjectFactory,
|
||||
raw_query_context: dict[str, Any],
|
||||
):
|
||||
"""timeseries_limit should be renamed to series_limit."""
|
||||
raw_query_object = raw_query_context["queries"][0]
|
||||
raw_query_object.pop("series_limit", None)
|
||||
raw_query_object["timeseries_limit"] = 5
|
||||
query_object = query_object_factory.create(
|
||||
raw_query_context["result_type"], **raw_query_object
|
||||
)
|
||||
assert query_object.series_limit == 5
|
||||
|
||||
def test_deprecated_timeseries_limit_zero_is_not_renamed(
|
||||
self,
|
||||
query_object_factory: QueryObjectFactory,
|
||||
raw_query_context: dict[str, Any],
|
||||
):
|
||||
"""timeseries_limit=0 is falsy — popped; series_limit defaults to 0."""
|
||||
raw_query_object = raw_query_context["queries"][0]
|
||||
raw_query_object.pop("series_limit", None)
|
||||
raw_query_object["timeseries_limit"] = 0
|
||||
query_object = query_object_factory.create(
|
||||
raw_query_context["result_type"], **raw_query_object
|
||||
)
|
||||
# 0 is falsy so it does not propagate — QueryObject default is also 0
|
||||
assert query_object.series_limit == 0
|
||||
|
||||
Reference in New Issue
Block a user