This is an automated email from the ASF dual-hosted git repository. beto pushed a commit to branch dbt-metricflow in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/dbt-metricflow by this push: new 83c8c4d7e5 Works 83c8c4d7e5 is described below commit 83c8c4d7e5d9252212e3d0b0ee1e73d8271ce65f Author: Beto Dealmeida <robe...@dealmeida.net> AuthorDate: Thu Jul 17 12:02:48 2025 -0400 Works --- .../src/shared-controls/dndControls.tsx | 57 ++-- .../src/shared-controls/semanticLayerControls.tsx | 99 +++++++ .../components/controls/SemanticLayerControls.tsx | 225 +++++++++++++++ .../controls/SemanticLayerVerification.tsx | 308 +++++++++++++++++++++ superset-frontend/src/setup/setupSemanticLayer.ts | 41 +++ 5 files changed, 707 insertions(+), 23 deletions(-) diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx index 9d199f1edd..03cb0fefbd 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx @@ -85,43 +85,54 @@ function needsSemanticLayerVerification(datasource: Dataset): boolean { /** * Enhance a control with semantic layer verification if available + * This creates a lazy-enhanced control that checks for utilities at runtime */ function enhanceControlWithSemanticLayer( baseControl: any, controlName: string, verificationType: 'metrics' | 'columns', ) { - if (!withAsyncVerification) { - return baseControl; - } - - const verificationFn = - verificationType === 'metrics' - ? createMetricsVerification() - : createColumnsVerification(); - + // Return a control that will be enhanced at runtime if utilities are available return { ...baseControl, - type: withAsyncVerification({ - baseControl: baseControl.type, - verify: verificationFn, - onChange: createSemanticLayerOnChange( - controlName, - SEMANTIC_LAYER_CONTROL_FIELDS, - ), - showLoadingState: true, - }), + // Override the type to use a function that checks for enhancement at runtime + get type() { + if (withAsyncVerification) { + const verificationFn = + verificationType === 'metrics' + ? createMetricsVerification() + : createColumnsVerification(); + + return withAsyncVerification({ + baseControl: baseControl.type, + verify: verificationFn, + onChange: createSemanticLayerOnChange( + controlName, + SEMANTIC_LAYER_CONTROL_FIELDS, + ), + showLoadingState: true, + }); + } + return baseControl.type; + }, mapStateToProps: (state: any, controlState: any) => { // Call the original mapStateToProps if it exists const originalProps = baseControl.mapStateToProps ? baseControl.mapStateToProps(state, controlState) : {}; - return { - ...originalProps, - needAsyncVerification: needsSemanticLayerVerification(state.datasource), - form_data: state.form_data, - }; + // Only add semantic layer props if utilities are available + if (withAsyncVerification) { + const needsVerification = needsSemanticLayerVerification(state.datasource); + + return { + ...originalProps, + needAsyncVerification: needsVerification, + form_data: state.form_data, + }; + } + + return originalProps; }, }; } diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/semanticLayerControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/semanticLayerControls.tsx new file mode 100644 index 0000000000..2ef130c870 --- /dev/null +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/semanticLayerControls.tsx @@ -0,0 +1,99 @@ +/** + * 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 { + dndAdhocMetricsControl, + dndAdhocMetricControl, + dndAdhocMetricControl2, + dndGroupByControl, + dndColumnsControl, +} from './dndControls'; +// Placeholder for semantic layer controls - these would be imported from the main app +const semanticLayerDndAdhocMetricsControl = null; +const semanticLayerDndAdhocMetricControl = null; +const semanticLayerDndAdhocMetricControl2 = null; +const semanticLayerDndGroupByControl = null; +const semanticLayerDndColumnsControl = null; + +/** + * Enhanced shared controls that include semantic layer verification + * when using compatible datasources. + */ +export const enhancedSharedControls = { + // Original controls + dndAdhocMetricsControl, + dndAdhocMetricControl, + dndAdhocMetricControl2, + dndGroupByControl, + dndColumnsControl, + + // Enhanced controls with semantic layer verification + semanticLayerDndAdhocMetricsControl, + semanticLayerDndAdhocMetricControl, + semanticLayerDndAdhocMetricControl2, + semanticLayerDndGroupByControl, + semanticLayerDndColumnsControl, +}; + +/** + * Get the appropriate control based on datasource capabilities + */ +export function getSemanticLayerControl( + controlName: string, + datasource?: any, +): any { + // Check if datasource supports semantic layer verification + const supportsSemanticLayer = + datasource && + 'database' in datasource && + datasource.database?.engine_information?.supports_dynamic_columns; + + if (supportsSemanticLayer) { + switch (controlName) { + case 'dndAdhocMetricsControl': + return semanticLayerDndAdhocMetricsControl; + case 'dndAdhocMetricControl': + return semanticLayerDndAdhocMetricControl; + case 'dndAdhocMetricControl2': + return semanticLayerDndAdhocMetricControl2; + case 'dndGroupByControl': + return semanticLayerDndGroupByControl; + case 'dndColumnsControl': + return semanticLayerDndColumnsControl; + default: + break; + } + } + + // Return original control for non-semantic layer datasources + switch (controlName) { + case 'dndAdhocMetricsControl': + return dndAdhocMetricsControl; + case 'dndAdhocMetricControl': + return dndAdhocMetricControl; + case 'dndAdhocMetricControl2': + return dndAdhocMetricControl2; + case 'dndGroupByControl': + return dndGroupByControl; + case 'dndColumnsControl': + return dndColumnsControl; + default: + return null; + } +} diff --git a/superset-frontend/src/explore/components/controls/SemanticLayerControls.tsx b/superset-frontend/src/explore/components/controls/SemanticLayerControls.tsx new file mode 100644 index 0000000000..15d659486d --- /dev/null +++ b/superset-frontend/src/explore/components/controls/SemanticLayerControls.tsx @@ -0,0 +1,225 @@ +/** + * 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 { + dndAdhocMetricsControl, + dndAdhocMetricControl, + dndAdhocMetricControl2, + dndGroupByControl, + dndColumnsControl, + Dataset, +} from '@superset-ui/chart-controls'; +import withAsyncVerification from './withAsyncVerification'; +import { + createMetricsVerification, + createColumnsVerification, + createSemanticLayerOnChange, + SEMANTIC_LAYER_CONTROL_FIELDS, +} from './SemanticLayerVerification'; + +/** + * Check if a datasource supports semantic layer verification + */ +function needsSemanticLayerVerification(datasource: Dataset): boolean { + if (!datasource || !('database' in datasource) || !datasource.database) { + return false; + } + + const database = datasource.database as any; + return Boolean(database.engine_information?.supports_dynamic_columns); +} + +/** + * Enhanced metrics control with semantic layer verification + */ +export const semanticLayerDndAdhocMetricsControl = { + ...dndAdhocMetricsControl, + type: withAsyncVerification({ + baseControl: 'DndMetricSelect', + verify: createMetricsVerification(), + onChange: createSemanticLayerOnChange( + 'metrics', + SEMANTIC_LAYER_CONTROL_FIELDS, + ), + showLoadingState: true, + }), + mapStateToProps: (state: any, controlState: any) => { + // Call the original mapStateToProps if it exists + const originalProps = dndAdhocMetricsControl.mapStateToProps + ? dndAdhocMetricsControl.mapStateToProps(state, controlState) + : {}; + + return { + ...originalProps, + needAsyncVerification: needsSemanticLayerVerification(state.datasource), + form_data: state.form_data, + }; + }, +}; + +/** + * Enhanced single metric control with semantic layer verification + */ +export const semanticLayerDndAdhocMetricControl = { + ...dndAdhocMetricControl, + type: withAsyncVerification({ + baseControl: 'DndMetricSelect', + verify: createMetricsVerification(), + onChange: createSemanticLayerOnChange( + 'metric', + SEMANTIC_LAYER_CONTROL_FIELDS, + ), + showLoadingState: true, + }), + mapStateToProps: (state: any, controlState: any) => { + // Call the original mapStateToProps if it exists + const originalProps = dndAdhocMetricControl.mapStateToProps + ? dndAdhocMetricControl.mapStateToProps(state, controlState) + : {}; + + return { + ...originalProps, + needAsyncVerification: needsSemanticLayerVerification(state.datasource), + form_data: state.form_data, + }; + }, +}; + +/** + * Enhanced secondary metric control with semantic layer verification + */ +export const semanticLayerDndAdhocMetricControl2 = { + ...dndAdhocMetricControl2, + type: withAsyncVerification({ + baseControl: 'DndMetricSelect', + verify: createMetricsVerification(), + onChange: createSemanticLayerOnChange( + 'metric_2', + SEMANTIC_LAYER_CONTROL_FIELDS, + ), + showLoadingState: true, + }), + mapStateToProps: (state: any, controlState: any) => { + // Call the original mapStateToProps if it exists + const originalProps = dndAdhocMetricControl2.mapStateToProps + ? dndAdhocMetricControl2.mapStateToProps(state, controlState) + : {}; + + return { + ...originalProps, + needAsyncVerification: needsSemanticLayerVerification(state.datasource), + form_data: state.form_data, + }; + }, +}; + +/** + * Enhanced group by control with semantic layer verification + */ +export const semanticLayerDndGroupByControl = { + ...dndGroupByControl, + type: withAsyncVerification({ + baseControl: 'DndColumnSelect', + verify: createColumnsVerification(), + onChange: createSemanticLayerOnChange( + 'groupby', + SEMANTIC_LAYER_CONTROL_FIELDS, + ), + showLoadingState: true, + }), + mapStateToProps: (state: any, controlState: any) => { + // Call the original mapStateToProps if it exists + const originalProps = dndGroupByControl.mapStateToProps + ? dndGroupByControl.mapStateToProps(state, controlState) + : {}; + + return { + ...originalProps, + needAsyncVerification: needsSemanticLayerVerification(state.datasource), + form_data: state.form_data, + }; + }, +}; + +/** + * Enhanced columns control with semantic layer verification + */ +export const semanticLayerDndColumnsControl = { + ...dndColumnsControl, + type: withAsyncVerification({ + baseControl: 'DndColumnSelect', + verify: createColumnsVerification(), + onChange: createSemanticLayerOnChange( + 'columns', + SEMANTIC_LAYER_CONTROL_FIELDS, + ), + showLoadingState: true, + }), + mapStateToProps: (state: any, controlState: any) => { + // Call the original mapStateToProps if it exists + const originalProps = dndColumnsControl.mapStateToProps + ? dndColumnsControl.mapStateToProps(state, controlState) + : {}; + + return { + ...originalProps, + needAsyncVerification: needsSemanticLayerVerification(state.datasource), + form_data: state.form_data, + }; + }, +}; + +/** + * Create override function for semantic layer controls + */ +function createSemanticLayerControlOverride(enhancedControl: any) { + return (originalConfig: any) => + // For semantic layer datasources, use the enhanced control + // For regular datasources, use the original control + ({ + ...originalConfig, + ...enhancedControl, + }); +} + +/** + * Control overrides mapping + */ +export const semanticLayerControlOverrides = { + metrics: createSemanticLayerControlOverride( + semanticLayerDndAdhocMetricsControl, + ), + metric: createSemanticLayerControlOverride( + semanticLayerDndAdhocMetricControl, + ), + metric_2: createSemanticLayerControlOverride( + semanticLayerDndAdhocMetricControl2, + ), + percent_metrics: createSemanticLayerControlOverride( + semanticLayerDndAdhocMetricsControl, + ), + timeseries_limit_metric: createSemanticLayerControlOverride( + semanticLayerDndAdhocMetricControl, + ), + groupby: createSemanticLayerControlOverride(semanticLayerDndGroupByControl), + columns: createSemanticLayerControlOverride(semanticLayerDndColumnsControl), + series_columns: createSemanticLayerControlOverride( + semanticLayerDndColumnsControl, + ), +}; diff --git a/superset-frontend/src/explore/components/controls/SemanticLayerVerification.tsx b/superset-frontend/src/explore/components/controls/SemanticLayerVerification.tsx new file mode 100644 index 0000000000..e618ee8f92 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/SemanticLayerVerification.tsx @@ -0,0 +1,308 @@ +/** + * 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 { SupersetClient, JsonValue } from '@superset-ui/core'; +import { Dataset } from '@superset-ui/chart-controls'; +import { AsyncVerify, ControlPropsWithExtras } from './withAsyncVerification'; + +/** + * Utility to extract current form fields from form data + */ +export function collectQueryFields(formData: any): { + dimensions: string[]; + metrics: string[]; +} { + const dimensions: string[] = []; + const metrics: string[] = []; + + // Extract dimensions from various field types + if (formData.groupby) { + dimensions.push( + ...(Array.isArray(formData.groupby) + ? formData.groupby + : [formData.groupby]), + ); + } + if (formData.columns) { + dimensions.push( + ...(Array.isArray(formData.columns) + ? formData.columns + : [formData.columns]), + ); + } + + // Extract metrics from various field types + if (formData.metrics) { + metrics.push( + ...(Array.isArray(formData.metrics) + ? formData.metrics + : [formData.metrics]), + ); + } + if (formData.metric) { + metrics.push(formData.metric); + } + if (formData.percent_metrics) { + metrics.push( + ...(Array.isArray(formData.percent_metrics) + ? formData.percent_metrics + : [formData.percent_metrics]), + ); + } + + // Filter out null/undefined values and convert objects to strings if needed + const cleanDimensions = dimensions + .filter(dim => dim != null) + .map(dim => + typeof dim === 'string' ? dim : (dim as any)?.column_name || String(dim), + ); + + const cleanMetrics = metrics + .filter(metric => metric != null) + .map(metric => + typeof metric === 'string' + ? metric + : (metric as any)?.metric_name || String(metric), + ); + + return { + dimensions: [...new Set(cleanDimensions)], // Remove duplicates + metrics: [...new Set(cleanMetrics)], // Remove duplicates + }; +} + +/** + * Check if a datasource supports semantic layer verification + */ +function supportsSemanticLayerVerification(datasource: Dataset): boolean { + if (!datasource || !('database' in datasource) || !datasource.database) { + return false; + } + + const database = datasource.database as any; + return Boolean(database.engine_information?.supports_dynamic_columns); +} + +/** + * Call the validation API + */ +async function callValidationAPI( + datasource: Dataset, + selectedDimensions: string[], + selectedMetrics: string[], +): Promise<{ dimensions: string[]; metrics: string[] } | null> { + const databaseId = (datasource.database as any)?.id; + if (!datasource?.id || !databaseId) { + return null; + } + + try { + const response = await SupersetClient.post({ + endpoint: `/api/v1/database/${databaseId}/valid_metrics_and_dimensions/`, + jsonPayload: { + datasource_id: datasource.id, + dimensions: selectedDimensions, + metrics: selectedMetrics, + }, + }); + + return response.json as { dimensions: string[]; metrics: string[] }; + } catch (error) { + console.warn('Failed to fetch valid metrics and dimensions:', error); + return null; + } +} + +/** + * Create verification function for metrics controls + */ +export function createMetricsVerification(): AsyncVerify { + return async (props: ControlPropsWithExtras) => { + const { datasource, form_data, savedMetrics = [], actions } = props; + + // Only verify for semantic layer datasources + if (!supportsSemanticLayerVerification(datasource as Dataset)) { + return null; + } + + // Extract current form fields + const queryFields = collectQueryFields(form_data || {}); + + // Call validation API + const validationResult = await callValidationAPI( + datasource as Dataset, + queryFields.dimensions, + queryFields.metrics, + ); + + if (!validationResult) { + return null; + } + + // Filter saved metrics to only include valid ones + const validMetricNames = new Set(validationResult.metrics); + const filteredSavedMetrics = savedMetrics.filter((metric: any) => + validMetricNames.has(metric.metric_name || metric), + ); + + // Also filter the datasource metrics (for left panel) if this is a Dataset + const dataset = datasource as Dataset; + let filteredDatasourceMetrics = dataset.metrics; + let filteredDatasourceColumns = dataset.columns; + + if (dataset.metrics) { + filteredDatasourceMetrics = dataset.metrics.filter((metric: any) => + validMetricNames.has(metric.metric_name || metric), + ); + } + + // Also filter columns using the same validation result + const validDimensionNames = new Set(validationResult.dimensions); + if (dataset.columns) { + filteredDatasourceColumns = dataset.columns.filter((column: any) => + validDimensionNames.has(column.column_name || column), + ); + } + + // Create filtered datasource for left panel + const filteredDatasource = { + ...dataset, + metrics: filteredDatasourceMetrics, + columns: filteredDatasourceColumns, + }; + + // Update the Redux store's datasource to affect the left panel + if (actions && typeof actions.syncDatasourceMetadata === 'function') { + actions.syncDatasourceMetadata(filteredDatasource); + } + + return { + savedMetrics: filteredSavedMetrics, + datasource: filteredDatasource, + }; + }; +} + +/** + * Create verification function for dimensions controls + */ +export function createColumnsVerification(): AsyncVerify { + return async (props: ControlPropsWithExtras) => { + const { datasource, form_data, options = [], actions } = props; + + // Only verify for semantic layer datasources + if (!supportsSemanticLayerVerification(datasource as Dataset)) { + return null; + } + + // Extract current form fields + const queryFields = collectQueryFields(form_data || {}); + + // Call validation API + const validationResult = await callValidationAPI( + datasource as Dataset, + queryFields.dimensions, + queryFields.metrics, + ); + + if (!validationResult) { + return null; + } + + // Filter dimension options to only include valid ones + const validDimensionNames = new Set(validationResult.dimensions); + const filteredOptions = options.filter((option: any) => + validDimensionNames.has(option.column_name || option), + ); + + // Also filter the datasource columns (for left panel) if this is a Dataset + const dataset = datasource as Dataset; + let filteredDatasourceColumns = dataset.columns; + let filteredDatasourceMetrics = dataset.metrics; + + if (dataset.columns) { + filteredDatasourceColumns = dataset.columns.filter((column: any) => + validDimensionNames.has(column.column_name || column), + ); + } + + // Also filter metrics using the same validation result + const validMetricNames = new Set(validationResult.metrics); + if (dataset.metrics) { + filteredDatasourceMetrics = dataset.metrics.filter((metric: any) => + validMetricNames.has(metric.metric_name || metric), + ); + } + + // Create filtered datasource for left panel + const filteredDatasource = { + ...dataset, + columns: filteredDatasourceColumns, + metrics: filteredDatasourceMetrics, + }; + + // Update the Redux store's datasource to affect the left panel + if (actions && typeof actions.syncDatasourceMetadata === 'function') { + actions.syncDatasourceMetadata(filteredDatasource); + } + + return { + options: filteredOptions, + datasource: filteredDatasource, + }; + }; +} + +/** + * Create onChange handler that triggers re-rendering of other controls when values change + */ +export function createSemanticLayerOnChange( + controlName: string, + affectedControls: string[], +) { + return (value: JsonValue, props: ControlPropsWithExtras) => { + const { actions, form_data } = props; + + // Trigger re-rendering of affected controls by updating their values + // This forces the verification to run again + affectedControls.forEach(controlField => { + if ( + controlField !== controlName && + form_data && + form_data[controlField] + ) { + actions.setControlValue(controlField, form_data[controlField], []); + } + }); + }; +} + +/** + * Get list of control fields that should trigger re-rendering + */ +export const SEMANTIC_LAYER_CONTROL_FIELDS = [ + 'metrics', + 'metric', + 'metric_2', + 'percent_metrics', + 'timeseries_limit_metric', + 'groupby', + 'columns', + 'series_columns', +]; diff --git a/superset-frontend/src/setup/setupSemanticLayer.ts b/superset-frontend/src/setup/setupSemanticLayer.ts new file mode 100644 index 0000000000..b7816890a5 --- /dev/null +++ b/superset-frontend/src/setup/setupSemanticLayer.ts @@ -0,0 +1,41 @@ +/** + * 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 { setSemanticLayerUtilities } from '@superset-ui/chart-controls'; +import withAsyncVerification from 'src/explore/components/controls/withAsyncVerification'; +import { + createMetricsVerification, + createColumnsVerification, + createSemanticLayerOnChange, + SEMANTIC_LAYER_CONTROL_FIELDS, +} from 'src/explore/components/controls/SemanticLayerVerification'; + +/** + * Initialize semantic layer controls by setting up the utilities + * in the chart controls package. + */ +export default function setupSemanticLayer() { + setSemanticLayerUtilities({ + withAsyncVerification, + createMetricsVerification, + createColumnsVerification, + createSemanticLayerOnChange, + SEMANTIC_LAYER_CONTROL_FIELDS, + }); +}