Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/components/DataSourceFormBody/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,18 @@ const DataSourceFormBody: FC<DataSourceFormBodyProps> = ({
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]
);
Expand Down
1 change: 1 addition & 0 deletions src/components/Modal/index.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
position: absolute;
top: -15px;
right: -60px;
filter: brightness(0.45) saturate(1.15);
}
147 changes: 124 additions & 23 deletions src/components/SmartGeneration/FilterBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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];

Expand All @@ -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 (
<Space size={8} align="center" wrap>
<Select
showSearch
style={{ width: 200 }}
style={{ width: 360 }}
size="small"
placeholder="Column"
value={filter.column || undefined}
onChange={(val) => onColumnChange(index, val)}
filterOption={(input, option) =>
(option?.label ?? "")
.toString()
.toLowerCase()
.includes(input.toLowerCase())
}
options={columnOptions}
onChange={(val) => {
onColumnChange(index, val);
setColumnSearch("");
}}
onSearch={setColumnSearch}
onBlur={() => setColumnSearch("")}
popupMatchSelectWidth={false}
dropdownStyle={{ width: 520 }}
optionFilterProp="label"
filterOption={false}
options={filteredColumnOptions}
/>
<Select
style={{ width: 160 }}
Expand Down Expand Up @@ -716,7 +805,19 @@ const FilterBuilder: FC<FilterBuilderProps> = ({
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]
);

Expand Down
40 changes: 30 additions & 10 deletions src/components/SmartGeneration/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1130,17 +1130,29 @@ const SmartGeneration: FC<SmartGenerationProps> = ({
[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(() => {
Expand All @@ -1160,6 +1172,16 @@ const SmartGeneration: FC<SmartGenerationProps> = ({
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);
Expand Down Expand Up @@ -1329,11 +1351,9 @@ const SmartGeneration: FC<SmartGenerationProps> = ({
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 ?? "")
Expand Down
4 changes: 3 additions & 1 deletion src/pages/Explore/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ const ExploreWrapper = () => {
branch_id: currentBranch?.id,
},
pause: true,
requestPolicy: "cache-and-network",
});

useCheckResponse(
Expand Down Expand Up @@ -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;
}
Expand Down
35 changes: 33 additions & 2 deletions src/pages/Models/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,15 @@ export const Models: React.FC<ModelsProps> = ({
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 (
<Layout
icon={<ModelsActiveIcon />}
Expand Down Expand Up @@ -314,8 +323,16 @@ export const Models: React.FC<ModelsProps> = ({
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
}
/>
</Modal>
</>
Expand Down Expand Up @@ -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();
};

Expand Down