feat(tdengine): 优化子表 TAG 值填写交互

- 超级表名改为下拉选择,自动从当前库拉取超级表列表
- 选择超级表后自动拉取 TAG 定义,逐行显示名称/类型/输入框
- 根据 TAG 类型自动处理引号拼接 TAGS 值
- 加载失败时降级为文本输入框兜底
- 新增 i18n 中/英/日/德/繁中翻译
This commit is contained in:
Syngnat
2026-08-03 09:27:55 +08:00
parent 0e8ec993c4
commit 42e26a52cd
6 changed files with 190 additions and 14 deletions

View File

@@ -1,6 +1,6 @@
import Modal from './common/ResizableDraggableModal';
import React, { useEffect, useState, useContext, useMemo, useRef, useCallback } from 'react';
import { Table, Tabs, Button, message, Input, Checkbox, AutoComplete, Tooltip, Select, Empty, Space, Tag, Radio } from 'antd';
import { Table, Tabs, Button, message, Input, Checkbox, AutoComplete, Tooltip, Select, Empty, Space, Tag, Radio, Spin } from 'antd';
import { ReloadOutlined, SaveOutlined, PlusOutlined, DeleteOutlined, MenuOutlined, FileTextOutlined, EyeOutlined, EditOutlined, ExclamationCircleOutlined, CopyOutlined, TableOutlined } from '@ant-design/icons';
import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragOverlay } from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
@@ -8,7 +8,7 @@ import { CSS } from '@dnd-kit/utilities';
import Editor from './MonacoEditor';
import { TabData, ColumnDefinition, IndexDefinition, ForeignKeyDefinition, TriggerDefinition } from '../types';
import { useStore } from '../store';
import { DBGetColumns, DBGetIndexes, DBQueryAudited, DBGetForeignKeys, DBGetTriggers, DBShowCreateTable } from '../../wailsjs/go/app/App';
import { DBGetColumns, DBGetIndexes, DBQuery, DBQueryAudited, DBGetForeignKeys, DBGetTriggers, DBShowCreateTable } from '../../wailsjs/go/app/App';
import { hasIndexFormChanged, normalizeIndexFormFromRow, resolveIndexMetadataResponse, shouldRestoreOriginalIndex, toggleIndexSelection as getNextIndexSelection, type IndexDisplaySnapshot } from './tableDesignerIndexUtils';
import { buildIndexCreateSqlPreview } from './tableDesignerIndexSql';
import { buildAlterTablePreviewSql, buildCreateTablePreviewSql, hasAlterTableDraftChanges, type StarRocksCreateTableOptions, type StarRocksDistributionType, type StarRocksKeyModel, type StarRocksTableKind, type TDengineCreateTableOptions, type TDengineTableKind, type TDengineTagDefinition } from './tableDesignerSchemaSql';
@@ -456,6 +456,11 @@ const TableDesigner: React.FC<{ tab: TabData; embedded?: boolean }> = ({ tab, em
{ _key: 'tag-1', name: 'location', type: 'BINARY(64)' },
]);
const [tdengineTagValues, setTdengineTagValues] = useState('');
const [tdengineStableOptions, setTdengineStableOptions] = useState<{ label: string; value: string }[]>([]);
const [tdengineStableOptionsLoading, setTdengineStableOptionsLoading] = useState(false);
const [tdengineChildTagDefs, setTdengineChildTagDefs] = useState<TDengineTagDraft[]>([]);
const [tdengineChildTagValues, setTdengineChildTagValues] = useState<Record<string, string>>({});
const [tdengineChildTagDefsLoading, setTdengineChildTagDefsLoading] = useState(false);
const [columnsLoading, setColumnsLoading] = useState(false);
const [indexesLoading, setIndexesLoading] = useState(false);
@@ -1668,6 +1673,99 @@ ${selectedTrigger.statement}`;
}
}, [isTDengineNewTable, tab.initialTab]);
useEffect(() => {
if (!isTDengineChildNewTable) {
setTdengineStableOptions([]);
return;
}
let cancelled = false;
const fetchSuperTables = async () => {
setTdengineStableOptionsLoading(true);
try {
const conn = connections.find(c => c.id === tab.connectionId);
if (!conn) return;
const config = {
...conn.config,
port: Number(conn.config.port),
password: conn.config.password || "",
database: conn.config.database || "",
useSSH: conn.config.useSSH || false,
ssh: conn.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" }
};
const rpcConfig = buildRpcConnectionConfig(config) as any;
const dbName = tab.dbName || '';
const res = await DBQuery(rpcConfig, dbName, `SHOW STABLES FROM \`${dbName}\``);
if (cancelled) return;
if (res.success && Array.isArray(res.data)) {
const options = res.data.map((row: any) => {
const name = row?.table_name || row?.Table || row?.name || row?.tablename || '';
return { label: String(name), value: String(name) };
}).filter(opt => opt.value);
setTdengineStableOptions(options);
} else {
setTdengineStableOptions([]);
}
} catch {
if (!cancelled) setTdengineStableOptions([]);
} finally {
if (!cancelled) setTdengineStableOptionsLoading(false);
}
};
fetchSuperTables();
return () => { cancelled = true; };
}, [isTDengineChildNewTable, tab.connectionId, tab.dbName, connections]);
useEffect(() => {
if (!isTDengineChildNewTable || !tdengineStableName.trim()) {
setTdengineChildTagDefs([]);
setTdengineChildTagValues({});
return;
}
let cancelled = false;
const fetchTagDefs = async () => {
setTdengineChildTagDefsLoading(true);
try {
const conn = connections.find(c => c.id === tab.connectionId);
if (!conn) return;
const config = {
...conn.config,
port: Number(conn.config.port),
password: conn.config.password || "",
database: conn.config.database || "",
useSSH: conn.config.useSSH || false,
ssh: conn.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" }
};
const rpcConfig = buildRpcConnectionConfig(config) as any;
const dbName = tab.dbName || '';
const res = await DBGetColumns(rpcConfig, dbName, tdengineStableName.trim());
if (cancelled) return;
if (res.success && Array.isArray(res.data)) {
const tagDefs: TDengineTagDraft[] = res.data
.filter((col: any) => (col?.key || col?.Key || '') === 'TAG')
.map((col: any, idx: number) => ({
_key: `tagdef-${idx}`,
name: String(col?.name || col?.Name || ''),
type: String(col?.type || col?.Type || ''),
}));
setTdengineChildTagDefs(tagDefs);
setTdengineChildTagValues({});
} else {
setTdengineChildTagDefs([]);
setTdengineChildTagValues({});
}
} catch {
if (!cancelled) {
setTdengineChildTagDefs([]);
setTdengineChildTagValues({});
}
} finally {
if (!cancelled) setTdengineChildTagDefsLoading(false);
}
};
fetchTagDefs();
return () => { cancelled = true; };
}, [isTDengineChildNewTable, tdengineStableName, tab.connectionId, tab.dbName, connections]);
const parseStarRocksRollupOptions = (raw: string): StarRocksCreateTableOptions['rollups'] => (
String(raw || '')
.split(/\r?\n/)
@@ -1705,11 +1803,23 @@ ${selectedTrigger.statement}`;
const buildTDengineCreateOptions = (): TDengineCreateTableOptions | undefined => {
if (!isTDengineNewTable) return undefined;
let tagValues = tdengineTagValues;
if (tdengineTableKind === 'child' && tdengineChildTagDefs.length > 0) {
tagValues = tdengineChildTagDefs
.map(tag => {
const val = tdengineChildTagValues[tag.name] ?? '';
if (!val) return '';
const needQuote = /^(BINARY|NCHAR|VARBINARY|TIMESTAMP)/i.test(tag.type);
return needQuote ? `'${val.replace(/'/g, "\\'")}'` : val;
})
.filter(Boolean)
.join(', ');
}
return {
tableKind: tdengineTableKind,
stableName: tdengineStableName,
tagDefinitions: tdengineTagDefinitions.map(({ _key, ...tag }) => tag),
tagValues: tdengineTagValues,
tagValues,
};
};
@@ -2509,7 +2619,13 @@ END;`;
message.error(t('table_designer.message.tdengine_stable_name_required', undefined, i18nLanguage));
return;
}
if (!tdengineTagValues.trim()) {
if (tdengineChildTagDefs.length > 0) {
const missing = tdengineChildTagDefs.filter(tag => !(tdengineChildTagValues[tag.name] ?? '').trim());
if (missing.length > 0) {
message.error(t('table_designer.message.tdengine_tag_values_required', undefined, i18nLanguage));
return;
}
} else if (!tdengineTagValues.trim()) {
message.error(t('table_designer.message.tdengine_tag_values_required', undefined, i18nLanguage));
return;
}
@@ -2968,19 +3084,59 @@ END;`;
{tdengineTableKind === 'child' && (
<Space direction="vertical" size={10} style={{ width: '100%' }}>
<Input
<Select
{...noAutoCapInputProps}
value={tdengineStableName}
onChange={(event) => setTdengineStableName(event.target.value)}
showSearch
allowClear
value={tdengineStableName || undefined}
onChange={(value) => setTdengineStableName(value || '')}
placeholder={t('table_designer.tdengine.placeholder.stable_name', undefined, i18nLanguage)}
options={tdengineStableOptions}
loading={tdengineStableOptionsLoading}
style={{ width: '100%' }}
filterOption={(input, option) =>
(option?.label as string ?? '').toLowerCase().includes(input.toLowerCase())
}
notFoundContent={tdengineStableOptionsLoading ? null : t('table_designer.tdengine.message.no_stable_found', undefined, i18nLanguage)}
/>
<Input.TextArea
{...noAutoCapInputProps}
value={tdengineTagValues}
onChange={(event) => setTdengineTagValues(event.target.value)}
autoSize={{ minRows: 3, maxRows: 8 }}
placeholder={t('table_designer.tdengine.placeholder.tag_values', undefined, i18nLanguage)}
/>
{tdengineChildTagDefsLoading || tdengineChildTagDefs.length > 0 ? (
<Spin spinning={tdengineChildTagDefsLoading}>
{tdengineChildTagDefs.length > 0 ? (
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<Space size={8} style={{ width: '100%' }}>
<span style={{ width: 160, color: '#888', fontSize: 12 }}>{t('table_designer.tdengine.tag.name', undefined, i18nLanguage)}</span>
<span style={{ width: 160, color: '#888', fontSize: 12 }}>{t('table_designer.tdengine.tag.type', undefined, i18nLanguage)}</span>
<span style={{ color: '#888', fontSize: 12 }}>{t('table_designer.tdengine.tag.value', undefined, i18nLanguage)}</span>
</Space>
{tdengineChildTagDefs.map((tag) => (
<Space key={tag._key} align="start" wrap>
<span style={{ width: 160, lineHeight: '32px', fontSize: 13, fontWeight: 500 }}>{tag.name}</span>
<span style={{ width: 160, lineHeight: '32px', fontSize: 12, color: '#888' }}>{tag.type}</span>
<Input
{...noAutoCapInputProps}
value={tdengineChildTagValues[tag.name] ?? ''}
onChange={(event) => setTdengineChildTagValues(prev => ({ ...prev, [tag.name]: event.target.value }))}
placeholder={t('table_designer.tdengine.placeholder.tag_value_input', undefined, i18nLanguage)}
style={{ width: 280 }}
/>
</Space>
))}
</Space>
) : (
<div style={{ padding: '16px 0', textAlign: 'center', color: '#888' }}>
{t('table_designer.tdengine.message.loading_tag_defs', undefined, i18nLanguage)}
</div>
)}
</Spin>
) : (
<Input.TextArea
{...noAutoCapInputProps}
value={tdengineTagValues}
onChange={(event) => setTdengineTagValues(event.target.value)}
autoSize={{ minRows: 3, maxRows: 8 }}
placeholder={t('table_designer.tdengine.placeholder.tag_values', undefined, i18nLanguage)}
/>
)}
</Space>
)}
</Space>

