diff --git a/src/components/DataSourceFormBody/index.tsx b/src/components/DataSourceFormBody/index.tsx index 7508d198..ee02fb95 100644 --- a/src/components/DataSourceFormBody/index.tsx +++ b/src/components/DataSourceFormBody/index.tsx @@ -72,14 +72,18 @@ const DataSourceFormBody: FC = ({ const SMART_GEN_TABLES = ["semantic_events", "data_points", "entities"]; const onSmartGenerate = useCallback( - (_schemaName: string, _tableName: string) => { + (schemaName: string, tableName: string) => { if (!editId || !teamData?.dataSources) return; const ds = teamData.dataSources.find((d) => d.id === editId); const activeBranch = ds?.branches?.find( (b) => b.status === Branch_Statuses_Enum.Active ); if (!activeBranch) return; - setLocation(`${MODELS}/${editId}/${activeBranch.id}/smartgen`); + const schemaParam = encodeURIComponent(schemaName); + const tableParam = encodeURIComponent(tableName); + setLocation( + `${MODELS}/${editId}/${activeBranch.id}/smartgen?schema=${schemaParam}&table=${tableParam}` + ); }, [editId, teamData, setLocation] ); diff --git a/src/components/Modal/index.module.less b/src/components/Modal/index.module.less index 12032dd3..9a1ece1d 100644 --- a/src/components/Modal/index.module.less +++ b/src/components/Modal/index.module.less @@ -16,4 +16,5 @@ position: absolute; top: -15px; right: -60px; + filter: brightness(0.45) saturate(1.15); } diff --git a/src/components/SmartGeneration/FilterBuilder.tsx b/src/components/SmartGeneration/FilterBuilder.tsx index f7e756bd..74e41618 100644 --- a/src/components/SmartGeneration/FilterBuilder.tsx +++ b/src/components/SmartGeneration/FilterBuilder.tsx @@ -121,6 +121,68 @@ function getOperatorsForType(valueType: string): OperatorDef[] { return ALL_OPERATORS.filter((op) => allowed.includes(op.value)); } +function normalizeText(value: unknown): string { + if (value == null) return ""; + return String(value).toLowerCase().trim(); +} + +function findOrderedTokenStart(text: string, tokens: string[]): number { + let cursor = 0; + let firstStart = -1; + for (const token of tokens) { + const idx = text.indexOf(token, cursor); + if (idx < 0) return -1; + if (firstStart < 0) firstStart = idx; + cursor = idx + token.length; + } + return firstStart; +} + +function classifyColumnMatch( + query: string, + option: { label?: unknown; value?: unknown } +): { matched: boolean; tier: number; index: number } { + const value = normalizeText(option?.value); + const label = normalizeText(option?.label); + const text = value || label; + if (!query) return { matched: true, tier: 9, index: 0 }; + if (!text) + return { matched: false, tier: 99, index: Number.MAX_SAFE_INTEGER }; + + // Tier 0: exact match + if (text === query || label === query) { + return { matched: true, tier: 0, index: 0 }; + } + + // Tier 1: left-most prefix match + if (text.startsWith(query)) { + return { matched: true, tier: 1, index: 0 }; + } + + // Tier 2/3: ordered token match from left to right + const queryTokens = query.split(/\s+/).filter(Boolean); + const orderedIdx = findOrderedTokenStart(text, queryTokens); + if (orderedIdx >= 0) { + const atBoundary = + orderedIdx === 0 || /[._\-\s]/.test(text[orderedIdx - 1] || ""); + return { matched: true, tier: atBoundary ? 2 : 3, index: orderedIdx }; + } + + return { matched: false, tier: 99, index: Number.MAX_SAFE_INTEGER }; +} + +function isKeyValueColumn(columnName: string): boolean { + const name = normalizeText(columnName); + return ( + name.includes(".key") || + name.includes(".value") || + name.endsWith("_key") || + name.endsWith("_value") || + name.includes("key.") || + name.includes("value.") + ); +} + // --------------------------------------------------------------------------- // Cube.js load API — same pipeline as Explore page // --------------------------------------------------------------------------- @@ -364,20 +426,9 @@ function useColumnValues( const doFetch = useCallback( (searchTerm: string, signal: AbortSignal) => { const a = argsRef.current; - if (a.dimensionMember && a.dimensionMap && a.datasourceId) { - // Cube.js load path (model exists) - const cubeFilters = buildCubeFilters(a.siblingFilters, a.dimensionMap); - return loadCubeValues( - a.dimensionMember, - searchTerm, - cubeFilters, - a.datasourceId, - a.branchId, - signal - ); - } if (a.tableName && a.tableSchema && a.column && a.datasourceId) { - // Direct query path (no model — partition filter from securityContext) + // Prefer direct table query so value options are always sourced + // from the currently selected table/schema in Smart Generate. return loadDirectValues( a.tableName, a.tableSchema, @@ -389,6 +440,18 @@ function useColumnValues( signal ); } + if (a.dimensionMember && a.dimensionMap && a.datasourceId) { + // Fallback path when table context is unavailable + const cubeFilters = buildCubeFilters(a.siblingFilters, a.dimensionMap); + return loadCubeValues( + a.dimensionMember, + searchTerm, + cubeFilters, + a.datasourceId, + a.branchId, + signal + ); + } return Promise.resolve([]); }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -640,6 +703,7 @@ const FilterRow: FC<{ }) => { const vt = getValueType(schema, filter.column); const operators = getOperatorsForType(vt); + const [columnSearch, setColumnSearch] = useState(""); const dimensionMember = dimensionMap?.[filter.column]; @@ -654,22 +718,47 @@ const FilterRow: FC<{ branchId ); + const filteredColumnOptions = useMemo(() => { + const query = normalizeText(columnSearch); + if (!query) return columnOptions; + + return columnOptions + .map((option, originalIndex) => { + const match = classifyColumnMatch(query, option); + return { option, originalIndex, ...match }; + }) + .filter((entry) => entry.matched) + .sort((a, b) => { + // New search behavior: strict left-to-right ordering. + if (a.tier !== b.tier) return a.tier - b.tier; + if (a.index !== b.index) return a.index - b.index; + if (a.originalIndex !== b.originalIndex) { + return a.originalIndex - b.originalIndex; + } + return a.option.value.localeCompare(b.option.value); + }) + .map((entry) => entry.option); + }, [columnOptions, columnSearch]); + return ( = ({ branchId, }) => { const columnOptions = useMemo( - () => schema.map((col) => ({ label: col.name, value: col.name })), + () => + schema + .map((col) => ({ label: col.name, value: col.name })) + .sort((a, b) => { + const aKeyValue = isKeyValueColumn(a.value); + const bKeyValue = isKeyValueColumn(b.value); + + if (aKeyValue !== bKeyValue) { + return aKeyValue ? -1 : 1; + } + + return a.value.localeCompare(b.value); + }), [schema] ); diff --git a/src/components/SmartGeneration/index.tsx b/src/components/SmartGeneration/index.tsx index 6a4a6fa8..4c894744 100644 --- a/src/components/SmartGeneration/index.tsx +++ b/src/components/SmartGeneration/index.tsx @@ -1130,17 +1130,29 @@ const SmartGeneration: FC = ({ [dataSource.id, branchId, filters] ); - // On reprofile flow: pre-populate filters and stay on select step for user review + // Sync late-arriving initial selection (e.g. after page refresh). + // Initial table/schema can be resolved by parent after this component mounts. useEffect(() => { - if (hasInitial && selectedTable && selectedSchema) { - // Pre-populate filters from previous generation if available + const tableExistsInSchema = + !!initialSchema && + !!initialTable && + !!schema?.[initialSchema]?.[initialTable]; + + if (tableExistsInSchema) { + setSelectedSchema(initialSchema); + setSelectedTable(initialTable); + setStep("select"); + setError(null); + + // Pre-populate filters from previous generation when reprofiling. if (previousFilters && previousFilters.length > 0) { setFilters(previousFilters); } - // Stay on "select" step so user can review/edit filters before profiling } + }, [initialSchema, initialTable, previousFilters, schema]); + + useEffect(() => { return () => abortRef.current?.abort(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const tableOptions = useMemo(() => { @@ -1160,6 +1172,16 @@ const SmartGeneration: FC = ({ return options; }, [schema]); + const isTableOptionsLoading = !schema || tableOptions.length === 0; + + const selectedTableValue = useMemo(() => { + if (!selectedSchema || !selectedTable) return undefined; + const value = `${selectedSchema}::${selectedTable}`; + return tableOptions.some((option) => option.value === value) + ? value + : undefined; + }, [selectedSchema, selectedTable, tableOptions]); + const handleTableSelect = useCallback((value: string) => { const [schemaName, tableName] = value.split("::"); setSelectedSchema(schemaName); @@ -1329,11 +1351,9 @@ const SmartGeneration: FC = ({ placeholder="Search for a table..." style={{ width: "100%" }} size="large" - value={ - selectedTable - ? `${selectedSchema}::${selectedTable}` - : undefined - } + disabled={isTableOptionsLoading} + loading={isTableOptionsLoading} + value={selectedTableValue} onChange={handleTableSelect} filterOption={(input, option) => (option?.label ?? "") diff --git a/src/pages/Explore/index.tsx b/src/pages/Explore/index.tsx index 53395bd2..9cee0671 100644 --- a/src/pages/Explore/index.tsx +++ b/src/pages/Explore/index.tsx @@ -220,6 +220,7 @@ const ExploreWrapper = () => { branch_id: currentBranch?.id, }, pause: true, + requestPolicy: "cache-and-network", }); useCheckResponse( @@ -303,7 +304,8 @@ const ExploreWrapper = () => { useTrackedEffect(() => { if (curSource?.id && currentBranch?.id) { - execMetaQuery(); + // Force fresh cube metadata so newly generated models appear immediately. + execMetaQuery({ requestPolicy: "network-only" }); doReset(initialState); delete currentExploration.data; } diff --git a/src/pages/Models/index.tsx b/src/pages/Models/index.tsx index 5169e898..490d9af6 100644 --- a/src/pages/Models/index.tsx +++ b/src/pages/Models/index.tsx @@ -205,6 +205,15 @@ export const Models: React.FC = ({ const Layout = dataSources && dataSources.length === 0 ? AppLayout : SidebarLayout; + const smartGenQueryTarget = useMemo(() => { + if (!smartGenModalVisible || typeof window === "undefined") return null; + const params = new URLSearchParams(window.location.search); + const schema = params.get("schema"); + const table = params.get("table"); + if (!schema || !table) return null; + return { schema, table }; + }, [smartGenModalVisible]); + return ( } @@ -314,8 +323,16 @@ export const Models: React.FC = ({ branchId={currentBranch?.id || ""} onComplete={onSmartGenComplete} onCancel={onModalClose} - initialTable={reprofileTarget?.table ?? undefined} - initialSchema={reprofileTarget?.schema ?? undefined} + initialTable={ + reprofileTarget?.table ?? + smartGenQueryTarget?.table ?? + undefined + } + initialSchema={ + reprofileTarget?.schema ?? + smartGenQueryTarget?.schema ?? + undefined + } /> @@ -668,6 +685,20 @@ const ModelsWrapper: React.FC = () => { } await execCreateVersionMutation({ object: versionData }); + // Invalidate CubeJS per-user caches so Explore reflects deleted/updated models immediately. + try { + await fetch("/api/v1/internal/invalidate-cache", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + type: "user", + userId: currentUser?.id, + }), + }); + } catch { + // Non-fatal: metadata refresh below still runs. + } + execQueryMeta({ id: curSource?.id, branch_id: currentBranch?.id }); execVersionAll(); };