mirror of
https://github.com/apache/superset.git
synced 2026-07-18 20:55:47 +00:00
feat: semantic layer extension (#37815)
This commit is contained in:
@@ -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={[
|
||||
|
||||
@@ -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
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -69,7 +69,7 @@ const baseFormData = {
|
||||
};
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
const store = mockStore({});
|
||||
const store = mockStore({ explore: {} });
|
||||
|
||||
function setup({
|
||||
value = undefined,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user