View File

@@ -8332,6 +8332,10 @@
"table_designer.tdengine.table_kind.stable": "Supertabelle",
"table_designer.tdengine.tag.name": "TAG-Name",
"table_designer.tdengine.tag.type": "TAG-Typ",
"table_designer.tdengine.tag.value": "TAG-Wert",
"table_designer.tdengine.placeholder.tag_value_input": "TAG-Wert eingeben",
"table_designer.tdengine.message.no_stable_found": "Keine Supertabelle gefunden, bitte erstellen Sie zuerst eine Supertabelle",
"table_designer.tdengine.message.loading_tag_defs": "Lade TAG-Definitionen…",
"table_designer.starrocks.bucket_mode.auto": "Eimeranzahl automatisch",
"table_designer.starrocks.bucket_mode.number": "Feste Eimeranzahl",
"table_designer.starrocks.distribution.hash": "Hash-Verteilung",

View File

@@ -8332,6 +8332,10 @@
"table_designer.tdengine.table_kind.stable": "Supertable",
"table_designer.tdengine.tag.name": "TAG name",
"table_designer.tdengine.tag.type": "TAG type",
"table_designer.tdengine.tag.value": "TAG value",
"table_designer.tdengine.placeholder.tag_value_input": "Enter TAG value",
"table_designer.tdengine.message.no_stable_found": "No supertable found, please create a supertable first",
"table_designer.tdengine.message.loading_tag_defs": "Loading TAG definitions…",
"table_designer.starrocks.bucket_mode.auto": "Auto buckets",
"table_designer.starrocks.bucket_mode.number": "Fixed bucket count",
"table_designer.starrocks.distribution.hash": "Hash distribution",

