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 a2c885659231ac6112964b82805363591e177883 Author: Beto Dealmeida <robe...@dealmeida.net> AuthorDate: Thu Jul 17 22:20:19 2025 -0400 Checkpoint --- .../src/shared-controls/dndControls.tsx | 18 +- .../plugin-chart-table/src/controlPanel.tsx | 24 ++- .../DndColumnSelectControl/ColumnSelectPopover.tsx | 227 ++++++++++++--------- .../MetricControl/AdhocMetricEditPopover/index.jsx | 114 ++++++++--- .../controls/SemanticLayerVerification.tsx | 105 ++++++---- .../components/controls/withAsyncVerification.tsx | 20 +- 6 files changed, 323 insertions(+), 185 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 1056a5414f..25140fdbc4 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 @@ -75,7 +75,7 @@ export function setSemanticLayerUtilities(utilities: { createSemanticLayerOnChange, SEMANTIC_LAYER_CONTROL_FIELDS, } = utilities); - + // Notify all enhanced controls that utilities are now available enhancedControls.forEach(control => { control.invalidateCache(); @@ -106,16 +106,16 @@ function enhanceControlWithSemanticLayer( // Cache for the enhanced control type let cachedEnhancedType: any = null; let utilitiesWereAvailable = false; - + // Register with notification system enhancedControls.push({ controlName, invalidateCache: () => { cachedEnhancedType = null; utilitiesWereAvailable = false; - } + }, }); - + // Return a control that will be enhanced at runtime if utilities are available return { ...baseControl, @@ -123,7 +123,7 @@ function enhanceControlWithSemanticLayer( get type() { // Check if utilities became available since last call const utilitiesAvailableNow = !!withAsyncVerification; - + if (utilitiesAvailableNow) { // If utilities just became available or we haven't cached yet, create enhanced control if (!utilitiesWereAvailable || !cachedEnhancedType) { @@ -131,7 +131,7 @@ function enhanceControlWithSemanticLayer( verificationType === 'metrics' ? createMetricsVerification(controlName) : createColumnsVerification(controlName); - + cachedEnhancedType = withAsyncVerification({ baseControl: baseControl.type, verify: verificationFn, @@ -141,13 +141,13 @@ function enhanceControlWithSemanticLayer( ), showLoadingState: true, }); - + utilitiesWereAvailable = true; } - + return cachedEnhancedType; } - + utilitiesWereAvailable = false; return baseControl.type; }, diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx index 86bdcc6f3e..bc0ce2afcb 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx @@ -311,23 +311,29 @@ const config: ControlPanelConfig = { sharedControls?.metrics?.mapStateToProps; const newState = originalMapStateToProps?.(state, controlState) ?? {}; - + // Add table-specific props while preserving semantic layer enhancements - newState.columns = datasource?.columns[0]?.hasOwnProperty('filterable') + newState.columns = datasource?.columns[0]?.hasOwnProperty( + 'filterable', + ) ? (datasource as Dataset)?.columns?.filter( (c: ColumnMeta) => c.filterable, ) : datasource?.columns; newState.savedMetrics = defineSavedMetrics(datasource); - newState.selectedMetrics = form_data.metrics || + newState.selectedMetrics = + form_data.metrics || (form_data.metric ? [form_data.metric] : []); newState.datasource = datasource; - newState.externalValidationErrors = validateAggControlValues(controls, [ - controls.groupby?.value, - controls.percent_metrics?.value, - controlState.value, - ]); - + newState.externalValidationErrors = validateAggControlValues( + controls, + [ + controls.groupby?.value, + controls.percent_metrics?.value, + controlState.value, + ], + ); + return newState; }, rerender: ['groupby', 'percent_metrics'], diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx index 952b0490f3..6e0bed1b2e 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx @@ -51,9 +51,9 @@ import { import sqlKeywords from 'src/SqlLab/utils/sqlKeywords'; import { getColumnKeywords } from 'src/explore/controlUtils/getColumnKeywords'; import { StyledColumnOption } from 'src/explore/components/optionRenderers'; -import { +import { collectQueryFields, - callValidationAPI + callValidationAPI, } from 'src/explore/components/controls/SemanticLayerVerification'; import { POPOVER_INITIAL_HEIGHT, @@ -132,15 +132,17 @@ const ColumnSelectPopover = ({ state => state.explore.form_data, ); const store = useStore(); - + // Check if this is a semantic layer dataset const isSemanticLayer = useMemo(() => { if (!datasource || !('database' in datasource) || !datasource.database) { return false; } - return Boolean(datasource.database.engine_information?.supports_dynamic_columns); + return Boolean( + datasource.database.engine_information?.supports_dynamic_columns, + ); }, [datasource]); - + // For semantic layers, disable Saved and Custom SQL tabs const effectiveDisabledTabs = useMemo(() => { const tabs = new Set(disabledTabs); @@ -150,7 +152,7 @@ const ColumnSelectPopover = ({ } return tabs; }, [disabledTabs, isSemanticLayer]); - + const [initialLabel] = useState(label); const [initialAdhocColumn, initialCalculatedColumn, initialSimpleColumn] = getInitialColumnValues(editedColumn); @@ -166,7 +168,8 @@ const ColumnSelectPopover = ({ >(initialSimpleColumn); const [selectedTab, setSelectedTab] = useState<string | null>(null); const [validDimensions, setValidDimensions] = useState<string[] | null>(null); - const [isLoadingValidDimensions, setIsLoadingValidDimensions] = useState(false); + const [isLoadingValidDimensions, setIsLoadingValidDimensions] = + useState(false); const previousFormDataRef = useRef<string>(''); const [resizeButton, width, height] = useResizeButton( @@ -176,34 +179,31 @@ const ColumnSelectPopover = ({ const sqlEditorRef = useRef(null); - const [calculatedColumns, simpleColumns] = useMemo( - () => { - // Use columns from Redux datasource state (which includes disabled states) instead of props - const columnsToUse = datasource?.columns || columns || []; - - const [calculated, simple] = columnsToUse.reduce( - (acc: [ColumnMeta[], ColumnMeta[]], column: ColumnMeta) => { - if (column.expression) { - acc[0].push(column); - } else { - acc[1].push(column); - } - return acc; - }, - [[], []], - ) || [[], []]; - - // For semantic layer datasets, filter simple columns to show only valid dimensions - // Use the isDisabled state set by the main verification system instead of separate API calls - if (isSemanticLayer) { - const filteredSimple = simple.filter(column => !column.isDisabled); - return [calculated, filteredSimple]; - } + const [calculatedColumns, simpleColumns] = useMemo(() => { + // Use columns from Redux datasource state (which includes disabled states) instead of props + const columnsToUse = datasource?.columns || columns || []; - return [calculated, simple]; - }, - [datasource?.columns, columns, isSemanticLayer], - ); + const [calculated, simple] = columnsToUse.reduce( + (acc: [ColumnMeta[], ColumnMeta[]], column: ColumnMeta) => { + if (column.expression) { + acc[0].push(column); + } else { + acc[1].push(column); + } + return acc; + }, + [[], []], + ) || [[], []]; + + // For semantic layer datasets, filter simple columns to show only valid dimensions + // Use the isDisabled state set by the main verification system instead of separate API calls + if (isSemanticLayer) { + const filteredSimple = simple.filter(column => !column.isDisabled); + return [calculated, filteredSimple]; + } + + return [calculated, simple]; + }, [datasource?.columns, columns, isSemanticLayer]); const onSqlExpressionChange = useCallback( sqlExpression => { @@ -249,7 +249,7 @@ const ColumnSelectPopover = ({ if (isSemanticLayer) { return TABS_KEYS.SIMPLE; } - + // Original logic for non-semantic layer datasets return initialAdhocColumn ? TABS_KEYS.SQL_EXPRESSION @@ -272,42 +272,57 @@ const ColumnSelectPopover = ({ console.log('datasource exists:', !!datasource); console.log('selectedTab:', selectedTab); console.log('TABS_KEYS.SIMPLE:', TABS_KEYS.SIMPLE); - console.log('Should trigger API?', isSemanticLayer && formData && datasource && - (selectedTab === TABS_KEYS.SIMPLE || selectedTab === null)); - + console.log( + 'Should trigger API?', + isSemanticLayer && + formData && + datasource && + (selectedTab === TABS_KEYS.SIMPLE || selectedTab === null), + ); + // Disable column modal API calls - semantic layer verification handles disabled states automatically - if (false && isSemanticLayer && formData && datasource && - (selectedTab === TABS_KEYS.SIMPLE || selectedTab === null)) { - + if ( + false && + isSemanticLayer && + formData && + datasource && + (selectedTab === TABS_KEYS.SIMPLE || selectedTab === null) + ) { const fetchValidDimensions = async () => { setIsLoadingValidDimensions(true); - + try { // Use the same 50ms delay that fixed the main verification timing issue await new Promise(resolve => setTimeout(resolve, 50)); - + // Get the most current form data from store const currentState = store.getState() as ExplorePageState; let currentFormData = currentState.explore.form_data; - + // If we're in a table and don't have metrics/dimensions, try to get from controls state - if ((!currentFormData.metrics && !currentFormData.groupby && !currentFormData.all_columns) || - (Array.isArray(currentFormData.metrics) && currentFormData.metrics.length === 0 && - Array.isArray(currentFormData.groupby) && currentFormData.groupby.length === 0)) { - + if ( + (!currentFormData.metrics && + !currentFormData.groupby && + !currentFormData.all_columns) || + (Array.isArray(currentFormData.metrics) && + currentFormData.metrics.length === 0 && + Array.isArray(currentFormData.groupby) && + currentFormData.groupby.length === 0) + ) { // Try to get from the controls state instead const controlsState = (currentState as any).explore?.controls; if (controlsState) { const enhancedFormData = { ...currentFormData }; - + // Get metrics from controls if (controlsState.metrics?.value) { enhancedFormData.metrics = controlsState.metrics.value; } if (controlsState.percent_metrics?.value) { - enhancedFormData.percent_metrics = controlsState.percent_metrics.value; + enhancedFormData.percent_metrics = + controlsState.percent_metrics.value; } - + // Get dimensions from controls if (controlsState.groupby?.value) { enhancedFormData.groupby = controlsState.groupby.value; @@ -315,25 +330,48 @@ const ColumnSelectPopover = ({ if (controlsState.all_columns?.value) { enhancedFormData.all_columns = controlsState.all_columns.value; } - + console.log('=== ENHANCED FORM DATA FROM CONTROLS ==='); - console.log('Controls state metrics:', controlsState.metrics?.value); - console.log('Controls state groupby:', controlsState.groupby?.value); - console.log('Controls state all_columns:', controlsState.all_columns?.value); + console.log( + 'Controls state metrics:', + controlsState.metrics?.value, + ); + console.log( + 'Controls state groupby:', + controlsState.groupby?.value, + ); + console.log( + 'Controls state all_columns:', + controlsState.all_columns?.value, + ); console.log('Enhanced form data:', enhancedFormData); - + currentFormData = enhancedFormData; } } - + console.log('=== COLUMN MODAL DEBUG ==='); - console.log('Column modal Redux state keys:', Object.keys(currentFormData)); + console.log( + 'Column modal Redux state keys:', + Object.keys(currentFormData), + ); console.log('Column modal form data:', currentFormData); - console.log('Column modal has metrics?', 'metrics' in currentFormData, currentFormData.metrics); - console.log('Column modal has groupby?', 'groupby' in currentFormData, currentFormData.groupby); - console.log('Column modal collected query fields:', collectQueryFields(currentFormData)); + console.log( + 'Column modal has metrics?', + 'metrics' in currentFormData, + currentFormData.metrics, + ); + console.log( + 'Column modal has groupby?', + 'groupby' in currentFormData, + currentFormData.groupby, + ); + console.log( + 'Column modal collected query fields:', + collectQueryFields(currentFormData), + ); console.log('=== END COLUMN MODAL DEBUG ==='); - + const queryFields = collectQueryFields(currentFormData); const validationResult = await callValidationAPI( datasource, @@ -352,37 +390,36 @@ const ColumnSelectPopover = ({ setIsLoadingValidDimensions(false); } }; - + // Trigger API call after a delay to ensure state is current const timeoutId = setTimeout(() => { fetchValidDimensions(); }, 100); - + return () => clearTimeout(timeoutId); - } else { - setValidDimensions(null); - setIsLoadingValidDimensions(false); } + setValidDimensions(null); + setIsLoadingValidDimensions(false); }, [isSemanticLayer, selectedTab, datasource, store]); - + // Also trigger when form data changes (for subsequent updates) useEffect(() => { if (isSemanticLayer && validDimensions !== null && formData && datasource) { const currentFormDataString = JSON.stringify(formData); - + // Only make API call if form data actually changed and we already have loaded once if (currentFormDataString !== previousFormDataRef.current) { previousFormDataRef.current = currentFormDataString; - + const fetchValidDimensions = async () => { setIsLoadingValidDimensions(true); - + try { await new Promise(resolve => setTimeout(resolve, 50)); - + const currentState = store.getState() as ExplorePageState; const currentFormData = currentState.explore.form_data; - + const queryFields = collectQueryFields(currentFormData); const validationResult = await callValidationAPI( datasource, @@ -398,7 +435,7 @@ const ColumnSelectPopover = ({ setIsLoadingValidDimensions(false); } }; - + setTimeout(() => { fetchValidDimensions(); }, 50); @@ -517,15 +554,18 @@ const ColumnSelectPopover = ({ items={[ { key: TABS_KEYS.SAVED, - label: isSemanticLayer && effectiveDisabledTabs.has('saved') ? ( - <Tooltip - title={t('Saved expressions are not supported for semantic layer datasets')} - > - {t('Saved')} - </Tooltip> - ) : ( - t('Saved') - ), + label: + isSemanticLayer && effectiveDisabledTabs.has('saved') ? ( + <Tooltip + title={t( + 'Saved expressions are not supported for semantic layer datasets', + )} + > + {t('Saved')} + </Tooltip> + ) : ( + t('Saved') + ), disabled: effectiveDisabledTabs.has('saved'), children: ( <> @@ -662,15 +702,18 @@ const ColumnSelectPopover = ({ }, { key: TABS_KEYS.SQL_EXPRESSION, - label: isSemanticLayer && effectiveDisabledTabs.has('sqlExpression') ? ( - <Tooltip - title={t('Custom SQL expressions are not supported for semantic layer datasets')} - > - {t('Custom SQL')} - </Tooltip> - ) : ( - t('Custom SQL') - ), + label: + isSemanticLayer && effectiveDisabledTabs.has('sqlExpression') ? ( + <Tooltip + title={t( + 'Custom SQL expressions are not supported for semantic layer datasets', + )} + > + {t('Custom SQL')} + </Tooltip> + ) : ( + t('Custom SQL') + ), disabled: effectiveDisabledTabs.has('sqlExpression'), children: ( <> diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.jsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.jsx index 82d498da7c..f6bc84a8f0 100644 --- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.jsx +++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.jsx @@ -19,6 +19,7 @@ /* eslint-disable camelcase */ import { PureComponent } from 'react'; import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; import { isDefined, t, @@ -68,6 +69,8 @@ const propTypes = { datasource: PropTypes.object, isNewMetric: PropTypes.bool, isLabelModified: PropTypes.bool, + // Props from Redux + reduxDatasource: PropTypes.object, }; const defaultProps = { @@ -90,7 +93,7 @@ const StyledSelect = styled(Select)` export const SAVED_TAB_KEY = 'SAVED'; -export default class AdhocMetricEditPopover extends PureComponent { +class AdhocMetricEditPopover extends PureComponent { // "Saved" is a default tab unless there are no saved metrics for dataset defaultActiveTabKey = this.getDefaultTab(); @@ -149,16 +152,19 @@ export default class AdhocMetricEditPopover extends PureComponent { getDefaultTab() { const { adhocMetric, savedMetric, savedMetricsOptions, isNewMetric } = this.props; - + // For semantic layer datasets, always default to Saved tab if available if (this.isSemanticLayer()) { - if (Array.isArray(savedMetricsOptions) && savedMetricsOptions.length > 0) { + if ( + Array.isArray(savedMetricsOptions) && + savedMetricsOptions.length > 0 + ) { return SAVED_TAB_KEY; } // If no saved metrics available, still return SAVED_TAB_KEY to show empty state return SAVED_TAB_KEY; } - + if (isDefined(adhocMetric.column) || isDefined(adhocMetric.sqlExpression)) { return adhocMetric.expressionType; } @@ -177,7 +183,9 @@ export default class AdhocMetricEditPopover extends PureComponent { if (!datasource || !('database' in datasource) || !datasource.database) { return false; } - return Boolean(datasource.database.engine_information?.supports_dynamic_columns); + return Boolean( + datasource.database.engine_information?.supports_dynamic_columns, + ); } onSave() { @@ -324,11 +332,29 @@ export default class AdhocMetricEditPopover extends PureComponent { datasource, isNewMetric, isLabelModified, + reduxDatasource, ...popoverProps } = this.props; const { adhocMetric, savedMetric } = this.state; const keywords = sqlKeywords.concat(getColumnKeywords(columns)); + // For semantic layer datasets, filter saved metrics to show only valid ones + // Use the isDisabled state set by the main verification system instead of all metrics + let filteredSavedMetricsOptions = savedMetricsOptions; + if (this.isSemanticLayer() && reduxDatasource?.metrics) { + // Create a set of metric names that are NOT disabled in Redux state + const validMetricNames = new Set( + reduxDatasource.metrics + .filter(metric => !metric.isDisabled) + .map(metric => metric.metric_name), + ); + + // Filter savedMetricsOptions to only include valid metrics + filteredSavedMetricsOptions = ensureIsArray(savedMetricsOptions).filter( + metric => validMetricNames.has(metric.metric_name), + ); + } + const columnValue = (adhocMetric.column && adhocMetric.column.column_name) || adhocMetric.inferSqlExpressionColumn(); @@ -354,7 +380,10 @@ export default class AdhocMetricEditPopover extends PureComponent { const savedSelectProps = { ariaLabel: t('Select saved metrics'), - placeholder: t('%s saved metric(s)', savedMetricsOptions?.length ?? 0), + placeholder: t( + '%s saved metric(s)', + filteredSavedMetricsOptions?.length ?? 0, + ), value: savedMetric?.metric_name, onChange: this.onSavedMetricChange, allowClear: true, @@ -399,10 +428,10 @@ export default class AdhocMetricEditPopover extends PureComponent { key: SAVED_TAB_KEY, label: t('Saved'), children: - ensureIsArray(savedMetricsOptions).length > 0 ? ( + ensureIsArray(filteredSavedMetricsOptions).length > 0 ? ( <FormItem label={t('Saved metric')}> <StyledSelect - options={ensureIsArray(savedMetricsOptions).map( + options={ensureIsArray(filteredSavedMetricsOptions).map( savedMetric => ({ value: savedMetric.metric_name, label: this.renderMetricOption(savedMetric), @@ -446,19 +475,24 @@ export default class AdhocMetricEditPopover extends PureComponent { }, { key: EXPRESSION_TYPES.SIMPLE, - label: extra.disallow_adhoc_metrics || this.isSemanticLayer() ? ( - <Tooltip - title={ - this.isSemanticLayer() - ? t('Simple ad-hoc metrics are not supported for semantic layer datasets') - : t('Simple ad-hoc metrics are not enabled for this dataset') - } - > - {t('Simple')} - </Tooltip> - ) : ( - t('Simple') - ), + label: + extra.disallow_adhoc_metrics || this.isSemanticLayer() ? ( + <Tooltip + title={ + this.isSemanticLayer() + ? t( + 'Simple ad-hoc metrics are not supported for semantic layer datasets', + ) + : t( + 'Simple ad-hoc metrics are not enabled for this dataset', + ) + } + > + {t('Simple')} + </Tooltip> + ) : ( + t('Simple') + ), disabled: extra.disallow_adhoc_metrics || this.isSemanticLayer(), children: ( <> @@ -487,19 +521,24 @@ export default class AdhocMetricEditPopover extends PureComponent { }, { key: EXPRESSION_TYPES.SQL, - label: extra.disallow_adhoc_metrics || this.isSemanticLayer() ? ( - <Tooltip - title={ - this.isSemanticLayer() - ? t('Custom SQL ad-hoc metrics are not supported for semantic layer datasets') - : t('Custom SQL ad-hoc metrics are not enabled for this dataset') - } - > - {t('Custom SQL')} - </Tooltip> - ) : ( - t('Custom SQL') - ), + label: + extra.disallow_adhoc_metrics || this.isSemanticLayer() ? ( + <Tooltip + title={ + this.isSemanticLayer() + ? t( + 'Custom SQL ad-hoc metrics are not supported for semantic layer datasets', + ) + : t( + 'Custom SQL ad-hoc metrics are not enabled for this dataset', + ) + } + > + {t('Custom SQL')} + </Tooltip> + ) : ( + t('Custom SQL') + ), disabled: extra.disallow_adhoc_metrics || this.isSemanticLayer(), children: ( <SQLEditor @@ -558,3 +597,10 @@ export default class AdhocMetricEditPopover extends PureComponent { } AdhocMetricEditPopover.propTypes = propTypes; AdhocMetricEditPopover.defaultProps = defaultProps; + +// Map Redux state to props to get access to datasource with disabled states +const mapStateToProps = state => ({ + reduxDatasource: state.explore?.datasource, +}); + +export default connect(mapStateToProps)(AdhocMetricEditPopover); diff --git a/superset-frontend/src/explore/components/controls/SemanticLayerVerification.tsx b/superset-frontend/src/explore/components/controls/SemanticLayerVerification.tsx index d1698b4c75..fc58512bc7 100644 --- a/superset-frontend/src/explore/components/controls/SemanticLayerVerification.tsx +++ b/superset-frontend/src/explore/components/controls/SemanticLayerVerification.tsx @@ -63,23 +63,17 @@ export function collectQueryFields(formData: any): { } if (formData.series) { dimensions.push( - ...(Array.isArray(formData.series) - ? formData.series - : [formData.series]), + ...(Array.isArray(formData.series) ? formData.series : [formData.series]), ); } if (formData.entity) { dimensions.push( - ...(Array.isArray(formData.entity) - ? formData.entity - : [formData.entity]), + ...(Array.isArray(formData.entity) ? formData.entity : [formData.entity]), ); } if (formData.x_axis) { dimensions.push( - ...(Array.isArray(formData.x_axis) - ? formData.x_axis - : [formData.x_axis]), + ...(Array.isArray(formData.x_axis) ? formData.x_axis : [formData.x_axis]), ); } @@ -139,7 +133,7 @@ export function collectQueryFields(formData: any): { dimensions: [...new Set(cleanDimensions)], // Remove duplicates metrics: [...new Set(cleanMetrics)], // Remove duplicates }; - + console.log('collectQueryFields output:', result); return result; } @@ -157,7 +151,10 @@ function supportsSemanticLayerVerification(datasource: Dataset): boolean { } // Cache for API calls to prevent duplicates -const apiCallCache = new Map<string, Promise<{ dimensions: string[]; metrics: string[] } | null>>(); +const apiCallCache = new Map< + string, + Promise<{ dimensions: string[]; metrics: string[] } | null> +>(); /** * Call the validation API @@ -192,7 +189,9 @@ export async function callValidationAPI( dimensions: selectedDimensions, metrics: selectedMetrics, }, - }).then(response => response.json as { dimensions: string[]; metrics: string[] }); + }).then( + response => response.json as { dimensions: string[]; metrics: string[] }, + ); // Cache the promise apiCallCache.set(cacheKey, apiPromise); @@ -230,20 +229,24 @@ export function createMetricsVerification(controlName?: string): AsyncVerify { // 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 - return new Promise((resolve) => { + return new Promise(resolve => { setTimeout(async () => { console.log('=== METRICS VERIFICATION DEFERRED EXECUTION ==='); - + // 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; let currentFormData = form_data; - + if (store) { try { const state = store.getState(); const exploreState = state.explore || {}; currentFormData = exploreState.form_data || form_data; - console.log('Fresh form_data from store after delay:', currentFormData); + console.log( + 'Fresh form_data from store after delay:', + currentFormData, + ); } catch (error) { console.warn('Could not access Redux store:', error); } @@ -291,20 +294,30 @@ export function createMetricsVerification(controlName?: string): AsyncVerify { 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 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)) + validationResult.metrics.filter(metric => + originalMetricNames.has(metric), + ), ); const filteredValidDimensionNames = new Set( - validationResult.dimensions.filter(dim => originalDimensionNames.has(dim)) + validationResult.dimensions.filter(dim => + originalDimensionNames.has(dim), + ), ); if (dataset.metrics) { updatedDatasourceMetrics = dataset.metrics.map((metric: any) => ({ ...metric, - isDisabled: !filteredValidMetricNames.has(metric.metric_name || metric), + isDisabled: !filteredValidMetricNames.has( + metric.metric_name || metric, + ), })); } @@ -312,7 +325,9 @@ export function createMetricsVerification(controlName?: string): AsyncVerify { if (dataset.columns) { updatedDatasourceColumns = dataset.columns.map((column: any) => ({ ...column, - isDisabled: !filteredValidDimensionNames.has(column.column_name || column), + isDisabled: !filteredValidDimensionNames.has( + column.column_name || column, + ), })); } @@ -356,20 +371,24 @@ export function createColumnsVerification(controlName?: string): AsyncVerify { // 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 - return new Promise((resolve) => { + return new Promise(resolve => { setTimeout(async () => { console.log('=== COLUMNS VERIFICATION DEFERRED EXECUTION ==='); - + // 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; let currentFormData = form_data; - + if (store) { try { const state = store.getState(); const exploreState = state.explore || {}; currentFormData = exploreState.form_data || form_data; - console.log('Fresh form_data from store after delay:', currentFormData); + console.log( + 'Fresh form_data from store after delay:', + currentFormData, + ); } catch (error) { console.warn('Could not access Redux store:', error); } @@ -418,28 +437,40 @@ export function createColumnsVerification(controlName?: string): AsyncVerify { 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 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)) + validationResult.dimensions.filter(dim => + originalDimensionNames.has(dim), + ), ); const filteredValidMetricNames = new Set( - validationResult.metrics.filter(metric => originalMetricNames.has(metric)) + 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), + isDisabled: !filteredValidDimensionNames.has( + column.column_name || column, + ), })); } if (dataset.metrics) { updatedDatasourceMetrics = dataset.metrics.map((metric: any) => ({ ...metric, - isDisabled: !filteredValidMetricNames.has(metric.metric_name || metric), + isDisabled: !filteredValidMetricNames.has( + metric.metric_name || metric, + ), })); } @@ -502,7 +533,7 @@ export const SEMANTIC_LAYER_CONTROL_FIELDS = [ 'y', 'size', 'secondary_metric', - + // Dimension controls 'groupby', 'columns', diff --git a/superset-frontend/src/explore/components/controls/withAsyncVerification.tsx b/superset-frontend/src/explore/components/controls/withAsyncVerification.tsx index 92daa2ec73..41c0f83a3c 100644 --- a/superset-frontend/src/explore/components/controls/withAsyncVerification.tsx +++ b/superset-frontend/src/explore/components/controls/withAsyncVerification.tsx @@ -151,7 +151,6 @@ export default function withAsyncVerification({ const [isLoading, setIsLoading] = useState<boolean>(initialIsLoading); const { addWarningToast } = restProps.actions; const verificationTriggeredByChange = useRef(false); - // memoize `restProps`, so that verification only triggers when material // props are actually updated. @@ -205,7 +204,7 @@ export default function withAsyncVerification({ if (onChange) { onChange(value, { ...otherProps, ...verifiedProps }); } - + // Trigger verification with the new value if verification is enabled if (needAsyncVerification && verify) { verificationTriggeredByChange.current = true; @@ -213,7 +212,14 @@ export default function withAsyncVerification({ verifyProps(verify, propsWithNewValue); } }, - [basicOnChange, otherProps, verifiedProps, needAsyncVerification, verify, verifyProps], + [ + basicOnChange, + otherProps, + verifiedProps, + needAsyncVerification, + verify, + verifyProps, + ], ); useEffect(() => { @@ -225,7 +231,13 @@ export default function withAsyncVerification({ } verifyProps(verify, otherProps); } - }, [needAsyncVerification, verify, otherProps, verifyProps, skipEffectVerification]); + }, [ + needAsyncVerification, + verify, + otherProps, + verifyProps, + skipEffectVerification, + ]); return ( <ControlComponent