diff --git a/public/locales/en/models.json b/public/locales/en/models.json index a639e20d..c7bde4a5 100644 --- a/public/locales/en/models.json +++ b/public/locales/en/models.json @@ -34,5 +34,11 @@ "remove_branch": "Remove branch" }, "last_modify": "Last modify", - "no_model": "Select model to edit" + "no_model": "Select model to edit", + "smart_gen": { + "output_names_title": "Output names & save location", + "output_save_branch_label": "Save generated model to branch", + "output_save_branch_hint": "The new file and version are created on this branch (can differ from the sidebar). By default, matches the branch you used when profiling; change it here before Preview or Apply.", + "no_branches": "This data source has no branches. Create a branch first, then use Smart Generate." + } } diff --git a/public/locales/ru/models.json b/public/locales/ru/models.json index c9f1bc10..99713dd6 100644 --- a/public/locales/ru/models.json +++ b/public/locales/ru/models.json @@ -34,5 +34,11 @@ "remove_branch": "Удалить ветку" }, "last_modify": "Последнее изменение", - "no_model": "Выберите модель для редактирования" + "no_model": "Выберите модель для редактирования", + "smart_gen": { + "output_names_title": "Имена и место сохранения", + "output_save_branch_label": "Сохранить сгенерированную модель в ветку", + "output_save_branch_hint": "Новый файл и версия создаются в этой ветке (может отличаться от боковой панели). По умолчанию совпадает с веткой, в которой вы запускали профилирование; перед предпросмотром или применением можно сменить.", + "no_branches": "У источника данных нет веток. Сначала создайте ветку, затем запустите умную генерацию." + } } diff --git a/public/locales/zh/models.json b/public/locales/zh/models.json index 8285aeea..6c9cfc20 100644 --- a/public/locales/zh/models.json +++ b/public/locales/zh/models.json @@ -34,5 +34,11 @@ "remove_branch": "删除分支" }, "last_modify": "上次修改", - "no_model": "选择要编辑的模型" + "no_model": "选择要编辑的模型", + "smart_gen": { + "output_names_title": "输出名称与保存位置", + "output_save_branch_label": "将生成的模型保存到分支", + "output_save_branch_hint": "新文件和版本会写入此分支(可与侧栏不同)。默认与剖析时所在分支一致;在预览或应用前可在此修改。", + "no_branches": "此数据源还没有分支。请先创建分支,再使用智能生成。" + } } diff --git a/src/components/SmartGeneration/index.module.less b/src/components/SmartGeneration/index.module.less index a5146db9..c5563d88 100644 --- a/src/components/SmartGeneration/index.module.less +++ b/src/components/SmartGeneration/index.module.less @@ -564,3 +564,18 @@ gap: 8px; margin-bottom: 8px; } + +.branchAlert { + margin-bottom: 16px; +} + +.outputNamesSection { + border: 1px solid rgba(114, 46, 209, 0.18); + background: #faf5ff; + border-radius: 8px; + padding: 16px 16px 12px; +} + +.saveBranchInOutput { + display: block; +} diff --git a/src/components/SmartGeneration/index.tsx b/src/components/SmartGeneration/index.tsx index 21c29b76..87d08f21 100644 --- a/src/components/SmartGeneration/index.tsx +++ b/src/components/SmartGeneration/index.tsx @@ -18,8 +18,9 @@ import { useTranslation } from "react-i18next"; import Button from "@/components/Button"; import AuthTokensStore from "@/stores/AuthTokensStore"; -import type { DataSourceInfo, Schema } from "@/types/dataSource"; +import type { Branch, DataSourceInfo, Schema } from "@/types/dataSource"; import { + Branch_Statuses_Enum, useSmartGenDataSchemasMutation, useFetchMetaQuery, } from "@/graphql/generated"; @@ -846,7 +847,8 @@ interface SmartGenerationProps { dataSource: DataSourceInfo; schema: Schema | undefined; branchId: string; - onComplete: () => void; + branches: Branch[]; + onComplete: (savedBranchId: string) => void; onCancel: () => void; /** Pre-selected table schema (e.g. "cst") from model provenance — skips table selection */ initialSchema?: string; @@ -860,6 +862,7 @@ const SmartGeneration: FC = ({ dataSource, schema, branchId, + branches, onComplete, onCancel, initialSchema, @@ -867,8 +870,12 @@ const SmartGeneration: FC = ({ previousFilters, }) => { const { t } = useTranslation(["models", "common"]); - const hasInitial = !!(initialSchema && initialTable); const [step, setStep] = useState("select"); + const [targetBranchId, setTargetBranchId] = useState(branchId); + const [lockedBranchId, setLockedBranchId] = useState(null); + const [saveToBranchId, setSaveToBranchId] = useState(""); + + const metaBranchId = lockedBranchId || targetBranchId; const [filters, setFilters] = useState( previousFilters || [] ); @@ -907,19 +914,34 @@ const SmartGeneration: FC = ({ const [smartGenResult, execSmartGen] = useSmartGenDataSchemasMutation(); + useEffect(() => { + setTargetBranchId(branchId); + }, [branchId]); + + useEffect(() => { + if (!branches?.length) return; + const ids = new Set(branches.map((b) => b.id)); + if (targetBranchId && ids.has(targetBranchId)) return; + if (branchId && ids.has(branchId)) { + setTargetBranchId(branchId); + return; + } + setTargetBranchId(branches[0].id); + }, [branches, branchId, targetBranchId]); + // Fetch Cube.js meta — same query the Explore page uses. // pause=false when both datasource + branch are available (mirrors Explore page pattern). const [metaResult, execMetaQuery] = useFetchMetaQuery({ - variables: { datasource_id: dataSource.id!, branch_id: branchId }, - pause: !dataSource.id || !branchId, + variables: { datasource_id: dataSource.id!, branch_id: metaBranchId }, + pause: !dataSource.id || !metaBranchId, }); // Re-fetch meta when datasource or branch changes useEffect(() => { - if (dataSource.id && branchId) { + if (dataSource.id && metaBranchId) { execMetaQuery(); } - }, [dataSource.id, branchId, execMetaQuery]); + }, [dataSource.id, metaBranchId, execMetaQuery]); // Build dimension map: raw ClickHouse column name → fully-qualified Cube.js // dimension member (e.g. "event" → "semantic_events.event"). @@ -1009,6 +1031,7 @@ const SmartGeneration: FC = ({ abortRef.current = controller; setStep("profiling"); + setLockedBranchId(targetBranchId); setError(null); setRawProfile(null); setChangePreview(null); @@ -1026,12 +1049,12 @@ const SmartGeneration: FC = ({ Accept: "text/event-stream", ...(token ? { Authorization: `Bearer ${token}` } : {}), "x-hasura-datasource-id": dataSource.id!, - "x-hasura-branch-id": branchId, + "x-hasura-branch-id": targetBranchId, }, body: JSON.stringify({ table: tblName, schema: tblSchema, - branchId, + branchId: targetBranchId, ...(filters.length > 0 ? { filters } : {}), }), signal: controller.signal, @@ -1088,10 +1111,13 @@ const SmartGeneration: FC = ({ .map((c: any) => c.name); setSelectedColumns(new Set(active)); } + setSaveToBranchId(targetBranchId); setStep("preview"); } else if (eventType === "error") { setError(data.error || "Profiling failed"); setStep("select"); + setLockedBranchId(null); + setSaveToBranchId(""); } }; @@ -1130,9 +1156,11 @@ const SmartGeneration: FC = ({ if (err.name === "AbortError") return; // cancelled by user setError(err.message || "Profiling failed"); setStep("select"); + setLockedBranchId(null); + setSaveToBranchId(""); }); }, - [dataSource.id, branchId, filters] + [dataSource.id, targetBranchId, filters] ); // Sync late-arriving initial selection (e.g. after page refresh). @@ -1177,6 +1205,20 @@ const SmartGeneration: FC = ({ return options; }, [schema]); + const branchOptions = useMemo( + () => + (branches || []).map((b) => ({ + value: b.id, + label: + b.status === Branch_Statuses_Enum.Active + ? `${b.name} (${t("common:words.default")})` + : b.name, + })), + [branches, t] + ); + + const saveBranch = saveToBranchId || lockedBranchId || targetBranchId; + const isTableOptionsLoading = !schema || tableOptions.length === 0; const selectedTableValue = useMemo(() => { @@ -1196,9 +1238,13 @@ const SmartGeneration: FC = ({ }, []); const handleProfile = useCallback(() => { + if (!branchOptions.length) { + setError(t("models:smart_gen.no_branches")); + return; + } if (!selectedTable || !selectedSchema) return; startProfile(selectedSchema, selectedTable); - }, [selectedTable, selectedSchema, startProfile]); + }, [branchOptions.length, selectedTable, selectedSchema, startProfile, t]); // Preview changes (dry-run) — no ClickHouse query, uses cached profile const handlePreviewChanges = useCallback(async () => { @@ -1216,7 +1262,7 @@ const SmartGeneration: FC = ({ const result = await execSmartGen({ datasource_id: dataSource.id!, - branch_id: branchId, + branch_id: saveBranch, table_name: selectedTable, table_schema: selectedSchema, merge_strategy: mergeStrategy, @@ -1282,7 +1328,7 @@ const SmartGeneration: FC = ({ setStep("change_preview"); }, [ dataSource.id, - branchId, + saveBranch, selectedTable, selectedSchema, mergeStrategy, @@ -1319,7 +1365,7 @@ const SmartGeneration: FC = ({ const result = await execSmartGen({ datasource_id: dataSource.id!, - branch_id: branchId, + branch_id: saveBranch, table_name: selectedTable, table_schema: selectedSchema, merge_strategy: mergeStrategy, @@ -1346,7 +1392,7 @@ const SmartGeneration: FC = ({ setStep("done"); }, [ dataSource.id, - branchId, + saveBranch, selectedTable, selectedSchema, mergeStrategy, @@ -1365,6 +1411,8 @@ const SmartGeneration: FC = ({ abortRef.current?.abort(); setError(null); setStep("select"); + setLockedBranchId(null); + setSaveToBranchId(""); }, []); const genData = smartGenResult.data?.smart_gen_dataschemas; @@ -1384,6 +1432,15 @@ const SmartGeneration: FC = ({ + {step === "select" && branchOptions.length === 0 && ( + + )} + {/* Step 1: Table Selection */} {step === "select" && ( <> @@ -1440,7 +1497,7 @@ const SmartGeneration: FC = ({ tableName={selectedTable} tableSchema={selectedSchema} datasourceId={dataSource.id!} - branchId={branchId} + branchId={metaBranchId} /> )} @@ -1461,7 +1518,7 @@ const SmartGeneration: FC = ({