View File

@@ -8332,6 +8332,10 @@
"table_designer.tdengine.table_kind.stable": "スーパーテーブル",
"table_designer.tdengine.tag.name": "TAG 名",
"table_designer.tdengine.tag.type": "TAG 型",
"table_designer.tdengine.tag.value": "TAG 値",
"table_designer.tdengine.placeholder.tag_value_input": "TAG の値を入力",
"table_designer.tdengine.message.no_stable_found": "スーパーテーブルが見つかりません。最初にスーパーテーブルを作成してください",
"table_designer.tdengine.message.loading_tag_defs": "TAG 定義を読み込み中…",
"table_designer.starrocks.bucket_mode.auto": "バケット数を自動設定",
"table_designer.starrocks.bucket_mode.number": "固定バケット数",
"table_designer.starrocks.distribution.hash": "ハッシュ分散",

View File

@@ -8332,6 +8332,10 @@
"table_designer.tdengine.table_kind.stable": "超级表",
"table_designer.tdengine.tag.name": "TAG 名称",
"table_designer.tdengine.tag.type": "TAG 类型",
"table_designer.tdengine.tag.value": "TAG 值",
"table_designer.tdengine.placeholder.tag_value_input": "请输入 TAG 值",
"table_designer.tdengine.message.no_stable_found": "未找到超级表,请先创建超级表",
"table_designer.tdengine.message.loading_tag_defs": "正在加载 TAG 定义…",
"table_designer.starrocks.bucket_mode.auto": "自动分桶数",
"table_designer.starrocks.bucket_mode.number": "固定分桶数",
"table_designer.starrocks.distribution.hash": "哈希分布",

View File

@@ -8332,6 +8332,10 @@
"table_designer.tdengine.table_kind.stable": "超級表",
"table_designer.tdengine.tag.name": "TAG 名稱",
"table_designer.tdengine.tag.type": "TAG 類型",
"table_designer.tdengine.tag.value": "TAG 值",
"table_designer.tdengine.placeholder.tag_value_input": "請輸入 TAG 值",
"table_designer.tdengine.message.no_stable_found": "未找到超級表,請先創建超級表",
"table_designer.tdengine.message.loading_tag_defs": "正在加載 TAG 定義…",
"table_designer.starrocks.bucket_mode.auto": "自動分桶數",
"table_designer.starrocks.bucket_mode.number": "固定分桶數",
"table_designer.starrocks.distribution.hash": "雜湊分佈",