feat: semantic layer extension (#37815)

This commit is contained in:
Beto Dealmeida
2026-05-05 12:07:46 -04:00
committed by GitHub
parent 9e91ae8cff
commit cb53745d43
141 changed files with 18851 additions and 667 deletions

View File

@@ -142,6 +142,10 @@ const ColumnSelectPopover = ({
const datasourceType = useSelector<ExplorePageState, string | undefined>(
state => state.explore.datasource.type,
);
const compatibleDimensions = useSelector<
ExplorePageState,
string[] | null | undefined
>(state => state.explore.compatibleDimensions);
const [initialLabel] = useState(label);
const [initialAdhocColumn, initialCalculatedColumn, initialSimpleColumn] =
getInitialColumnValues(editedColumn);
@@ -167,21 +171,22 @@ const ColumnSelectPopover = ({
const sqlEditorRef = useRef<editors.EditorHandle>(null);
const [calculatedColumns, simpleColumns] = useMemo(
() =>
columns?.reduce(
(acc: [ColumnMeta[], ColumnMeta[]], column: ColumnMeta) => {
if (column.expression) {
acc[0].push(column);
} else {
acc[1].push(column);
}
return acc;
},
[[], []],
),
[columns],
);
const [calculatedColumns, simpleColumns] = useMemo(() => {
const [calc, simple] = (columns ?? []).reduce(
(acc: [ColumnMeta[], ColumnMeta[]], column: ColumnMeta) => {
if (column.expression) {
acc[0].push(column);
} else {
acc[1].push(column);
}
return acc;
},
[[], []],
);
const alpha = (a: ColumnMeta, b: ColumnMeta) =>
(a.column_name ?? '').localeCompare(b.column_name ?? '');
return [calc.sort(alpha), simple.sort(alpha)];
}, [columns]);
// Filter metrics that are already selected in the chart
const availableMetrics = useMemo(() => {
@@ -551,6 +556,11 @@ const ColumnSelectPopover = ({
key: `column-${simpleColumn.column_name}`,
column_name: simpleColumn.column_name,
verbose_name: simpleColumn.verbose_name ?? '',
disabled:
compatibleDimensions != null &&
!compatibleDimensions.includes(
simpleColumn.column_name,
),
})),
...availableMetrics.map(metric => ({
value: metric.metric_name,
@@ -565,6 +575,9 @@ const ColumnSelectPopover = ({
key: `metric-${metric.metric_name}`,
metric_name: metric.metric_name,
verbose_name: metric.verbose_name ?? '',
disabled:
compatibleDimensions != null &&
!compatibleDimensions.includes(metric.metric_name),
})),
]}
optionFilterProps={[

View File

@@ -23,6 +23,7 @@ import AdhocFilter from 'src/explore/components/controls/FilterControl/AdhocFilt
import { OptionSortType } from 'src/explore/types';
import { useGetTimeRangeLabel } from 'src/explore/components/controls/FilterControl/utils';
import OptionWrapper from './OptionWrapper';
import { datasetLabelLower } from 'src/features/semanticLayers/label';
export interface DndAdhocFilterOptionProps {
adhocFilter: AdhocFilter;
@@ -68,7 +69,10 @@ export default function DndAdhocFilterOption({
isExtra={adhocFilter.isExtra}
datasourceWarningMessage={
adhocFilter.datasourceWarning
? t('This filter might be incompatible with current dataset')
? t(
'This filter might be incompatible with current %s',
datasetLabelLower(),
)
: undefined
}
/>

View File

@@ -38,6 +38,7 @@ import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetr
import MetricDefinitionValue from 'src/explore/components/controls/MetricControl/MetricDefinitionValue';
import ColumnSelectPopoverTrigger from './ColumnSelectPopoverTrigger';
import { DndControlProps } from './types';
import { datasetLabelLower } from 'src/features/semanticLayers/label';
const AGGREGATED_DECK_GL_CHART_TYPES = [
'deck_screengrid',
@@ -129,6 +130,16 @@ function DndColumnMetricSelect(props: DndColumnMetricSelectProps) {
formData,
} = props;
// Semantic views do not support arbitrary SQL expressions as dimensions.
// Merge 'sqlExpression' into disabledTabs so the Custom SQL tab is hidden.
const effectiveDisabledTabs = useMemo(
() =>
String(datasource?.type) === 'semantic_view'
? new Set([...(disabledTabs ?? []), 'sqlExpression'])
: disabledTabs,
[datasource?.type, disabledTabs],
);
const [newColumnPopoverVisible, setNewColumnPopoverVisible] = useState(false);
const combinedOptionsMap = useMemo(() => {
@@ -303,7 +314,7 @@ function DndColumnMetricSelect(props: DndColumnMetricSelectProps) {
}}
editedColumn={column}
isTemporal={isTemporal}
disabledTabs={disabledTabs}
disabledTabs={effectiveDisabledTabs}
>
<OptionWrapper
key={`column-${idx}`}
@@ -326,7 +337,10 @@ function DndColumnMetricSelect(props: DndColumnMetricSelectProps) {
typeof item === 'object' &&
'error_text' in item &&
item.error_text)
? t('This metric might be incompatible with current dataset')
? t(
'This metric might be incompatible with current %s',
datasetLabelLower(),
)
: undefined;
return (
@@ -440,7 +454,7 @@ function DndColumnMetricSelect(props: DndColumnMetricSelectProps) {
togglePopover={toggleColumnPopover}
closePopover={closeColumnPopover}
isTemporal={false}
disabledTabs={disabledTabs}
disabledTabs={effectiveDisabledTabs}
metrics={savedMetrics}
selectedMetrics={selectedMetrics}
>

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { t } from '@apache-superset/core/translation';
import { AdhocColumn, QueryFormColumn, isAdhocColumn } from '@superset-ui/core';
import { tn } from '@apache-superset/core/translation';
@@ -27,8 +28,10 @@ import OptionWrapper from 'src/explore/components/controls/DndColumnSelectContro
import { OptionSelector } from 'src/explore/components/controls/DndColumnSelectControl/utils';
import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types';
import { DndItemType } from 'src/explore/components/DndItemType';
import { ExplorePageState } from 'src/explore/types';
import ColumnSelectPopoverTrigger from './ColumnSelectPopoverTrigger';
import { DndControlProps } from './types';
import { datasetLabelLower } from 'src/features/semanticLayers/label';
export type DndColumnSelectProps = DndControlProps<QueryFormColumn> & {
options: ColumnMeta[];
@@ -49,6 +52,19 @@ function DndColumnSelect(props: DndColumnSelectProps) {
isTemporal,
disabledTabs,
} = props;
// Semantic views do not support arbitrary SQL expressions as dimensions.
const datasourceType = useSelector<ExplorePageState, string | undefined>(
state => state.explore.datasource?.type,
);
const effectiveDisabledTabs = useMemo(
() =>
datasourceType === 'semantic_view'
? new Set([...(disabledTabs ?? []), 'sqlExpression'])
: disabledTabs,
[datasourceType, disabledTabs],
);
const [newColumnPopoverVisible, setNewColumnPopoverVisible] = useState(false);
const optionSelector = useMemo(() => {
@@ -103,7 +119,10 @@ function DndColumnSelect(props: DndColumnSelectProps) {
optionSelector.values.map((column, idx) => {
const datasourceWarningMessage =
isAdhocColumn(column) && column.datasourceWarning
? t('This column might be incompatible with current dataset')
? t(
'This column might be incompatible with current %s',
datasetLabelLower(),
)
: undefined;
const withCaret = isAdhocColumn(column) || !column.error_text;
@@ -121,7 +140,7 @@ function DndColumnSelect(props: DndColumnSelectProps) {
}}
editedColumn={column}
isTemporal={isTemporal}
disabledTabs={disabledTabs}
disabledTabs={effectiveDisabledTabs}
>
<OptionWrapper
key={idx}
@@ -205,7 +224,7 @@ function DndColumnSelect(props: DndColumnSelectProps) {
closePopover={closePopover}
visible={newColumnPopoverVisible}
isTemporal={isTemporal}
disabledTabs={disabledTabs}
disabledTabs={effectiveDisabledTabs}
>
<div />
</ColumnSelectPopoverTrigger>

View File

@@ -69,7 +69,7 @@ const baseFormData = {
};
const mockStore = configureStore([thunk]);
const store = mockStore({});
const store = mockStore({ explore: {} });
function setup({
value = undefined,

View File

@@ -69,14 +69,20 @@ const adhocMetricB = {
};
test('renders with default props', () => {
render(<DndMetricSelect {...defaultProps} />, { useDnd: true });
render(<DndMetricSelect {...defaultProps} />, {
useDnd: true,
useRedux: true,
});
expect(
screen.getByText('Drop a column/metric here or click'),
).toBeInTheDocument();
});
test('renders with default props and multi = true', () => {
render(<DndMetricSelect {...defaultProps} multi />, { useDnd: true });
render(<DndMetricSelect {...defaultProps} multi />, {
useDnd: true,
useRedux: true,
});
expect(
screen.getByText('Drop columns/metrics here or click'),
).toBeInTheDocument();
@@ -86,6 +92,7 @@ test('render selected metrics correctly', () => {
const metricValues = ['metric_a', 'metric_b', adhocMetricB];
render(<DndMetricSelect {...defaultProps} value={metricValues} multi />, {
useDnd: true,
useRedux: true,
});
expect(screen.getByText('metric_a')).toBeVisible();
expect(screen.getByText('Metric B')).toBeVisible();
@@ -107,6 +114,7 @@ test('warn selected custom metric when metric gets removed from dataset', async
/>,
{
useDnd: true,
useRedux: true,
},
);
@@ -159,6 +167,7 @@ test('warn selected custom metric when metric gets removed from dataset for sing
/>,
{
useDnd: true,
useRedux: true,
},
);
@@ -217,6 +226,7 @@ test('remove selected adhoc metric when column gets removed from dataset', async
/>,
{
useDnd: true,
useRedux: true,
},
);
@@ -259,6 +269,7 @@ test('update adhoc metric name when column label in dataset changes', () => {
/>,
{
useDnd: true,
useRedux: true,
},
);
@@ -304,6 +315,7 @@ test('can drag metrics', async () => {
const metricValues = ['metric_a', 'metric_b', adhocMetricB];
render(<DndMetricSelect {...defaultProps} value={metricValues} multi />, {
useDnd: true,
useRedux: true,
});
expect(screen.getByText('metric_a')).toBeVisible();
@@ -341,6 +353,7 @@ test('cannot drop a duplicated item', () => {
</>,
{
useDnd: true,
useRedux: true,
},
);
@@ -374,6 +387,7 @@ test('can drop a saved metric when disallow_adhoc_metrics', () => {
</>,
{
useDnd: true,
useRedux: true,
},
);
@@ -415,6 +429,7 @@ test('cannot drop non-saved metrics when disallow_adhoc_metrics', () => {
</>,
{
useDnd: true,
useRedux: true,
},
);
@@ -463,6 +478,7 @@ test('title changes on custom SQL text change', async () => {
/>,
{
useDnd: true,
useRedux: true,
},
);

View File

@@ -41,6 +41,7 @@ import { DndItemType } from 'src/explore/components/DndItemType';
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
import { savedMetricType } from 'src/explore/components/controls/MetricControl/types';
import { AGGREGATES } from 'src/explore/constants';
import { datasetLabelLower } from 'src/features/semanticLayers/label';
const EMPTY_OBJECT = {};
const DND_ACCEPTED_TYPES = [DndItemType.Column, DndItemType.Metric];
@@ -77,7 +78,10 @@ const coerceMetrics = (
) {
return {
metric_name: metric,
error_text: t('This metric might be incompatible with current dataset'),
error_text: t(
'This metric might be incompatible with current %s',
datasetLabelLower(),
),
uuid: nanoid(),
};
}
@@ -128,6 +132,26 @@ const DndMetricSelect = (props: any) => {
return extra;
}, [datasource?.extra]);
// Semantic views do not support arbitrary SQL expressions as metrics.
const disallowAdhocMetrics =
extra.disallow_adhoc_metrics || datasource?.type === 'semantic_view';
// AdhocMetricEditPopover reads `datasource.extra.disallow_adhoc_metrics`
// directly, so we need to inject the flag there too — not just in canDrop.
const datasourceForPopover = useMemo(() => {
if (!disallowAdhocMetrics || !datasource) return datasource;
let parsedExtra: Record<string, unknown> = {};
if (datasource.extra) {
try {
parsedExtra = JSON.parse(datasource.extra as string);
} catch {} // eslint-disable-line no-empty
}
return {
...datasource,
extra: JSON.stringify({ ...parsedExtra, disallow_adhoc_metrics: true }),
};
}, [disallowAdhocMetrics, datasource]);
const savedMetricSet = useMemo(
() =>
new Set(
@@ -184,7 +208,7 @@ const DndMetricSelect = (props: any) => {
const canDrop = useCallback(
(item: DatasourcePanelDndItem) => {
if (
extra.disallow_adhoc_metrics &&
disallowAdhocMetrics &&
(item.type !== DndItemType.Metric ||
!savedMetricSet.has(item.value.metric_name))
) {
@@ -293,14 +317,17 @@ const DndMetricSelect = (props: any) => {
columns={props.columns}
savedMetrics={props.savedMetrics}
savedMetricsOptions={getSavedMetricOptionsForMetric(index)}
datasource={props.datasource}
datasource={datasourceForPopover}
onMoveLabel={moveLabel}
onDropLabel={handleDropLabel}
type={`${DndItemType.AdhocMetricOption}_${props.name}_${props.label}`}
multi={multi}
datasourceWarningMessage={
option instanceof AdhocMetric && option.datasourceWarning
? t('This metric might be incompatible with current dataset')
? t(
'This metric might be incompatible with current %s',
datasetLabelLower(),
)
: undefined
}
/>
@@ -399,7 +426,7 @@ const DndMetricSelect = (props: any) => {
columns={props.columns}
savedMetricsOptions={newSavedMetricOptions}
savedMetric={EMPTY_OBJECT as savedMetricType}
datasource={props.datasource}
datasource={datasourceForPopover}
isControlledComponent
visible={newMetricPopoverVisible}
togglePopover={togglePopover}