mirror of
https://github.com/apache/superset.git
synced 2026-08-12 11:11:01 +00:00
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
808d905c9e | ||
|
|
152f11d1d4 | ||
|
|
b2784f297c | ||
|
|
0e249e6348 | ||
|
|
ab52bd14c4 | ||
|
|
5b4035427d | ||
|
|
07712fbd20 | ||
|
|
580243d84a | ||
|
|
4827fbb615 | ||
|
|
1d4f31c4df | ||
|
|
bf71d49bd6 | ||
|
|
47973bd789 | ||
|
|
1b089f828a | ||
|
|
3c20d62ed2 | ||
|
|
cc15ffd044 | ||
|
|
1f43b3fa0e | ||
|
|
fc5a3cd32b | ||
|
|
4490bffd50 | ||
|
|
a1a05134d2 | ||
|
|
8c69f13a71 | ||
|
|
4c645ce952 | ||
|
|
379403834a | ||
|
|
469acf12b2 | ||
|
|
bc58d13c34 | ||
|
|
ebbf4777e3 |
@@ -153,79 +153,6 @@ export function setForceQuery(force: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
export const SET_COMPATIBILITY = 'SET_COMPATIBILITY';
|
||||
export function setCompatibility(payload: {
|
||||
compatibleMetrics: string[] | null;
|
||||
compatibleDimensions: string[] | null;
|
||||
compatibilityLoading: boolean;
|
||||
}) {
|
||||
return { type: SET_COMPATIBILITY, ...payload };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch compatible metrics and dimensions for the current selection.
|
||||
*
|
||||
* Only fires for semantic views — SQL datasets always have full compatibility
|
||||
* so we short-circuit to `null` (no filtering) for everything else.
|
||||
*
|
||||
* Covers both real-time selection changes (M3) and saved-chart loading (M4):
|
||||
* call this thunk on mount as well as whenever the metric / dimension
|
||||
* selection changes in Explore.
|
||||
*/
|
||||
export function fetchCompatibility(
|
||||
datasourceType: string,
|
||||
datasourceId: number,
|
||||
selectedMetrics: string[],
|
||||
selectedDimensions: string[],
|
||||
) {
|
||||
return async (dispatch: Dispatch) => {
|
||||
if (datasourceType !== 'semantic_view') {
|
||||
dispatch(
|
||||
setCompatibility({
|
||||
compatibleMetrics: null,
|
||||
compatibleDimensions: null,
|
||||
compatibilityLoading: false,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
setCompatibility({
|
||||
compatibleMetrics: null,
|
||||
compatibleDimensions: null,
|
||||
compatibilityLoading: true,
|
||||
}),
|
||||
);
|
||||
|
||||
try {
|
||||
const { json } = await SupersetClient.post({
|
||||
endpoint: `/api/v1/datasource/${datasourceType}/${datasourceId}/compatible`,
|
||||
jsonPayload: {
|
||||
selected_metrics: selectedMetrics,
|
||||
selected_dimensions: selectedDimensions,
|
||||
},
|
||||
});
|
||||
dispatch(
|
||||
setCompatibility({
|
||||
compatibleMetrics: json.result.compatible_metrics,
|
||||
compatibleDimensions: json.result.compatible_dimensions,
|
||||
compatibilityLoading: false,
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// On error fall back to no filtering so the user is never blocked.
|
||||
dispatch(
|
||||
setCompatibility({
|
||||
compatibleMetrics: null,
|
||||
compatibleDimensions: null,
|
||||
compatibilityLoading: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const SET_STASH_FORM_DATA = 'SET_STASH_FORM_DATA';
|
||||
export function setStashFormData(
|
||||
isHidden: boolean,
|
||||
@@ -268,7 +195,6 @@ export const exploreActions = {
|
||||
sliceUpdated,
|
||||
setForceQuery,
|
||||
syncDatasourceMetadata,
|
||||
fetchCompatibility,
|
||||
};
|
||||
|
||||
export type ExploreActions = typeof exploreActions;
|
||||
|
||||
+2
-40
@@ -16,9 +16,8 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { RefObject, useMemo } from 'react';
|
||||
import { RefObject } from 'react';
|
||||
import { useDrag } from 'react-dnd';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Metric } from '@superset-ui/core';
|
||||
import { css, styled, useTheme } from '@apache-superset/core/theme';
|
||||
import { ColumnMeta } from '@superset-ui/chart-controls';
|
||||
@@ -28,7 +27,6 @@ import {
|
||||
StyledMetricOption,
|
||||
} from 'src/explore/components/optionRenderers';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { ExplorePageState } from 'src/explore/types';
|
||||
|
||||
import { DatasourcePanelDndItem } from '../types';
|
||||
|
||||
@@ -72,40 +70,11 @@ export default function DatasourcePanelDragOption(
|
||||
) {
|
||||
const { labelRef, showTooltip, type, value } = props;
|
||||
const theme = useTheme();
|
||||
|
||||
// Read compatibility lists from Redux.
|
||||
// `null` means no filtering is active (SQL datasets, or no selection yet).
|
||||
const compatibleMetrics = useSelector<
|
||||
ExplorePageState,
|
||||
string[] | null | undefined
|
||||
>(state => state.explore.compatibleMetrics);
|
||||
const compatibleDimensions = useSelector<
|
||||
ExplorePageState,
|
||||
string[] | null | undefined
|
||||
>(state => state.explore.compatibleDimensions);
|
||||
|
||||
// An item is compatible when the list is null (no filter) or when its
|
||||
// name explicitly appears in the list returned by the backend.
|
||||
const isCompatible = useMemo(() => {
|
||||
if (type === DndItemType.Metric) {
|
||||
if (!compatibleMetrics) return true;
|
||||
return compatibleMetrics.includes((value as Metric).metric_name);
|
||||
}
|
||||
if (type === DndItemType.Column) {
|
||||
if (!compatibleDimensions) return true;
|
||||
return compatibleDimensions.includes(
|
||||
(value as ColumnMeta).column_name,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}, [type, value, compatibleMetrics, compatibleDimensions]);
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
item: {
|
||||
value: props.value,
|
||||
type: props.type,
|
||||
},
|
||||
canDrag: isCompatible,
|
||||
collect: monitor => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
@@ -118,14 +87,7 @@ export default function DatasourcePanelDragOption(
|
||||
};
|
||||
|
||||
return (
|
||||
<DatasourceItemContainer
|
||||
data-test="DatasourcePanelDragOption"
|
||||
ref={drag}
|
||||
style={{
|
||||
opacity: isCompatible ? 1 : 0.35,
|
||||
cursor: isCompatible ? 'grab' : 'not-allowed',
|
||||
}}
|
||||
>
|
||||
<DatasourceItemContainer data-test="DatasourcePanelDragOption" ref={drag}>
|
||||
{type === DndItemType.Column ? (
|
||||
<StyledColumnOption column={value as ColumnMeta} {...optionProps} />
|
||||
) : (
|
||||
|
||||
@@ -122,7 +122,7 @@ const sortColumns = (slice: DatasourcePanelColumn[]) =>
|
||||
if (col2?.is_dttm && !col1?.is_dttm) {
|
||||
return 1;
|
||||
}
|
||||
return (col1?.column_name ?? '').localeCompare(col2?.column_name ?? '');
|
||||
return 0;
|
||||
})
|
||||
.sort((a, b) => (b?.is_certified ?? 0) - (a?.is_certified ?? 0));
|
||||
|
||||
@@ -191,9 +191,7 @@ export default function DataSourcePanel({
|
||||
|
||||
const filteredMetrics = useMemo(() => {
|
||||
if (!searchKeyword) {
|
||||
return [...(allowedMetrics ?? [])].sort((a, b) =>
|
||||
(a?.metric_name ?? '').localeCompare(b?.metric_name ?? ''),
|
||||
);
|
||||
return allowedMetrics ?? [];
|
||||
}
|
||||
return matchSorter(allowedMetrics, searchKeyword, {
|
||||
keys: [
|
||||
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
JsonObject,
|
||||
MatrixifyFormData,
|
||||
DatasourceType,
|
||||
ensureIsArray,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
ControlStateMapping,
|
||||
@@ -413,49 +412,6 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
|
||||
[originalTitle, theme?.brandAppName, theme?.brandLogoAlt],
|
||||
);
|
||||
|
||||
// M3 + M4: fire compatibility check on mount and whenever the metric /
|
||||
// dimension selection changes. Only semantic views use the endpoint;
|
||||
// SQL datasets short-circuit to null inside fetchCompatibility.
|
||||
const selectedMetrics = useMemo(
|
||||
() =>
|
||||
ensureIsArray(props.form_data.metrics).filter(
|
||||
(m): m is string => typeof m === 'string',
|
||||
),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[JSON.stringify(props.form_data.metrics)],
|
||||
);
|
||||
const selectedDimensions = useMemo(
|
||||
() =>
|
||||
[
|
||||
...ensureIsArray(props.form_data.groupby),
|
||||
...ensureIsArray(props.form_data.columns),
|
||||
...(typeof props.form_data.x_axis === 'string'
|
||||
? [props.form_data.x_axis]
|
||||
: []),
|
||||
].filter((d): d is string => typeof d === 'string'),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
JSON.stringify(props.form_data.groupby),
|
||||
JSON.stringify(props.form_data.columns),
|
||||
props.form_data.x_axis,
|
||||
],
|
||||
);
|
||||
useEffect(() => {
|
||||
props.actions.fetchCompatibility(
|
||||
props.datasource.type,
|
||||
props.datasource.id as number,
|
||||
selectedMetrics,
|
||||
selectedDimensions,
|
||||
);
|
||||
// props.datasource.id covers the saved-chart-loading case (M4)
|
||||
}, [
|
||||
props.datasource.id,
|
||||
props.datasource.type,
|
||||
selectedMetrics,
|
||||
selectedDimensions,
|
||||
]);
|
||||
|
||||
|
||||
const addHistory = useCallback(
|
||||
async ({ isReplace = false, title } = {}) => {
|
||||
const formData = props.dashboardId
|
||||
|
||||
+15
-28
@@ -142,10 +142,6 @@ 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);
|
||||
@@ -171,22 +167,21 @@ const ColumnSelectPopover = ({
|
||||
|
||||
const sqlEditorRef = useRef<editors.EditorHandle>(null);
|
||||
|
||||
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]);
|
||||
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],
|
||||
);
|
||||
|
||||
// Filter metrics that are already selected in the chart
|
||||
const availableMetrics = useMemo(() => {
|
||||
@@ -556,11 +551,6 @@ 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,
|
||||
@@ -575,9 +565,6 @@ 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={[
|
||||
|
||||
+2
-12
@@ -130,16 +130,6 @@ 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(
|
||||
() =>
|
||||
datasource?.type === 'semantic_view'
|
||||
? new Set([...(disabledTabs ?? []), 'sqlExpression'])
|
||||
: disabledTabs,
|
||||
[datasource?.type, disabledTabs],
|
||||
);
|
||||
|
||||
const [newColumnPopoverVisible, setNewColumnPopoverVisible] = useState(false);
|
||||
|
||||
const combinedOptionsMap = useMemo(() => {
|
||||
@@ -314,7 +304,7 @@ function DndColumnMetricSelect(props: DndColumnMetricSelectProps) {
|
||||
}}
|
||||
editedColumn={column}
|
||||
isTemporal={isTemporal}
|
||||
disabledTabs={effectiveDisabledTabs}
|
||||
disabledTabs={disabledTabs}
|
||||
>
|
||||
<OptionWrapper
|
||||
key={`column-${idx}`}
|
||||
@@ -454,7 +444,7 @@ function DndColumnMetricSelect(props: DndColumnMetricSelectProps) {
|
||||
togglePopover={toggleColumnPopover}
|
||||
closePopover={closeColumnPopover}
|
||||
isTemporal={false}
|
||||
disabledTabs={effectiveDisabledTabs}
|
||||
disabledTabs={disabledTabs}
|
||||
metrics={savedMetrics}
|
||||
selectedMetrics={selectedMetrics}
|
||||
>
|
||||
|
||||
+2
-17
@@ -17,7 +17,6 @@
|
||||
* 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';
|
||||
@@ -28,7 +27,6 @@ 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/utils/semanticLayerLabels';
|
||||
@@ -52,19 +50,6 @@ 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(() => {
|
||||
@@ -140,7 +125,7 @@ function DndColumnSelect(props: DndColumnSelectProps) {
|
||||
}}
|
||||
editedColumn={column}
|
||||
isTemporal={isTemporal}
|
||||
disabledTabs={effectiveDisabledTabs}
|
||||
disabledTabs={disabledTabs}
|
||||
>
|
||||
<OptionWrapper
|
||||
key={idx}
|
||||
@@ -224,7 +209,7 @@ function DndColumnSelect(props: DndColumnSelectProps) {
|
||||
closePopover={closePopover}
|
||||
visible={newColumnPopoverVisible}
|
||||
isTemporal={isTemporal}
|
||||
disabledTabs={effectiveDisabledTabs}
|
||||
disabledTabs={disabledTabs}
|
||||
>
|
||||
<div />
|
||||
</ColumnSelectPopoverTrigger>
|
||||
|
||||
+3
-23
@@ -132,26 +132,6 @@ 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(
|
||||
@@ -204,7 +184,7 @@ const DndMetricSelect = (props: any) => {
|
||||
const canDrop = useCallback(
|
||||
(item: DatasourcePanelDndItem) => {
|
||||
if (
|
||||
disallowAdhocMetrics &&
|
||||
extra.disallow_adhoc_metrics &&
|
||||
(item.type !== DndItemType.Metric ||
|
||||
!savedMetricSet.has(item.value.metric_name))
|
||||
) {
|
||||
@@ -313,7 +293,7 @@ const DndMetricSelect = (props: any) => {
|
||||
columns={props.columns}
|
||||
savedMetrics={props.savedMetrics}
|
||||
savedMetricsOptions={getSavedMetricOptionsForMetric(index)}
|
||||
datasource={datasourceForPopover}
|
||||
datasource={props.datasource}
|
||||
onMoveLabel={moveLabel}
|
||||
onDropLabel={handleDropLabel}
|
||||
type={`${DndItemType.AdhocMetricOption}_${props.name}_${props.label}`}
|
||||
@@ -422,7 +402,7 @@ const DndMetricSelect = (props: any) => {
|
||||
columns={props.columns}
|
||||
savedMetricsOptions={newSavedMetricOptions}
|
||||
savedMetric={EMPTY_OBJECT as savedMetricType}
|
||||
datasource={datasourceForPopover}
|
||||
datasource={props.datasource}
|
||||
isControlledComponent
|
||||
visible={newMetricPopoverVisible}
|
||||
togglePopover={togglePopover}
|
||||
|
||||
+15
-19
@@ -415,25 +415,21 @@ export default class AdhocFilterEditPopover extends Component<
|
||||
</ErrorBoundary>
|
||||
),
|
||||
},
|
||||
...(datasource?.type === 'semantic_view'
|
||||
? []
|
||||
: [
|
||||
{
|
||||
key: ExpressionTypes.Sql,
|
||||
label: t('Custom SQL'),
|
||||
children: (
|
||||
<ErrorBoundary>
|
||||
<AdhocFilterEditPopoverSqlTabContent
|
||||
adhocFilter={this.state.adhocFilter}
|
||||
onChange={this.onAdhocFilterChange}
|
||||
options={this.props.options}
|
||||
height={this.state.height}
|
||||
datasource={datasource}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
),
|
||||
},
|
||||
]),
|
||||
{
|
||||
key: ExpressionTypes.Sql,
|
||||
label: t('Custom SQL'),
|
||||
children: (
|
||||
<ErrorBoundary>
|
||||
<AdhocFilterEditPopoverSqlTabContent
|
||||
adhocFilter={this.state.adhocFilter}
|
||||
onChange={this.onAdhocFilterChange}
|
||||
options={this.props.options}
|
||||
height={this.state.height}
|
||||
datasource={datasource}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{hasDeckSlices && (
|
||||
|
||||
+5
-34
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
/* eslint-disable camelcase */
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { isDefined, ensureIsArray, DatasourceType } from '@superset-ui/core';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import type { editors } from '@apache-superset/core';
|
||||
@@ -95,8 +94,6 @@ interface AdhocMetricEditPopoverProps {
|
||||
datasource?: DatasourceInfo;
|
||||
isNewMetric?: boolean;
|
||||
isLabelModified?: boolean;
|
||||
/** Names of metrics the user may select; null means no filtering. */
|
||||
compatibleMetrics?: string[] | null;
|
||||
}
|
||||
|
||||
interface AdhocMetricEditPopoverState {
|
||||
@@ -126,7 +123,7 @@ const StyledSelect = styled(Select)`
|
||||
|
||||
export const SAVED_TAB_KEY = 'SAVED';
|
||||
|
||||
class AdhocMetricEditPopover extends PureComponent<
|
||||
export default class AdhocMetricEditPopover extends PureComponent<
|
||||
AdhocMetricEditPopoverProps,
|
||||
AdhocMetricEditPopoverState
|
||||
> {
|
||||
@@ -441,24 +438,15 @@ class AdhocMetricEditPopover extends PureComponent<
|
||||
ensureIsArray(savedMetricsOptions).length > 0 ? (
|
||||
<FormItem label={t('Saved metric')}>
|
||||
<StyledSelect
|
||||
options={[...ensureIsArray(savedMetricsOptions)]
|
||||
.sort((a, b) =>
|
||||
(a.metric_name ?? '').localeCompare(
|
||||
b.metric_name ?? '',
|
||||
),
|
||||
)
|
||||
.map(savedMetric => ({
|
||||
options={ensureIsArray(savedMetricsOptions).map(
|
||||
savedMetric => ({
|
||||
value: savedMetric.metric_name,
|
||||
label: this.renderMetricOption(savedMetric),
|
||||
key: savedMetric.id,
|
||||
metric_name: savedMetric.metric_name,
|
||||
verbose_name: savedMetric.verbose_name ?? '',
|
||||
disabled:
|
||||
this.props.compatibleMetrics != null &&
|
||||
!this.props.compatibleMetrics.includes(
|
||||
savedMetric.metric_name,
|
||||
),
|
||||
}))}
|
||||
}),
|
||||
)}
|
||||
optionFilterProps={['metric_name', 'verbose_name']}
|
||||
{...savedSelectProps}
|
||||
/>
|
||||
@@ -608,20 +596,3 @@ class AdhocMetricEditPopover extends PureComponent<
|
||||
}
|
||||
// @ts-expect-error - defaultProps for backward compatibility
|
||||
AdhocMetricEditPopover.defaultProps = defaultProps;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Thin functional wrapper that injects compatibility data from Redux.
|
||||
// AdhocMetricEditPopover is a class component and cannot use hooks directly.
|
||||
// ---------------------------------------------------------------------------
|
||||
function AdhocMetricEditPopoverWithRedux(props: AdhocMetricEditPopoverProps) {
|
||||
const compatibleMetrics = useSelector(
|
||||
(state: any) =>
|
||||
state.explore?.compatibleMetrics as string[] | null | undefined,
|
||||
);
|
||||
return (
|
||||
<AdhocMetricEditPopover {...props} compatibleMetrics={compatibleMetrics} />
|
||||
);
|
||||
}
|
||||
|
||||
export { AdhocMetricEditPopover };
|
||||
export default AdhocMetricEditPopoverWithRedux;
|
||||
|
||||
@@ -65,9 +65,6 @@ export interface ExploreState {
|
||||
metadata?: {
|
||||
owners?: string[] | null;
|
||||
};
|
||||
compatibleMetrics?: string[] | null;
|
||||
compatibleDimensions?: string[] | null;
|
||||
compatibilityLoading?: boolean;
|
||||
saveAction?: SaveActionType | null;
|
||||
}
|
||||
|
||||
@@ -168,13 +165,6 @@ interface SetForceQueryAction {
|
||||
force: boolean;
|
||||
}
|
||||
|
||||
interface SetCompatibilityAction {
|
||||
type: typeof actions.SET_COMPATIBILITY;
|
||||
compatibleMetrics: string[] | null;
|
||||
compatibleDimensions: string[] | null;
|
||||
compatibilityLoading: boolean;
|
||||
}
|
||||
|
||||
type ExploreAction =
|
||||
| DynamicPluginControlsReadyAction
|
||||
| ToggleFaveStarAction
|
||||
@@ -193,7 +183,6 @@ type ExploreAction =
|
||||
| SetStashFormDataAction
|
||||
| SliceUpdatedAction
|
||||
| SetForceQueryAction
|
||||
| SetCompatibilityAction
|
||||
| HydrateExplore;
|
||||
|
||||
// Extended control state for dynamic form controls - uses Record for flexibility
|
||||
@@ -632,15 +621,6 @@ export default function exploreReducer(
|
||||
force: typedAction.force,
|
||||
};
|
||||
},
|
||||
[actions.SET_COMPATIBILITY]() {
|
||||
const typedAction = action as SetCompatibilityAction;
|
||||
return {
|
||||
...state,
|
||||
compatibleMetrics: typedAction.compatibleMetrics,
|
||||
compatibleDimensions: typedAction.compatibleDimensions,
|
||||
compatibilityLoading: typedAction.compatibilityLoading,
|
||||
};
|
||||
},
|
||||
[HYDRATE_EXPLORE]() {
|
||||
const typedAction = action as HydrateExplore;
|
||||
return {
|
||||
|
||||
@@ -130,9 +130,6 @@ export interface ExplorePageState {
|
||||
standalone: boolean;
|
||||
force: boolean;
|
||||
common: JsonObject;
|
||||
compatibleMetrics?: string[] | null;
|
||||
compatibleDimensions?: string[] | null;
|
||||
compatibilityLoading?: boolean;
|
||||
};
|
||||
sliceEntities?: JsonObject; // propagated from Dashboard view
|
||||
}
|
||||
|
||||
@@ -170,13 +170,13 @@ export default function SemanticLayerModal({
|
||||
setSelectedType(layer.type);
|
||||
setFormData(layer.configuration ?? {});
|
||||
setHasErrors(false);
|
||||
// In edit mode, fetch the enriched schema using the full saved
|
||||
// configuration so that dynamic dropdowns (account, project,
|
||||
// environment) show their human-readable labels immediately rather
|
||||
// than flashing raw IDs while the background refresh completes.
|
||||
// Fetch base schema (no configuration -> no Snowflake connection) to
|
||||
// show the form immediately. The existing maybeRefreshSchema machinery
|
||||
// will trigger an enriched fetch in the background once deps are
|
||||
// satisfied, and DynamicFieldControl will show per-field spinners.
|
||||
const { json: schemaJson } = await SupersetClient.post({
|
||||
endpoint: '/api/v1/semantic_layer/schema/configuration',
|
||||
jsonPayload: { type: layer.type, configuration: layer.configuration },
|
||||
jsonPayload: { type: layer.type },
|
||||
});
|
||||
applySchema(schemaJson.result);
|
||||
setStep('config');
|
||||
@@ -283,7 +283,7 @@ export default function SemanticLayerModal({
|
||||
|
||||
// Check if any dynamic field has all dependencies satisfied
|
||||
const hasSatisfiedDeps = Object.values(dynamicDeps).some(deps =>
|
||||
areDependenciesSatisfied(deps, data, configSchema ?? undefined),
|
||||
areDependenciesSatisfied(deps, data),
|
||||
);
|
||||
if (!hasSatisfiedDeps) return;
|
||||
|
||||
@@ -297,7 +297,7 @@ export default function SemanticLayerModal({
|
||||
fetchConfigSchema(selectedType, data);
|
||||
}, SCHEMA_REFRESH_DEBOUNCE_MS);
|
||||
},
|
||||
[selectedType, fetchConfigSchema, configSchema],
|
||||
[selectedType, fetchConfigSchema],
|
||||
);
|
||||
|
||||
const handleFormChange = useCallback(
|
||||
@@ -385,25 +385,16 @@ export default function SemanticLayerModal({
|
||||
/>
|
||||
</ModalFormField>
|
||||
{configSchema && (
|
||||
// Wrap in a form with autocomplete="off" so browsers do not
|
||||
// autofill credential fields (service token, account, etc.).
|
||||
// eslint-disable-next-line jsx-a11y/no-redundant-roles
|
||||
<form
|
||||
role="presentation"
|
||||
autoComplete="off"
|
||||
onSubmit={e => e.preventDefault()}
|
||||
>
|
||||
<JsonForms
|
||||
schema={configSchema}
|
||||
uischema={uiSchema}
|
||||
data={formData}
|
||||
renderers={renderers}
|
||||
cells={cellRegistryEntries}
|
||||
config={{ refreshingSchema, formData }}
|
||||
validationMode={validationMode}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
</form>
|
||||
<JsonForms
|
||||
schema={configSchema}
|
||||
uischema={uiSchema}
|
||||
data={formData}
|
||||
renderers={renderers}
|
||||
cells={cellRegistryEntries}
|
||||
config={{ refreshingSchema, formData }}
|
||||
validationMode={validationMode}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
)}
|
||||
</ModalContent>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
import { useEffect } from 'react';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { Spin, Select, Form } from 'antd';
|
||||
import { Spin } from 'antd';
|
||||
import { withJsonFormsControlProps } from '@jsonforms/react';
|
||||
import type {
|
||||
JsonSchema,
|
||||
@@ -46,18 +46,7 @@ export const SCHEMA_REFRESH_DEBOUNCE_MS = 500;
|
||||
function PasswordControl(props: ControlProps) {
|
||||
const uischema = {
|
||||
...props.uischema,
|
||||
options: {
|
||||
...props.uischema.options,
|
||||
type: 'password',
|
||||
inputProps: {
|
||||
...((props.uischema.options?.inputProps as Record<string, unknown>) ??
|
||||
{}),
|
||||
// Prevent browsers from autofilling stored login passwords into
|
||||
// service-token fields. 'new-password' is respected even when
|
||||
// 'off' is ignored (Chrome ≥ 34).
|
||||
autoComplete: 'new-password',
|
||||
},
|
||||
},
|
||||
options: { ...props.uischema.options, type: 'password' },
|
||||
};
|
||||
return TextControl({ ...props, uischema });
|
||||
}
|
||||
@@ -139,30 +128,17 @@ const readOnlyEntry = {
|
||||
/**
|
||||
* Checks whether all dependency values are filled (non-empty).
|
||||
* Handles nested objects (like auth) by checking they have at least one key.
|
||||
*
|
||||
* Fields that have a `default` in the schema are considered satisfied even
|
||||
* when the user has not explicitly touched them yet — JsonForms does not
|
||||
* write default values into `data` until a field is interacted with, so
|
||||
* without this fallback a field like `admin_host` (which ships with a
|
||||
* sensible default) would permanently block the refresh.
|
||||
*/
|
||||
export function areDependenciesSatisfied(
|
||||
dependencies: string[],
|
||||
data: Record<string, unknown>,
|
||||
schema?: JsonSchema,
|
||||
): boolean {
|
||||
return dependencies.every(dep => {
|
||||
const value = data[dep];
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
if (typeof value === 'object' && Object.keys(value).length === 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
// Fall back to the schema default when the field hasn't been touched yet.
|
||||
const defaultValue = schema?.properties?.[dep]?.default;
|
||||
return (
|
||||
defaultValue !== null && defaultValue !== undefined && defaultValue !== ''
|
||||
);
|
||||
if (value === null || value === undefined || value === '') return false;
|
||||
if (typeof value === 'object' && Object.keys(value).length === 0)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -180,7 +156,6 @@ function DynamicFieldControl(props: ControlProps) {
|
||||
areDependenciesSatisfied(
|
||||
deps as string[],
|
||||
(cfgData as Record<string, unknown>) ?? {},
|
||||
props.rootSchema,
|
||||
);
|
||||
|
||||
if (!refreshing) {
|
||||
@@ -211,66 +186,11 @@ const dynamicFieldEntry = {
|
||||
renderer: DynamicFieldRenderer,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renderer for fields that carry an ``x-enumNames`` array alongside their
|
||||
* ``enum`` values. Renders as an Antd Select showing human-readable labels
|
||||
* (from ``x-enumNames``) while storing the underlying enum values in form
|
||||
* data. Used for MetricFlow's integer-ID fields (account, project,
|
||||
* environment) where the backend provides both IDs and display names.
|
||||
*/
|
||||
function EnumNamesControl(props: ControlProps) {
|
||||
const { refreshingSchema } = props.config ?? {};
|
||||
const schema = props.schema as Record<string, unknown>;
|
||||
const enumValues = (schema.enum as unknown[]) ?? [];
|
||||
const enumNames =
|
||||
(schema['x-enumNames'] as string[]) ?? enumValues.map(String);
|
||||
|
||||
const options = enumValues.map((value, index) => ({
|
||||
value,
|
||||
label: enumNames[index] ?? String(value),
|
||||
}));
|
||||
|
||||
const tooltip = (props.uischema?.options as Record<string, unknown>)
|
||||
?.tooltip as string | undefined;
|
||||
|
||||
return (
|
||||
<Form.Item label={props.label} tooltip={tooltip}>
|
||||
<Select
|
||||
value={props.data ?? null}
|
||||
onChange={value => props.handleChange(props.path, value)}
|
||||
options={options}
|
||||
style={{ width: '100%' }}
|
||||
disabled={!props.enabled}
|
||||
allowClear
|
||||
loading={!!refreshingSchema}
|
||||
placeholder={
|
||||
(props.uischema?.options as Record<string, unknown>)
|
||||
?.placeholderText as string | undefined
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
const EnumNamesRenderer = withJsonFormsControlProps(EnumNamesControl);
|
||||
const enumNamesEntry = {
|
||||
// Rank 5: higher than the default string renderer (2–3) so this fires
|
||||
// whenever x-enumNames is present, regardless of the underlying type.
|
||||
tester: rankWith(
|
||||
5,
|
||||
schemaMatches(s => {
|
||||
const names = (s as Record<string, unknown>)['x-enumNames'];
|
||||
return Array.isArray(names) && (names as unknown[]).length > 0;
|
||||
}),
|
||||
),
|
||||
renderer: EnumNamesRenderer,
|
||||
};
|
||||
|
||||
export const renderers = [
|
||||
...rendererRegistryEntries,
|
||||
passwordEntry,
|
||||
constEntry,
|
||||
readOnlyEntry,
|
||||
enumNamesEntry,
|
||||
dynamicFieldEntry,
|
||||
];
|
||||
|
||||
|
||||
@@ -201,7 +201,20 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lastFetchConfig, setLastFetchConfig] =
|
||||
useState<ListViewFetchDataConfig | null>(null);
|
||||
const [currentSourceFilter, setCurrentSourceFilter] = useState<string>('');
|
||||
const currentSourceFilter = useMemo(() => {
|
||||
const sourceTypeFilter = lastFetchConfig?.filters.find(
|
||||
filter => filter.id === 'source_type',
|
||||
);
|
||||
if (
|
||||
sourceTypeFilter?.value &&
|
||||
typeof sourceTypeFilter.value === 'object' &&
|
||||
'value' in sourceTypeFilter.value
|
||||
) {
|
||||
return sourceTypeFilter.value.value as string;
|
||||
}
|
||||
return (sourceTypeFilter?.value as string | undefined) ?? '';
|
||||
}, [lastFetchConfig]);
|
||||
|
||||
// Track the current type and connection filter values so cascade-clear logic
|
||||
// can inspect them when a different filter changes.
|
||||
const currentTypeFilter = useRef<unknown>(undefined);
|
||||
@@ -361,13 +374,6 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||
const sourceTypeFilter = filterValues.find(f => f.id === 'source_type');
|
||||
const databaseFilter = filterValues.find(f => f.id === 'database');
|
||||
|
||||
// Track source filter for conditional Type filter visibility
|
||||
const sourceVal =
|
||||
sourceTypeFilter?.value && typeof sourceTypeFilter.value === 'object'
|
||||
? (sourceTypeFilter.value as { value: string }).value
|
||||
: ((sourceTypeFilter?.value as string) ?? '');
|
||||
setCurrentSourceFilter(sourceVal);
|
||||
|
||||
const otherFilters = filterValues
|
||||
.filter(f => f.id !== 'source_type' && f.id !== 'database')
|
||||
.filter(
|
||||
@@ -395,13 +401,13 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
const queryParams = rison.encode_uri({
|
||||
order_column: sortBy[0].id,
|
||||
order_direction: sortBy[0].desc ? 'desc' : 'asc',
|
||||
page: pageIndex,
|
||||
page_size: pageSize,
|
||||
...(otherFilters.length ? { filters: otherFilters } : {}),
|
||||
});
|
||||
const queryParams = rison.encode_uri({
|
||||
order_column: sortBy[0].id,
|
||||
order_direction: sortBy[0].desc ? 'desc' : 'asc',
|
||||
page: pageIndex,
|
||||
page_size: pageSize,
|
||||
...(otherFilters.length ? { filters: otherFilters } : {}),
|
||||
});
|
||||
|
||||
// Translate the "Data connection" filter: values prefixed with "sl:" are
|
||||
// semantic layer UUIDs; plain values are database IDs.
|
||||
@@ -498,20 +504,6 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||
state.common?.conf?.PREVENT_UNSAFE_DEFAULT_URLS_ON_DATASET || false,
|
||||
);
|
||||
|
||||
const currentSourceFilter = useMemo(() => {
|
||||
const sourceTypeFilter = lastFetchConfig?.filters.find(
|
||||
filter => filter.id === 'source_type',
|
||||
);
|
||||
if (
|
||||
sourceTypeFilter?.value &&
|
||||
typeof sourceTypeFilter.value === 'object' &&
|
||||
'value' in sourceTypeFilter.value
|
||||
) {
|
||||
return sourceTypeFilter.value.value as string;
|
||||
}
|
||||
return (sourceTypeFilter?.value as string | undefined) ?? '';
|
||||
}, [lastFetchConfig]);
|
||||
|
||||
const openDatasetImportModal = () => {
|
||||
showImportModal(true);
|
||||
};
|
||||
|
||||
@@ -270,26 +270,6 @@ class BaseDatasource(
|
||||
# Check if all requested columns are drillable
|
||||
return set(column_names).issubset(drillable_columns)
|
||||
|
||||
def get_compatible_metrics(
|
||||
self,
|
||||
selected_metrics: list[str],
|
||||
selected_dimensions: list[str],
|
||||
) -> list[str]:
|
||||
"""
|
||||
SQL datasets have no compatibility constraints — return all metrics.
|
||||
"""
|
||||
return [m.metric_name for m in self.metrics]
|
||||
|
||||
def get_compatible_dimensions(
|
||||
self,
|
||||
selected_metrics: list[str],
|
||||
selected_dimensions: list[str],
|
||||
) -> list[str]:
|
||||
"""
|
||||
SQL datasets have no compatibility constraints — return all columns.
|
||||
"""
|
||||
return [c.column_name for c in self.columns]
|
||||
|
||||
def get_time_grains(self) -> list[TimeGrainDict]:
|
||||
"""
|
||||
Get available time granularities from the database.
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -28,9 +27,7 @@ from superset.connectors.sqla.models import BaseDatasource
|
||||
from superset.daos.datasource import DatasourceDAO
|
||||
from superset.daos.exceptions import DatasourceNotFound, DatasourceTypeNotSupportedError
|
||||
from superset.exceptions import SupersetSecurityException
|
||||
from superset.extensions import cache_manager
|
||||
from superset.superset_typing import FlaskResponse
|
||||
from superset.utils import json
|
||||
from superset.utils.core import apply_max_row_limit, DatasourceType, SqlExpressionType
|
||||
from superset.views.base_api import BaseSupersetApi, statsd_metrics
|
||||
|
||||
@@ -310,129 +307,6 @@ class DatasourceRestApi(BaseSupersetApi):
|
||||
f"Valid types are: column, metric, where, having"
|
||||
) from None
|
||||
|
||||
@expose(
|
||||
"/<datasource_type>/<int:datasource_id>/compatible",
|
||||
methods=("POST",),
|
||||
)
|
||||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@event_logger.log_this_with_context(
|
||||
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}"
|
||||
f".compatible",
|
||||
log_to_statsd=False,
|
||||
)
|
||||
def compatible(
|
||||
self, datasource_type: str, datasource_id: int
|
||||
) -> FlaskResponse:
|
||||
"""Return metrics and dimensions compatible with the current selection.
|
||||
---
|
||||
post:
|
||||
summary: Get compatible metrics and dimensions
|
||||
parameters:
|
||||
- in: path
|
||||
schema:
|
||||
type: string
|
||||
name: datasource_type
|
||||
- in: path
|
||||
schema:
|
||||
type: integer
|
||||
name: datasource_id
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
selected_metrics:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
selected_dimensions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Compatible metrics and dimensions
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
result:
|
||||
type: object
|
||||
properties:
|
||||
compatible_metrics:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
compatible_dimensions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
400:
|
||||
$ref: '#/components/responses/400'
|
||||
401:
|
||||
$ref: '#/components/responses/401'
|
||||
403:
|
||||
$ref: '#/components/responses/403'
|
||||
404:
|
||||
$ref: '#/components/responses/404'
|
||||
"""
|
||||
try:
|
||||
datasource = DatasourceDAO.get_datasource(
|
||||
DatasourceType(datasource_type), datasource_id
|
||||
)
|
||||
datasource.raise_for_access()
|
||||
except ValueError:
|
||||
return self.response(
|
||||
400, message=f"Invalid datasource type: {datasource_type}"
|
||||
)
|
||||
except DatasourceTypeNotSupportedError as ex:
|
||||
return self.response(400, message=ex.message)
|
||||
except DatasourceNotFound as ex:
|
||||
return self.response(404, message=ex.message)
|
||||
except SupersetSecurityException as ex:
|
||||
return self.response(403, message=ex.message)
|
||||
|
||||
body = request.get_json(silent=True) or {}
|
||||
selected_metrics = body.get("selected_metrics", [])
|
||||
selected_dimensions = body.get("selected_dimensions", [])
|
||||
|
||||
# Build a stable cache key from the datasource identity and the
|
||||
# (sorted) selection so that order differences don't cause cache misses.
|
||||
cache_key = "compatible:" + hashlib.md5(
|
||||
json.dumps(
|
||||
{
|
||||
"uid": datasource.uid,
|
||||
"m": sorted(selected_metrics),
|
||||
"d": sorted(selected_dimensions),
|
||||
},
|
||||
sort_keys=True,
|
||||
).encode()
|
||||
).hexdigest()
|
||||
|
||||
if (cached := cache_manager.data_cache.get(cache_key)) is not None:
|
||||
return self.response(200, result=cached)
|
||||
|
||||
result = {
|
||||
"compatible_metrics": datasource.get_compatible_metrics(
|
||||
selected_metrics, selected_dimensions
|
||||
),
|
||||
"compatible_dimensions": datasource.get_compatible_dimensions(
|
||||
selected_metrics, selected_dimensions
|
||||
),
|
||||
}
|
||||
|
||||
timeout = datasource.cache_timeout or app.config.get(
|
||||
"CACHE_DEFAULT_TIMEOUT", 300
|
||||
)
|
||||
cache_manager.data_cache.set(cache_key, result, timeout=timeout)
|
||||
|
||||
return self.response(200, result=result)
|
||||
|
||||
@expose("/", methods=("GET",))
|
||||
@protect()
|
||||
@safe
|
||||
|
||||
@@ -507,43 +507,3 @@ class Explorable(Protocol):
|
||||
|
||||
:return: Language identifier string, or None if not applicable
|
||||
"""
|
||||
|
||||
# =========================================================================
|
||||
# Compatibility
|
||||
# =========================================================================
|
||||
|
||||
def get_compatible_metrics(
|
||||
self,
|
||||
selected_metrics: list[str],
|
||||
selected_dimensions: list[str],
|
||||
) -> list[str]:
|
||||
"""
|
||||
Return the names of metrics that can be queried alongside the current
|
||||
selection of metrics and dimensions.
|
||||
|
||||
SQL datasets always return every metric name unchanged — there is no
|
||||
concept of incompatibility at the SQL layer. Semantic views delegate
|
||||
to ``SemanticView.get_compatible_metrics`` so the semantic layer can
|
||||
enforce its own join / grain constraints.
|
||||
|
||||
:param selected_metrics: Metric names already chosen by the user.
|
||||
:param selected_dimensions: Dimension names already chosen by the user.
|
||||
:return: Names of metrics the user is still allowed to add.
|
||||
"""
|
||||
|
||||
def get_compatible_dimensions(
|
||||
self,
|
||||
selected_metrics: list[str],
|
||||
selected_dimensions: list[str],
|
||||
) -> list[str]:
|
||||
"""
|
||||
Return the names of dimensions that can be queried alongside the
|
||||
current selection of metrics and dimensions.
|
||||
|
||||
SQL datasets always return every column name unchanged. Semantic
|
||||
views delegate to ``SemanticView.get_compatible_dimensions``.
|
||||
|
||||
:param selected_metrics: Metric names already chosen by the user.
|
||||
:param selected_dimensions: Dimension names already chosen by the user.
|
||||
:return: Names of dimensions the user is still allowed to add.
|
||||
"""
|
||||
|
||||
@@ -404,49 +404,6 @@ class SemanticView(AuditMixinNullable, Model):
|
||||
def is_rls_supported(self) -> bool:
|
||||
return False
|
||||
|
||||
def raise_for_access(self) -> None:
|
||||
"""No-op: semantic view access control is not yet implemented."""
|
||||
|
||||
@property
|
||||
def query_language(self) -> str | None:
|
||||
return None
|
||||
|
||||
def get_compatible_metrics(
|
||||
self,
|
||||
selected_metrics: list[str],
|
||||
selected_dimensions: list[str],
|
||||
) -> list[str]:
|
||||
"""
|
||||
Return metric names compatible with the current selection.
|
||||
|
||||
Translates string names to semantic-layer objects, delegates to the
|
||||
view implementation, and translates the result back to names.
|
||||
"""
|
||||
metric_map = {m.name: m for m in self.implementation.get_metrics()}
|
||||
dim_map = {d.name: d for d in self.implementation.get_dimensions()}
|
||||
sel_metrics = {metric_map[n] for n in selected_metrics if n in metric_map}
|
||||
sel_dims = {dim_map[n] for n in selected_dimensions if n in dim_map}
|
||||
compatible = self.implementation.get_compatible_metrics(
|
||||
sel_metrics, sel_dims
|
||||
)
|
||||
return [m.name for m in compatible]
|
||||
|
||||
def get_compatible_dimensions(
|
||||
self,
|
||||
selected_metrics: list[str],
|
||||
selected_dimensions: list[str],
|
||||
) -> list[str]:
|
||||
"""
|
||||
Return dimension names compatible with the current selection.
|
||||
|
||||
Translates string names to semantic-layer objects, delegates to the
|
||||
view implementation, and translates the result back to names.
|
||||
"""
|
||||
metric_map = {m.name: m for m in self.implementation.get_metrics()}
|
||||
dim_map = {d.name: d for d in self.implementation.get_dimensions()}
|
||||
sel_metrics = {metric_map[n] for n in selected_metrics if n in metric_map}
|
||||
sel_dims = {dim_map[n] for n in selected_dimensions if n in dim_map}
|
||||
compatible = self.implementation.get_compatible_dimensions(
|
||||
sel_metrics, sel_dims
|
||||
)
|
||||
return [d.name for d in compatible]
|
||||
|
||||
Reference in New Issue
Block a user