mirror of
https://github.com/apache/superset.git
synced 2026-07-16 03:35:45 +00:00
Compare commits
7 Commits
fix/issue-
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6814bc8925 | ||
|
|
4e8d0e0abe | ||
|
|
5da4fe7996 | ||
|
|
f1c0a461fd | ||
|
|
ef10501e6b | ||
|
|
17ae25b8d4 | ||
|
|
2a77c0156a |
8
superset-frontend/package-lock.json
generated
8
superset-frontend/package-lock.json
generated
@@ -34018,9 +34018,9 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz",
|
||||
"integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
||||
"integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
@@ -41919,7 +41919,7 @@
|
||||
"react-draggable": "^4.7.0",
|
||||
"react-error-boundary": "^6.1.2",
|
||||
"react-js-cron": "^5.2.0",
|
||||
"react-markdown": "^9.0.7",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resize-detector": "^7.1.2",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"react-ultimate-pagination": "^1.3.2",
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"react-draggable": "^4.7.0",
|
||||
"react-error-boundary": "^6.1.2",
|
||||
"react-js-cron": "^5.2.0",
|
||||
"react-markdown": "^9.0.7",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resize-detector": "^7.1.2",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"react-ultimate-pagination": "^1.3.2",
|
||||
|
||||
@@ -547,6 +547,7 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
paginationPageSizeSelector={PAGE_SIZE_OPTIONS}
|
||||
suppressDragLeaveHidesColumns
|
||||
pinnedBottomRowData={showTotals ? [cleanedTotals] : undefined}
|
||||
tooltipShowDelay={500}
|
||||
localeText={{
|
||||
// Pagination controls
|
||||
next: t('Next'),
|
||||
|
||||
@@ -448,10 +448,18 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
/>
|
||||
);
|
||||
|
||||
const columnsForKeys = isUsingTimeComparison
|
||||
? (filteredColumns as InputColumn[])
|
||||
: (columns as InputColumn[]);
|
||||
const descriptionsKey = columnsForKeys
|
||||
.map(col => `${col.key}:${col.description ?? ''}`)
|
||||
.join('|');
|
||||
|
||||
return (
|
||||
<StyledChartContainer height={height}>
|
||||
<AgGridDataTable
|
||||
gridHeight={gridHeight}
|
||||
key={descriptionsKey}
|
||||
data={data || []}
|
||||
colDefsFromProps={colDefs}
|
||||
includeSearch={!!includeSearch}
|
||||
|
||||
@@ -346,6 +346,7 @@ const processColumns = memoizeOne(function processColumns(
|
||||
column_config: columnConfig = {},
|
||||
query_mode: queryMode,
|
||||
},
|
||||
rawDatasource,
|
||||
queriesData,
|
||||
} = props;
|
||||
const granularity = extractTimegrain(props.rawFormData);
|
||||
@@ -385,6 +386,19 @@ const processColumns = memoizeOne(function processColumns(
|
||||
? config.currencyFormat
|
||||
: savedCurrency;
|
||||
|
||||
// Internal names of metrics expressed as percentages have a "%" prefix,
|
||||
// however, their storage locations are defined in rawDatasource.metrics using the original names.
|
||||
const metricLookupKey = key.startsWith('%') ? key.slice(1) : key;
|
||||
const description =
|
||||
rawDatasource.columns?.find(
|
||||
(item: { column_name?: string; description?: string | null }) =>
|
||||
item.column_name === key,
|
||||
)?.description ??
|
||||
rawDatasource.metrics?.find(
|
||||
(item: { metric_name?: string; description?: string | null }) =>
|
||||
item.metric_name === metricLookupKey,
|
||||
)?.description;
|
||||
|
||||
let formatter;
|
||||
|
||||
if (isTime || config.d3TimeFormat) {
|
||||
@@ -431,6 +445,7 @@ const processColumns = memoizeOne(function processColumns(
|
||||
isPercentMetric,
|
||||
formatter,
|
||||
config,
|
||||
description,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
|
||||
@@ -192,6 +192,7 @@ export interface InputColumn {
|
||||
TimeFormatter | NumberFormatter | CustomFormatter | CurrencyFormatter;
|
||||
originalLabel?: string;
|
||||
metricName?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export type ValueRange = [number, number] | null;
|
||||
|
||||
@@ -19,12 +19,34 @@
|
||||
import { isEqualArray } from '@superset-ui/core';
|
||||
import { TableChartProps } from '../types';
|
||||
|
||||
const getDescriptions = (props: TableChartProps) => {
|
||||
const colnames = props.queriesData?.[0]?.colnames || [];
|
||||
const columns = props.rawDatasource?.columns || [];
|
||||
const metrics = props.rawDatasource?.metrics || [];
|
||||
|
||||
return colnames.map((key: string) => {
|
||||
// Internal names of metrics expressed as percentages have a "%" prefix,
|
||||
// however, their storage locations are defined in rawDatasource.metrics using the original names.
|
||||
const metricLookupKey = key.startsWith('%') ? key.slice(1) : key;
|
||||
return (
|
||||
columns.find((item: { column_name: string }) => item.column_name === key)
|
||||
?.description ??
|
||||
metrics.find(
|
||||
(item: { metric_name: string }) => item.metric_name === metricLookupKey,
|
||||
)?.description
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default function isEqualColumns(
|
||||
propsA: TableChartProps[],
|
||||
propsB: TableChartProps[],
|
||||
) {
|
||||
const a = propsA[0];
|
||||
const b = propsB[0];
|
||||
|
||||
const descA = getDescriptions(a);
|
||||
const descB = getDescriptions(b);
|
||||
return (
|
||||
a.datasource.columnFormats === b.datasource.columnFormats &&
|
||||
a.datasource.currencyFormats === b.datasource.currencyFormats &&
|
||||
@@ -41,6 +63,7 @@ export default function isEqualColumns(
|
||||
JSON.stringify(a.formData.extraFormData || null) ===
|
||||
JSON.stringify(b.formData.extraFormData || null) &&
|
||||
JSON.stringify(a.rawFormData.column_config || null) ===
|
||||
JSON.stringify(b.rawFormData.column_config || null)
|
||||
JSON.stringify(b.rawFormData.column_config || null) &&
|
||||
JSON.stringify(descA) === JSON.stringify(descB)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -290,6 +290,7 @@ export const useColDefs = ({
|
||||
return {
|
||||
field: colId,
|
||||
headerName: getHeaderLabel(col),
|
||||
headerTooltip: col.description,
|
||||
valueFormatter: p => valueFormatter(p, col),
|
||||
valueGetter: p => valueGetter(p, col),
|
||||
cellStyle: p => {
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import transformProps from '../src/transformProps';
|
||||
import { TableChartProps } from '../src/types';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import { QueryMode } from '@superset-ui/core';
|
||||
|
||||
function createMockChartProps(
|
||||
overrides: Partial<TableChartProps> = {},
|
||||
): TableChartProps {
|
||||
const defaultProps = {
|
||||
height: 400,
|
||||
width: 800,
|
||||
rawFormData: {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
query_mode: QueryMode.Aggregate,
|
||||
metrics: [],
|
||||
percent_metrics: [],
|
||||
column_config: {},
|
||||
table_timestamp_format: '',
|
||||
granularity_sqla: 'day',
|
||||
time_range: 'No filter',
|
||||
},
|
||||
queriesData: [
|
||||
{
|
||||
data: [],
|
||||
colnames: [],
|
||||
coltypes: [],
|
||||
rowcount: 0,
|
||||
applied_filters: [],
|
||||
rejected_filters: [],
|
||||
},
|
||||
],
|
||||
datasource: {
|
||||
columns: [],
|
||||
metrics: [],
|
||||
columnFormats: {},
|
||||
currencyFormats: {},
|
||||
verboseMap: {},
|
||||
},
|
||||
rawDatasource: {
|
||||
columns: [],
|
||||
metrics: [],
|
||||
},
|
||||
filterState: {},
|
||||
hooks: { setDataMask: jest.fn(), onChartStateChange: jest.fn() },
|
||||
ownState: {},
|
||||
emitCrossFilters: false,
|
||||
theme: {},
|
||||
...overrides,
|
||||
};
|
||||
return defaultProps as unknown as TableChartProps;
|
||||
}
|
||||
|
||||
test('extracts description from datasource.columns for a regular column', () => {
|
||||
const props = createMockChartProps({
|
||||
queriesData: [
|
||||
{
|
||||
data: [{ col1: 'value' }],
|
||||
colnames: ['col1'],
|
||||
coltypes: [GenericDataType.String],
|
||||
rowcount: 1,
|
||||
applied_filters: [],
|
||||
rejected_filters: [],
|
||||
} as unknown as TableChartProps['queriesData'][number],
|
||||
],
|
||||
rawDatasource: {
|
||||
columns: [
|
||||
{ column_name: 'col1', description: 'This is a column description' },
|
||||
],
|
||||
metrics: [],
|
||||
},
|
||||
});
|
||||
|
||||
const result = transformProps(props);
|
||||
const { columns } = result;
|
||||
const columnMeta = columns.find(c => c.key === 'col1');
|
||||
expect(columnMeta).toBeDefined();
|
||||
expect(columnMeta!.description).toBe('This is a column description');
|
||||
});
|
||||
|
||||
test('extracts description from datasource.metrics for a metric column', () => {
|
||||
const props = createMockChartProps({
|
||||
rawFormData: {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
query_mode: QueryMode.Aggregate,
|
||||
metrics: ['sum_sales'],
|
||||
percent_metrics: [],
|
||||
column_config: {},
|
||||
table_timestamp_format: '',
|
||||
granularity_sqla: 'day',
|
||||
time_range: 'No filter',
|
||||
},
|
||||
queriesData: [
|
||||
{
|
||||
data: [{ sum_sales: 100 }],
|
||||
colnames: ['sum_sales'],
|
||||
coltypes: [GenericDataType.Numeric],
|
||||
rowcount: 1,
|
||||
applied_filters: [],
|
||||
rejected_filters: [],
|
||||
},
|
||||
] as unknown as TableChartProps['queriesData'],
|
||||
rawDatasource: {
|
||||
columns: [],
|
||||
metrics: [
|
||||
{ metric_name: 'sum_sales', description: 'Total sales amount' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = transformProps(props);
|
||||
const { columns } = result;
|
||||
const columnMeta = columns.find(c => c.key === 'sum_sales');
|
||||
expect(columnMeta).toBeDefined();
|
||||
expect(columnMeta!.description).toBe('Total sales amount');
|
||||
});
|
||||
|
||||
test('prefers column description over metric description when both exist with same key', () => {
|
||||
const props = createMockChartProps({
|
||||
rawFormData: {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
query_mode: QueryMode.Aggregate,
|
||||
metrics: ['revenue'],
|
||||
percent_metrics: [],
|
||||
column_config: {},
|
||||
table_timestamp_format: '',
|
||||
granularity_sqla: 'day',
|
||||
time_range: 'No filter',
|
||||
},
|
||||
queriesData: [
|
||||
{
|
||||
data: [{ revenue: 500 }],
|
||||
colnames: ['revenue'],
|
||||
coltypes: [GenericDataType.Numeric],
|
||||
rowcount: 1,
|
||||
applied_filters: [],
|
||||
rejected_filters: [],
|
||||
},
|
||||
] as unknown as TableChartProps['queriesData'],
|
||||
rawDatasource: {
|
||||
columns: [{ column_name: 'revenue', description: 'Column desc' }],
|
||||
metrics: [{ metric_name: 'revenue', description: 'Metric desc' }],
|
||||
},
|
||||
});
|
||||
|
||||
const result = transformProps(props);
|
||||
const { columns } = result;
|
||||
const columnMeta = columns.find(c => c.key === 'revenue');
|
||||
expect(columnMeta!.description).toBe('Column desc');
|
||||
});
|
||||
|
||||
test('handles percent metrics correctly – uses base metric name for lookup', () => {
|
||||
const props = createMockChartProps({
|
||||
rawFormData: {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
query_mode: QueryMode.Aggregate,
|
||||
metrics: ['profit'],
|
||||
percent_metrics: ['profit'],
|
||||
column_config: {},
|
||||
table_timestamp_format: '',
|
||||
granularity_sqla: 'day',
|
||||
time_range: 'No filter',
|
||||
},
|
||||
queriesData: [
|
||||
{
|
||||
data: [{ '%profit': 0.15 }],
|
||||
colnames: ['%profit'],
|
||||
coltypes: [GenericDataType.Numeric],
|
||||
rowcount: 1,
|
||||
applied_filters: [],
|
||||
rejected_filters: [],
|
||||
},
|
||||
] as unknown as TableChartProps['queriesData'],
|
||||
rawDatasource: {
|
||||
columns: [],
|
||||
metrics: [
|
||||
{ metric_name: 'profit', description: 'Profit margin percent' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = transformProps(props);
|
||||
const { columns } = result;
|
||||
const columnMeta = columns.find(c => c.key === '%profit');
|
||||
expect(columnMeta).toBeDefined();
|
||||
expect(columnMeta!.description).toBe('Profit margin percent');
|
||||
});
|
||||
|
||||
test('sets description to undefined when no matching column or metric is found', () => {
|
||||
const props = createMockChartProps({
|
||||
queriesData: [
|
||||
{
|
||||
data: [{ unknown_col: 'x' }],
|
||||
colnames: ['unknown_col'],
|
||||
coltypes: [GenericDataType.String],
|
||||
rowcount: 1,
|
||||
applied_filters: [],
|
||||
rejected_filters: [],
|
||||
},
|
||||
] as unknown as TableChartProps['queriesData'],
|
||||
rawDatasource: {
|
||||
columns: [],
|
||||
metrics: [],
|
||||
},
|
||||
});
|
||||
|
||||
const result = transformProps(props);
|
||||
const { columns } = result;
|
||||
const columnMeta = columns.find(c => c.key === 'unknown_col');
|
||||
expect(columnMeta!.description).toBeUndefined();
|
||||
});
|
||||
|
||||
test('uses description from column even when verboseMap renames the column', () => {
|
||||
const props = createMockChartProps({
|
||||
queriesData: [
|
||||
{
|
||||
data: [{ col_x: 10 }],
|
||||
colnames: ['col_x'],
|
||||
coltypes: [GenericDataType.Numeric],
|
||||
rowcount: 1,
|
||||
applied_filters: [],
|
||||
rejected_filters: [],
|
||||
},
|
||||
] as unknown as TableChartProps['queriesData'],
|
||||
datasource: {
|
||||
columns: [],
|
||||
metrics: [],
|
||||
columnFormats: {},
|
||||
currencyFormats: {},
|
||||
verboseMap: { col_x: 'Custom Label' },
|
||||
} as unknown as TableChartProps['datasource'],
|
||||
rawDatasource: {
|
||||
columns: [
|
||||
{ column_name: 'col_x', description: 'Original column description' },
|
||||
],
|
||||
metrics: [],
|
||||
},
|
||||
});
|
||||
|
||||
const result = transformProps(props);
|
||||
const { columns } = result;
|
||||
const columnMeta = columns.find(c => c.key === 'col_x');
|
||||
expect(columnMeta!.label).toBe('Custom Label');
|
||||
expect(columnMeta!.description).toBe('Original column description');
|
||||
});
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
CategoricalColorNamespace,
|
||||
getNumberFormatter,
|
||||
AxisType,
|
||||
getColumnLabel,
|
||||
getMetricLabel,
|
||||
NumberFormatter,
|
||||
tooltipHtml,
|
||||
@@ -140,13 +141,17 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
|
||||
const xAxisLabel: string = getMetricLabel(x);
|
||||
const yAxisLabel: string = getMetricLabel(y);
|
||||
const sizeLabel: string = getMetricLabel(size);
|
||||
const entityLabel: string = getColumnLabel(entity);
|
||||
const seriesLabel: string | undefined = bubbleSeries
|
||||
? getColumnLabel(bubbleSeries)
|
||||
: undefined;
|
||||
|
||||
const refs: Refs = {};
|
||||
|
||||
data.forEach(datum => {
|
||||
const dataName = bubbleSeries ? datum[bubbleSeries] : datum[entity];
|
||||
const dataName = seriesLabel ? datum[seriesLabel] : datum[entityLabel];
|
||||
const name = dataName ? String(dataName) : NULL_STRING;
|
||||
const bubbleSeriesValue = bubbleSeries ? datum[bubbleSeries] : null;
|
||||
const bubbleSeriesValue = seriesLabel ? datum[seriesLabel] : null;
|
||||
|
||||
series.push({
|
||||
name,
|
||||
@@ -155,7 +160,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
|
||||
datum[xAxisLabel],
|
||||
datum[yAxisLabel],
|
||||
datum[sizeLabel],
|
||||
datum[entity],
|
||||
datum[entityLabel],
|
||||
bubbleSeriesValue as any,
|
||||
],
|
||||
],
|
||||
|
||||
@@ -153,6 +153,78 @@ describe('Bubble transformProps', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('adhoc Custom SQL dimension', () => {
|
||||
const adhocColumn = {
|
||||
expressionType: 'SQL',
|
||||
sqlExpression: 'EXTRACT(YEAR FROM ds)',
|
||||
label: 'year',
|
||||
};
|
||||
const adhocQueriesData = [
|
||||
{
|
||||
data: [
|
||||
{
|
||||
year: 1992,
|
||||
customer_name: 'AV Stores, Co.',
|
||||
count: 10,
|
||||
'SUM(price_each)': 20,
|
||||
'SUM(sales)': 30,
|
||||
},
|
||||
{
|
||||
year: 1993,
|
||||
customer_name: 'Alpha Cognac',
|
||||
count: 40,
|
||||
'SUM(price_each)': 50,
|
||||
'SUM(sales)': 60,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
test('resolves the entity label so an adhoc dimension is not rendered as NULL', () => {
|
||||
const formData: SqlaFormData = { ...defaultFormData, entity: adhocColumn };
|
||||
const chartProps = new ChartProps({
|
||||
...chartConfig,
|
||||
queriesData: adhocQueriesData,
|
||||
formData,
|
||||
});
|
||||
const result = transformProps(chartProps as EchartsBubbleChartProps);
|
||||
|
||||
expect((result.echartOptions.legend as any).data).toEqual(['1992', '1993']);
|
||||
expect(result.echartOptions.series).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: '1992',
|
||||
data: [[10, 20, 30, 1992, null]],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('resolves the series label so an adhoc bubble series is not rendered as NULL', () => {
|
||||
const formData: SqlaFormData = {
|
||||
...defaultFormData,
|
||||
entity: 'customer_name',
|
||||
series: adhocColumn,
|
||||
};
|
||||
const chartProps = new ChartProps({
|
||||
...chartConfig,
|
||||
queriesData: adhocQueriesData,
|
||||
formData,
|
||||
});
|
||||
const result = transformProps(chartProps as EchartsBubbleChartProps);
|
||||
|
||||
expect((result.echartOptions.legend as any).data).toEqual(['1992', '1993']);
|
||||
expect(result.echartOptions.series).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: '1992',
|
||||
data: [[10, 20, 30, 'AV Stores, Co.', 1992]],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bubble formatTooltip', () => {
|
||||
const dollerFormatter = getNumberFormatter('$,.2f');
|
||||
const percentFormatter = getNumberFormatter(',.1%');
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
SupersetClient,
|
||||
getClientErrorObject,
|
||||
ClientErrorObject,
|
||||
sanitizeHtml,
|
||||
} from '@superset-ui/core';
|
||||
import setupErrorMessages from 'src/setup/setupErrorMessages';
|
||||
|
||||
@@ -41,7 +42,7 @@ function showApiMessage(resp: ClientErrorObject) {
|
||||
const severity = resp.severity || 'info';
|
||||
$(template)
|
||||
.addClass(`alert-${severity}`)
|
||||
.append(resp.message || '')
|
||||
.append(sanitizeHtml(resp.message || ''))
|
||||
.appendTo($('#alert-container'));
|
||||
}
|
||||
|
||||
|
||||
@@ -3331,9 +3331,16 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
|
||||
)
|
||||
if utils.is_adhoc_metric(col):
|
||||
# add adhoc sort by column to columns_by_name if not exists
|
||||
# Pass the template processor so a SIMPLE adhoc metric that
|
||||
# references a calculated column has that column's Jinja
|
||||
# expression rendered, matching SELECT/WHERE/GROUP BY. This
|
||||
# is a no-op for the SQL expression type, whose
|
||||
# `sqlExpression` was already rendered above; `processed`
|
||||
# only guards that branch.
|
||||
col = self.adhoc_metric_to_sqla(
|
||||
col,
|
||||
columns_by_name,
|
||||
template_processor=template_processor,
|
||||
processed=True,
|
||||
)
|
||||
# use the existing instance, if possible
|
||||
|
||||
@@ -31,6 +31,7 @@ from superset.db_engine_specs.odps import OdpsBaseEngineSpec, OdpsEngineSpec
|
||||
from superset.db_engine_specs.sqlite import SqliteEngineSpec
|
||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||
from superset.sql.parse import Table
|
||||
from superset.superset_typing import QueryObjectDict
|
||||
from superset.utils.database import get_example_database
|
||||
from tests.integration_tests.base_tests import SupersetTestCase
|
||||
from tests.integration_tests.test_app import app
|
||||
@@ -202,6 +203,51 @@ class SupersetTestCases(SupersetTestCase):
|
||||
in sql
|
||||
)
|
||||
|
||||
@mock.patch("superset.models.core.Database.db_engine_spec", BaseEngineSpec)
|
||||
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
|
||||
def test_jinja_calculated_column_in_order_by(self) -> None:
|
||||
"""
|
||||
A calculated column referenced by a SIMPLE adhoc metric in `orderby`
|
||||
should have its Jinja template rendered, the same way it is for
|
||||
SELECT/WHERE/GROUP BY.
|
||||
"""
|
||||
table = self.get_table(name="birth_names")
|
||||
TableColumn(
|
||||
column_name="gender_cc_jinja",
|
||||
type="VARCHAR(255)",
|
||||
table=table,
|
||||
expression="""
|
||||
case
|
||||
when gender='boy' then {{ "'male'" }}
|
||||
else {{ "'female'" }}
|
||||
end
|
||||
""",
|
||||
)
|
||||
|
||||
table.database.sqlalchemy_uri = "sqlite://"
|
||||
query_obj: QueryObjectDict = {
|
||||
"groupby": ["gender_cc_jinja"],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"orderby": [
|
||||
(
|
||||
{
|
||||
"expressionType": "SIMPLE",
|
||||
"column": {"column_name": "gender_cc_jinja"},
|
||||
"aggregate": "MAX",
|
||||
"label": "max_gender_cc_jinja",
|
||||
},
|
||||
True,
|
||||
)
|
||||
],
|
||||
}
|
||||
sql = table.get_query_str(query_obj)
|
||||
orderby_clause = sql.split("ORDER BY")[1]
|
||||
assert "{%" not in orderby_clause
|
||||
assert "{{" not in orderby_clause
|
||||
assert "'male'" in orderby_clause
|
||||
assert "'female'" in orderby_clause
|
||||
|
||||
|
||||
def test_time_grain_denylist():
|
||||
config = app.config.copy()
|
||||
|
||||
Reference in New Issue
Block a user