You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by be...@apache.org on 2025/07/18 18:54:05 UTC

(superset) 06/08: Checkpoint

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

commit e3f1ff71af119fc144eb52ddcbd946bab6ebcd62
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Fri Jul 18 14:31:01 2025 -0400

    Checkpoint
---
 .../src/shared-controls/dndControls.tsx            |   4 +-
 .../src/components/Datasource/DatasourceEditor.jsx |   3 +-
 .../src/components/Datasource/utils.js             |   1 +
 .../DndColumnSelectControl/ColumnSelectPopover.tsx |  12 +-
 .../controls/SemanticLayerVerification.tsx         | 322 ++++++++++++---------
 5 files changed, 188 insertions(+), 154 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 56566863f6..877998261e 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
@@ -175,8 +175,8 @@ function enhanceControlWithSemanticLayer(
         return {
           ...originalProps,
           needAsyncVerification: needsVerification,
-          // Only skip effect verification if there's no existing data
-          skipEffectVerification: !hasExistingData,
+          // Temporarily disable useEffect verification to prevent duplicates
+          skipEffectVerification: true,
           form_data: state.form_data,
           datasource: state.datasource, // Pass datasource to verification function
         };
diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
index 4a353b8629..b233d8c768 100644
--- a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
+++ b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
@@ -1737,8 +1737,7 @@ class DatasourceEditor extends PureComponent {
               ),
               children: (
                 <div>
-                  {this.state.datasource.database?.engine_information
-                    ?.supports_dynamic_columns && (
+                  {this.state.datasource.database?.backend === 'metricflow' && (
                     <ColumnButtonWrapper>
                       <StyledButtonWrapper>
                         <Button
diff --git a/superset-frontend/src/components/Datasource/utils.js b/superset-frontend/src/components/Datasource/utils.js
index b07571db68..bc67fbc50b 100644
--- a/superset-frontend/src/components/Datasource/utils.js
+++ b/superset-frontend/src/components/Datasource/utils.js
@@ -136,6 +136,7 @@ export function updateMetrics(prevMetrics, newMetrics, addSuccessToast) {
   // metrics: Array<{metric_name: string; expression: string; verbose_name?: string; ...}>
   const sourceMetricNames = newMetrics.map(metric => metric.metric_name);
   const currentMetrics = prevMetrics.reduce((agg, metric) => {
+    // eslint-disable-next-line no-param-reassign
     agg[metric.metric_name] = metric;
     return agg;
   }, {});
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx
index 00aa5fe880..2b0717189d 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx
@@ -45,7 +45,6 @@ import {
   SQLEditor,
   EmptyState,
   Tooltip,
-  Icons,
 } from '@superset-ui/core/components';
 
 import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
@@ -168,8 +167,6 @@ const ColumnSelectPopover = ({
   >(initialSimpleColumn);
   const [selectedTab, setSelectedTab] = useState<string | null>(null);
   const [validDimensions, setValidDimensions] = useState<string[] | null>(null);
-  const [isLoadingValidDimensions, setIsLoadingValidDimensions] =
-    useState(false);
   const previousFormDataRef = useRef<string>('');
 
   const [resizeButton, width, height] = useResizeButton(
@@ -275,7 +272,6 @@ const ColumnSelectPopover = ({
       (selectedTab === TABS_KEYS.SIMPLE || selectedTab === null)
     ) {
       const fetchValidDimensions = async () => {
-        setIsLoadingValidDimensions(true);
 
         try {
           // Use the same 50ms delay that fixed the main verification timing issue
@@ -336,7 +332,7 @@ const ColumnSelectPopover = ({
           console.warn('Failed to fetch valid dimensions:', error);
           setValidDimensions(null);
         } finally {
-          setIsLoadingValidDimensions(false);
+          // Cleanup
         }
       };
 
@@ -348,7 +344,7 @@ const ColumnSelectPopover = ({
       return () => clearTimeout(timeoutId);
     }
     setValidDimensions(null);
-    setIsLoadingValidDimensions(false);
+    return undefined;
   }, [isSemanticLayer, selectedTab, datasource, store]);
 
   // Also trigger when form data changes (for subsequent updates)
@@ -361,8 +357,6 @@ const ColumnSelectPopover = ({
         previousFormDataRef.current = currentFormDataString;
 
         const fetchValidDimensions = async () => {
-          setIsLoadingValidDimensions(true);
-
           try {
             await new Promise(resolve => setTimeout(resolve, 50));
 
@@ -381,7 +375,7 @@ const ColumnSelectPopover = ({
           } catch (error) {
             console.warn('Failed to fetch valid dimensions:', error);
           } finally {
-            setIsLoadingValidDimensions(false);
+            // Cleanup
           }
         };
 
diff --git a/superset-frontend/src/explore/components/controls/SemanticLayerVerification.tsx b/superset-frontend/src/explore/components/controls/SemanticLayerVerification.tsx
index e2b9d39363..037110217b 100644
--- a/superset-frontend/src/explore/components/controls/SemanticLayerVerification.tsx
+++ b/superset-frontend/src/explore/components/controls/SemanticLayerVerification.tsx
@@ -154,6 +154,82 @@ const apiCallCache = new Map<
   Promise<{ dimensions: string[]; metrics: string[] } | null>
 >();
 
+// Request debouncing - keyed by datasource + control combination
+const pendingRequests = new Map<string, Promise<any>>();
+const lastRequestTime = new Map<string, number>();
+
+/**
+ * Create verification result from API response
+ */
+function createVerificationResult(
+  validationResult: { dimensions: string[]; metrics: string[] },
+  savedMetrics: any[],
+  props: ControlPropsWithExtras,
+  controlName?: string,
+) {
+  const { datasource, actions } = props;
+
+  // 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),
+  );
+
+  // Mark datasource metrics and columns as disabled if invalid (for left panel)
+  const dataset = datasource as Dataset;
+  let updatedDatasourceMetrics = dataset.metrics;
+  let updatedDatasourceColumns = dataset.columns;
+
+  // Filter valid names to only include those that exist in the original datasource
+  const originalDimensionNames = new Set(
+    dataset.columns?.map((col: any) => col.column_name) || [],
+  );
+  const originalMetricNames = new Set(
+    dataset.metrics?.map((metric: any) => metric.metric_name) || [],
+  );
+
+  const filteredValidMetricNames = new Set(
+    validationResult.metrics.filter(metric => originalMetricNames.has(metric)),
+  );
+  const filteredValidDimensionNames = new Set(
+    validationResult.dimensions.filter(dim => originalDimensionNames.has(dim)),
+  );
+
+  if (dataset.metrics) {
+    updatedDatasourceMetrics = dataset.metrics.map((metric: any) => ({
+      ...metric,
+      isDisabled: !filteredValidMetricNames.has(metric.metric_name || metric),
+    }));
+  }
+
+  // Also update columns using the same validation result
+  if (dataset.columns) {
+    updatedDatasourceColumns = dataset.columns.map((column: any) => ({
+      ...column,
+      isDisabled: !filteredValidDimensionNames.has(
+        column.column_name || column,
+      ),
+    }));
+  }
+
+  // Create updated datasource for left panel
+  const updatedDatasource = {
+    ...dataset,
+    metrics: updatedDatasourceMetrics,
+    columns: updatedDatasourceColumns,
+  };
+
+  // Update the Redux store's datasource to affect the left panel
+  if (actions && typeof actions.syncDatasourceMetadata === 'function') {
+    actions.syncDatasourceMetadata(updatedDatasource);
+  }
+
+  return {
+    savedMetrics: filteredSavedMetrics,
+    datasource: updatedDatasource,
+  };
+}
+
 /**
  * Call the validation API
  */
@@ -161,6 +237,7 @@ export async function callValidationAPI(
   datasource: Dataset,
   selectedDimensions: string[],
   selectedMetrics: string[],
+  controlName?: string,
 ): Promise<{ dimensions: string[]; metrics: string[] } | null> {
   const databaseId = (datasource.database as any)?.id;
   if (!datasource?.id || !databaseId) {
@@ -174,12 +251,44 @@ export async function callValidationAPI(
     metrics: selectedMetrics.sort(),
   });
 
+  // Create a key for this specific control to prevent duplicate requests
+  const controlKey = `${datasource.id}_${controlName || 'unknown'}`;
+  const now = Date.now();
+  
   // Check if we already have a pending request for the same parameters
   if (apiCallCache.has(cacheKey)) {
+    console.log(`[API] Reusing cached request for control: ${controlName}`);
     return apiCallCache.get(cacheKey)!;
   }
 
+  // Check if we have a pending request for this specific control
+  if (pendingRequests.has(controlKey)) {
+    console.log(`[API] Request already pending for control: ${controlName}, waiting...`);
+    return pendingRequests.get(controlKey)!;
+  }
+
+  // Time-based deduplication: if we just made a request for this control, wait a bit
+  const lastTime = lastRequestTime.get(controlKey) || 0;
+  if (now - lastTime < 100) { // 100ms debounce
+    console.log(`[API] Request too soon for control: ${controlName}, debouncing...`);
+    return new Promise(resolve => {
+      setTimeout(async () => {
+        // Try again after debounce
+        const result = await callValidationAPI(datasource, selectedDimensions, selectedMetrics, controlName);
+        resolve(result);
+      }, 100);
+    });
+  }
+
+  lastRequestTime.set(controlKey, now);
+
   try {
+    console.log(`[API] Making request for control: ${controlName}`, {
+      datasource_id: datasource.id,
+      dimensions: selectedDimensions,
+      metrics: selectedMetrics,
+    });
+
     const apiPromise = SupersetClient.post({
       endpoint: `/api/v1/database/${databaseId}/valid_metrics_and_dimensions/`,
       jsonPayload: {
@@ -191,19 +300,25 @@ export async function callValidationAPI(
       response => response.json as { dimensions: string[]; metrics: string[] },
     );
 
-    // Cache the promise
+    // Cache the promise for the exact same parameters
     apiCallCache.set(cacheKey, apiPromise);
+    
+    // Also track this request for this specific control
+    pendingRequests.set(controlKey, apiPromise);
 
-    // Remove from cache after a short delay to allow for immediate duplicates
-    // Increased timeout to handle rapid successive calls from multiple controls
-    setTimeout(() => {
-      apiCallCache.delete(cacheKey);
-    }, 500);
+    // Clean up on completion
+    const result = await apiPromise;
+    apiCallCache.delete(cacheKey);
+    pendingRequests.delete(controlKey);
+    console.log(`[API] Request completed for control: ${controlName}`, result);
 
-    return await apiPromise;
+    return result;
   } catch (error) {
-    console.warn('Failed to fetch valid metrics and dimensions:', error);
+    // Clean up on error
     apiCallCache.delete(cacheKey);
+    pendingRequests.delete(controlKey);
+
+    console.warn('Failed to fetch valid metrics and dimensions:', error);
     return null;
   }
 }
@@ -213,20 +328,30 @@ export async function callValidationAPI(
  */
 export function createMetricsVerification(controlName?: string): AsyncVerify {
   return async (props: ControlPropsWithExtras) => {
-    const { datasource, form_data, savedMetrics = [], actions, value } = props;
+    const { datasource, form_data, savedMetrics = [], value } = props;
 
     // Only verify for semantic layer datasources
     if (!supportsSemanticLayerVerification(datasource as Dataset)) {
       return null;
     }
 
-    // Defer the actual verification to allow React/Redux to complete all updates
-    // This should solve the stale state issue by waiting for the event loop to complete
+    console.log(`[MetricsVerification] Triggered for control: ${controlName}`, {
+      datasource: datasource?.id,
+      form_data,
+      value,
+      savedMetrics: savedMetrics.length,
+      stackTrace: new Error().stack?.split('\n').slice(1, 4).join('\n'),
+    });
+
+    // Use requestAnimationFrame to ensure React has completed its updates
     return new Promise(resolve => {
-      setTimeout(async () => {
-        // Try to get fresh state from Redux store
-        const store =
-          (window as any).__REDUX_STORE__ || (actions as any)?._store;
+      requestAnimationFrame(async () => {
+        // Add a small delay to ensure all Redux updates have completed
+        await new Promise(r => setTimeout(r, 50));
+        
+        // Try to get the most current form data from Redux store
+        const { actions } = props;
+        const store = (window as any).__REDUX_STORE__ || (actions as any)?._store; // eslint-disable-line no-underscore-dangle
         let currentFormData = form_data;
 
         if (store) {
@@ -239,7 +364,7 @@ export function createMetricsVerification(controlName?: string): AsyncVerify {
           }
         }
 
-        // Create form data with the current value
+        // Create form data with the current value for this control
         const syntheticFormData = { ...currentFormData };
         if (controlName) {
           syntheticFormData[controlName] = value;
@@ -248,10 +373,16 @@ export function createMetricsVerification(controlName?: string): AsyncVerify {
         // Extract query fields using the complete form data approach
         const queryFields = collectQueryFields(syntheticFormData);
 
+        console.log(`[MetricsVerification] Query fields:`, queryFields);
+        console.log(`[MetricsVerification] Original form data:`, form_data);
+        console.log(`[MetricsVerification] Current form data:`, currentFormData);
+        console.log(`[MetricsVerification] Synthetic form data:`, syntheticFormData);
+
         const validationResult = await callValidationAPI(
           datasource as Dataset,
           queryFields.dimensions,
           queryFields.metrics,
+          controlName,
         );
 
         if (!validationResult) {
@@ -259,72 +390,14 @@ export function createMetricsVerification(controlName?: string): AsyncVerify {
           return;
         }
 
-        // 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),
-        );
-
-        // Mark datasource metrics and columns as disabled if invalid (for left panel)
-        const dataset = datasource as Dataset;
-        let updatedDatasourceMetrics = dataset.metrics;
-        let updatedDatasourceColumns = dataset.columns;
-
-        // Filter valid names to only include those that exist in the original datasource
-        const originalDimensionNames = new Set(
-          dataset.columns?.map((col: any) => col.column_name) || [],
-        );
-        const originalMetricNames = new Set(
-          dataset.metrics?.map((metric: any) => metric.metric_name) || [],
+        const result = createVerificationResult(
+          validationResult,
+          savedMetrics,
+          props,
+          controlName,
         );
-
-        const filteredValidMetricNames = new Set(
-          validationResult.metrics.filter(metric =>
-            originalMetricNames.has(metric),
-          ),
-        );
-        const filteredValidDimensionNames = new Set(
-          validationResult.dimensions.filter(dim =>
-            originalDimensionNames.has(dim),
-          ),
-        );
-
-        if (dataset.metrics) {
-          updatedDatasourceMetrics = dataset.metrics.map((metric: any) => ({
-            ...metric,
-            isDisabled: !filteredValidMetricNames.has(
-              metric.metric_name || metric,
-            ),
-          }));
-        }
-
-        // Also update columns using the same validation result
-        if (dataset.columns) {
-          updatedDatasourceColumns = dataset.columns.map((column: any) => ({
-            ...column,
-            isDisabled: !filteredValidDimensionNames.has(
-              column.column_name || column,
-            ),
-          }));
-        }
-
-        // Create updated datasource for left panel
-        const updatedDatasource = {
-          ...dataset,
-          metrics: updatedDatasourceMetrics,
-          columns: updatedDatasourceColumns,
-        };
-
-        // Update the Redux store's datasource to affect the left panel
-        if (actions && typeof actions.syncDatasourceMetadata === 'function') {
-          actions.syncDatasourceMetadata(updatedDatasource);
-        }
-
-        resolve({
-          savedMetrics: filteredSavedMetrics,
-          datasource: updatedDatasource,
-        });
-      }, 50); // 50ms delay to let React/Redux complete updates
+        resolve(result);
+      });
     }) as Promise<any>;
   };
 }
@@ -341,13 +414,22 @@ export function createColumnsVerification(controlName?: string): AsyncVerify {
       return null;
     }
 
-    // Defer the actual verification to allow React/Redux to complete all updates
-    // This should solve the stale state issue by waiting for the event loop to complete
+    console.log(`[ColumnsVerification] Triggered for control: ${controlName}`, {
+      datasource: datasource?.id,
+      form_data,
+      value,
+      options: options.length,
+      stackTrace: new Error().stack?.split('\n').slice(1, 4).join('\n'),
+    });
+
+    // Use requestAnimationFrame to ensure React has completed its updates
     return new Promise(resolve => {
-      setTimeout(async () => {
+      requestAnimationFrame(async () => {
+        // Add a small delay to ensure all Redux updates have completed
+        await new Promise(r => setTimeout(r, 50));
+        
         // Try to get fresh state from Redux store
-        const store =
-          (window as any).__REDUX_STORE__ || (actions as any)?._store;
+        const store = (window as any).__REDUX_STORE__ || (actions as any)?._store; // eslint-disable-line no-underscore-dangle
         let currentFormData = form_data;
 
         if (store) {
@@ -369,10 +451,15 @@ export function createColumnsVerification(controlName?: string): AsyncVerify {
         // Extract query fields using the complete form data approach
         const queryFields = collectQueryFields(syntheticFormData);
 
+        console.log(`[ColumnsVerification] Query fields:`, queryFields);
+        console.log(`[ColumnsVerification] Current form data:`, currentFormData);
+        console.log(`[ColumnsVerification] Synthetic form data:`, syntheticFormData);
+
         const validationResult = await callValidationAPI(
           datasource as Dataset,
           queryFields.dimensions,
           queryFields.metrics,
+          controlName,
         );
 
         if (!validationResult) {
@@ -387,66 +474,19 @@ export function createColumnsVerification(controlName?: string): AsyncVerify {
           isDisabled: !validDimensionNames.has(option.column_name || option),
         }));
 
-        // Mark datasource columns and metrics as disabled if invalid (for left panel)
-        const dataset = datasource as Dataset;
-        let updatedDatasourceColumns = dataset.columns;
-        let updatedDatasourceMetrics = dataset.metrics;
-
-        // Filter valid names to only include those that exist in the original datasource
-        const originalDimensionNames = new Set(
-          dataset.columns?.map((col: any) => col.column_name) || [],
-        );
-        const originalMetricNames = new Set(
-          dataset.metrics?.map((metric: any) => metric.metric_name) || [],
-        );
-
-        const filteredValidDimensionNames = new Set(
-          validationResult.dimensions.filter(dim =>
-            originalDimensionNames.has(dim),
-          ),
+        // Use createVerificationResult helper for consistent processing
+        const verificationResult = createVerificationResult(
+          validationResult,
+          [], // savedMetrics not used for columns verification
+          props,
+          controlName,
         );
-        const filteredValidMetricNames = new Set(
-          validationResult.metrics.filter(metric =>
-            originalMetricNames.has(metric),
-          ),
-        );
-
-        // Update the disabled state logic to use filtered valid names
-        if (dataset.columns) {
-          updatedDatasourceColumns = dataset.columns.map((column: any) => ({
-            ...column,
-            isDisabled: !filteredValidDimensionNames.has(
-              column.column_name || column,
-            ),
-          }));
-        }
-
-        if (dataset.metrics) {
-          updatedDatasourceMetrics = dataset.metrics.map((metric: any) => ({
-            ...metric,
-            isDisabled: !filteredValidMetricNames.has(
-              metric.metric_name || metric,
-            ),
-          }));
-        }
-
-        // Create updated datasource for left panel
-        const updatedDatasource = {
-          ...dataset,
-          columns: updatedDatasourceColumns,
-          metrics: updatedDatasourceMetrics,
-        };
-
-        // Update the Redux store's datasource to affect the left panel
-        if (actions && typeof actions.syncDatasourceMetadata === 'function') {
-          actions.syncDatasourceMetadata(updatedDatasource);
-        }
 
         resolve({
           options: updatedOptions,
-          datasource: updatedDatasource,
+          datasource: verificationResult.datasource,
         });
-      }, 50); // 50ms delay to let React/Redux complete updates
+      });
     }) as Promise<any>;
   };
 }