Compare commits

..

1 Commits

Author SHA1 Message Date
Beto Dealmeida
ba26de24ab fix: full CSV download in AgGrid 2026-07-02 12:25:11 -04:00
4 changed files with 31 additions and 119 deletions

View File

@@ -237,6 +237,20 @@ test('Should "export full CSV"', async () => {
expect(props.exportFullCSV).toHaveBeenCalledWith(371);
});
test('Should "export full CSV" for ag-grid table', async () => {
(global as any).featureFlags = {
[FeatureFlag.AllowFullCsvExport]: true,
};
const props = createProps(VizType.TableAgGrid);
renderWrapper(props);
openMenu();
expect(props.exportFullCSV).toHaveBeenCalledTimes(0);
userEvent.hover(screen.getByText('Download'));
userEvent.click(await screen.findByText('Export to full .CSV'));
expect(props.exportFullCSV).toHaveBeenCalledTimes(1);
expect(props.exportFullCSV).toHaveBeenCalledWith(371);
});
test('Should not show export full CSV if report is not table', async () => {
(global as any).featureFlags = {
[FeatureFlag.AllowFullCsvExport]: true,
@@ -274,6 +288,20 @@ test('Should "export full Excel"', async () => {
expect(props.exportFullXLSX).toHaveBeenCalledWith(371);
});
test('Should "export full Excel" for ag-grid table', async () => {
(global as any).featureFlags = {
[FeatureFlag.AllowFullCsvExport]: true,
};
const props = createProps(VizType.TableAgGrid);
renderWrapper(props);
openMenu();
expect(props.exportFullXLSX).toHaveBeenCalledTimes(0);
userEvent.hover(screen.getByText('Download'));
userEvent.click(await screen.findByText('Export to full Excel'));
expect(props.exportFullXLSX).toHaveBeenCalledTimes(1);
expect(props.exportFullXLSX).toHaveBeenCalledWith(371);
});
test('Should not show export full Excel if report is not table', async () => {
(global as any).featureFlags = {
[FeatureFlag.AllowFullCsvExport]: true,

View File

@@ -372,7 +372,8 @@ const SliceHeaderControls = (
supersetCanShare = false,
isCached = [],
} = props;
const isTable = slice.viz_type === VizType.Table;
const isTable =
slice.viz_type === VizType.Table || slice.viz_type === VizType.TableAgGrid;
const isPivotTable = slice.viz_type === VizType.PivotTable;
const cachedWhen = (cachedDttm || []).map(itemCachedDttm =>
(extendedDayjs.utc(itemCachedDttm) as any).fromNow(),

View File

@@ -43,10 +43,6 @@ from superset_core.semantic_layers.view import (
)
from superset.common.query_object import QueryObject
from superset.exceptions import (
InvalidPostProcessingError,
QueryObjectValidationError,
)
from superset.explorables.base import TimeGrainDict
from superset.extensions import encrypted_field_factory
from superset.models.helpers import AuditMixinNullable, QueryResult
@@ -285,13 +281,7 @@ class SemanticView(AuditMixinNullable, Model):
# =========================================================================
def get_query_result(self, query_object: QueryObject) -> QueryResult:
result = get_results(query_object)
if query_object.post_processing and not result.df.empty:
try:
result.df = query_object.exec_post_processing(result.df)
except InvalidPostProcessingError as ex:
raise QueryObjectValidationError(ex.message) from ex
return result
return get_results(query_object)
def get_query_str(self, query_obj: QueryObjectDict) -> str:
return "Not implemented for semantic layers"

View File

@@ -662,7 +662,6 @@ def test_semantic_view_get_query_result(
view = SemanticView()
mock_query_object = MagicMock()
mock_query_object.post_processing = []
mock_result = MagicMock()
with patch(
@@ -672,115 +671,9 @@ def test_semantic_view_get_query_result(
result = view.get_query_result(mock_query_object)
mock_get_results.assert_called_once_with(mock_query_object)
mock_query_object.exec_post_processing.assert_not_called()
assert result == mock_result
def test_semantic_view_get_query_result_runs_post_processing(
mock_implementation: MagicMock,
) -> None:
"""
``get_query_result`` must run ``query_object.exec_post_processing`` so that
features like ``percent_metrics`` (contribution) are applied to the semantic
layer's DataFrame — matching the dataset flow in
``superset/models/helpers.py``.
"""
import pandas as pd
view = SemanticView()
input_df = pd.DataFrame({"Orders Count": [40000.0]})
processed_df = pd.DataFrame({"Orders Count": [40000.0], "%Orders Count": [1.0]})
mock_query_object = MagicMock()
mock_query_object.post_processing = [
{
"operation": "contribution",
"options": {
"columns": ["Orders Count"],
"rename_columns": ["%Orders Count"],
},
}
]
mock_query_object.exec_post_processing.return_value = processed_df
mock_result = MagicMock()
mock_result.df = input_df
with patch(
"superset.semantic_layers.models.get_results",
return_value=mock_result,
):
result = view.get_query_result(mock_query_object)
mock_query_object.exec_post_processing.assert_called_once_with(input_df)
assert result is mock_result
assert list(result.df.columns) == ["Orders Count", "%Orders Count"]
def test_semantic_view_get_query_result_wraps_post_processing_errors(
mock_implementation: MagicMock,
) -> None:
"""
``InvalidPostProcessingError`` raised from post-processing must be re-raised
as ``QueryObjectValidationError`` so the API surfaces a clean 400 rather
than a 500.
"""
import pandas as pd
from superset.exceptions import (
InvalidPostProcessingError,
QueryObjectValidationError,
)
view = SemanticView()
mock_query_object = MagicMock()
mock_query_object.post_processing = [{"operation": "bogus"}]
mock_query_object.exec_post_processing.side_effect = InvalidPostProcessingError(
"boom"
)
mock_result = MagicMock()
mock_result.df = pd.DataFrame({"count": [1]})
with (
patch(
"superset.semantic_layers.models.get_results",
return_value=mock_result,
),
pytest.raises(QueryObjectValidationError, match="boom"),
):
view.get_query_result(mock_query_object)
def test_semantic_view_get_query_result_skips_post_processing_on_empty_df(
mock_implementation: MagicMock,
) -> None:
"""
Match the dataset flow's guard: skip post-processing when the DataFrame is
empty. Contribution and other ops assume at least one row.
"""
import pandas as pd
view = SemanticView()
mock_query_object = MagicMock()
mock_query_object.post_processing = [{"operation": "contribution"}]
mock_result = MagicMock()
mock_result.df = pd.DataFrame()
with patch(
"superset.semantic_layers.models.get_results",
return_value=mock_result,
):
result = view.get_query_result(mock_query_object)
mock_query_object.exec_post_processing.assert_not_called()
assert result is mock_result
def test_semantic_view_data_for_slices(
mock_implementation: MagicMock,
mock_dimensions: list[Dimension],