diff --git a/frontend/src/components/DriverManagerModal.test.tsx b/frontend/src/components/DriverManagerModal.test.tsx index a039e05..449d52c 100644 --- a/frontend/src/components/DriverManagerModal.test.tsx +++ b/frontend/src/components/DriverManagerModal.test.tsx @@ -71,7 +71,32 @@ vi.mock('antd', () => { ); - const Select = () => null; + const Select = ({ value, options, disabled, loading, placeholder, onOpenChange, onChange }: any) => ( + + ); const Progress = () =>
; const Tag = ({ children }: any) => {children}; const Switch = ({ checked, onChange, disabled }: any) => ( @@ -288,4 +313,76 @@ describe('DriverManagerModal toolbar actions', () => { vi.useRealTimers(); } }); + + it('allows switching installed TDengine drivers to a historical compatible version', async () => { + backendApp.GetDriverStatusList.mockResolvedValue({ + success: true, + data: { + downloadDir: 'D:/drivers', + drivers: [ + { + type: 'tdengine', + name: 'TDengine', + builtIn: false, + pinnedVersion: '3.7.8', + installedVersion: '3.7.8', + runtimeAvailable: true, + packageInstalled: true, + connectable: true, + defaultDownloadUrl: 'builtin://activate/tdengine', + message: '已启用', + }, + ], + }, + }); + backendApp.GetDriverVersionList.mockResolvedValue({ + success: true, + data: { + versions: [ + { version: '3.7.8', downloadUrl: 'builtin://activate/tdengine', recommended: true }, + { version: '3.3.1', downloadUrl: 'builtin://activate/tdengine?channel=history&version=3.3.1' }, + ], + }, + }); + backendApp.DownloadDriverPackage.mockResolvedValue({ success: true }); + + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + await flushPromises(); + + const refreshButton = findButton(renderer!, '刷新'); + await act(async () => { + await refreshButton.props.onClick(); + }); + await flushPromises(); + + const versionSelect = renderer!.root.findByType('select'); + await act(async () => { + versionSelect.props.onFocus(); + }); + await flushPromises(); + expect(backendApp.GetDriverVersionList).toHaveBeenCalledWith('tdengine', ''); + + const reloadedVersionSelect = renderer!.root.findByType('select'); + await act(async () => { + reloadedVersionSelect.props.onChange({ target: { value: '3.3.1@@builtin://activate/tdengine?channel=history&version=3.3.1' } }); + }); + await flushPromises(); + + const switchButtons = renderer!.root.findAll((node) => node.type === 'button' && textContent(node).includes('切换版本')); + expect(switchButtons).toHaveLength(1); + const switchButton = switchButtons[0]; + await act(async () => { + await switchButton.props.onClick(); + }); + + expect(backendApp.DownloadDriverPackage).toHaveBeenCalledWith( + 'tdengine', + '3.3.1', + 'builtin://activate/tdengine?channel=history&version=3.3.1', + 'D:/drivers', + ); + }); }); diff --git a/frontend/src/components/DriverManagerModal.tsx b/frontend/src/components/DriverManagerModal.tsx index f303a23..53f1870 100644 --- a/frontend/src/components/DriverManagerModal.tsx +++ b/frontend/src/components/DriverManagerModal.tsx @@ -695,6 +695,29 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG return selectedOption?.version || row.pinnedVersion || ''; }, [selectedVersionMap, versionMap]); + const resolveSelectedVersionOption = useCallback((row: DriverStatusRow) => { + const options = versionMap[row.type] || []; + const selectedKey = selectedVersionMap[row.type]; + return ( + options.find((item) => buildVersionOptionKey(item) === selectedKey) || + options.find((item) => item.recommended) || + options[0] + ); + }, [selectedVersionMap, versionMap]); + + const resolveInstalledDriverVersion = useCallback((row: DriverStatusRow) => ( + String(row.installedVersion || '').trim() || String(row.pinnedVersion || '').trim() + ), []); + + const isDriverVersionSwitchPending = useCallback((row: DriverStatusRow) => { + if (row.builtIn || (!row.packageInstalled && !row.connectable)) { + return false; + } + const selectedVersion = String(resolveSelectedVersionOption(row)?.version || '').trim(); + const installedVersion = resolveInstalledDriverVersion(row); + return !!selectedVersion && !!installedVersion && selectedVersion !== installedVersion; + }, [resolveInstalledDriverVersion, resolveSelectedVersionOption]); + const installDriver = useCallback(async ( row: DriverStatusRow, actionOptions?: { silentToast?: boolean; skipRefresh?: boolean }, @@ -714,9 +737,9 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG } const selectedKey = selectedVersionMap[row.type]; const selectedOption = + versionOptions.find((item) => buildVersionOptionKey(item) === selectedKey) || (row.needsUpdate ? versionOptions.find((item) => item.version === row.pinnedVersion) : undefined) || (row.needsUpdate ? versionOptions.find((item) => item.recommended) : undefined) || - versionOptions.find((item) => buildVersionOptionKey(item) === selectedKey) || versionOptions.find((item) => item.recommended) || versionOptions[0]; const selectedVersion = selectedOption?.version || row.pinnedVersion || ''; @@ -1022,20 +1045,12 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG return 内置驱动无需安装; } - const versionLocked = row.packageInstalled || row.connectable; - if (versionLocked) { - const installedVersion = String(row.installedVersion || '').trim(); - const revisionHint = row.needsUpdate ? ',需重装' : ''; - return ( - - {installedVersion ? `${installedVersion}(已安装${revisionHint})` : `已安装${row.needsUpdate ? ',需重装' : ''}`} - - ); - } - const options = versionMap[row.type] || []; const selectedKey = selectedVersionMap[row.type]; const selectOptions = buildVersionSelectOptions(options); + const installedVersion = resolveInstalledDriverVersion(row); + const versionSwitchPending = isDriverVersionSwitchPending(row); + const selectedOption = resolveSelectedVersionOption(row); const mongoHint = row.type === 'mongodb' ? 'MongoDB 4.0 请使用 1.17.x 兼容驱动;2.x 驱动要求 MongoDB 4.2+。' : ''; @@ -1063,6 +1078,13 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG void loadVersionPackageSize(row, value); }} /> + {(row.packageInstalled || row.connectable) ? ( + + {versionSwitchPending + ? `当前已安装 ${installedVersion || '当前版本'},已选择 ${selectedOption?.version || '目标版本'},点击“切换版本”生效` + : `${installedVersion ? `${installedVersion}(已安装` : '已安装'}${row.needsUpdate ? ',需重装' : ''}${installedVersion ? ')' : ''}`} + + ) : null} {mongoHint ? {mongoHint} : null}
); @@ -1078,6 +1100,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG const loadingLocal = actionState.driverType === row.type && actionState.kind === 'local'; const logs = operationLogMap[row.type] || []; const hasLogs = logs.length > 0; + const versionSwitchPending = isDriverVersionSwitchPending(row); if (isSlimBuildUnavailable && !row.packageInstalled) { return 当前精简版不可安装,请使用 Full 版; @@ -1087,6 +1110,10 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG + ) : versionSwitchPending ? ( + ) : row.connectable ? ( ; + const Input: any = ({ value, onChange, ...rest }: any) => ; + Input.Search = ({ value, onChange, ...rest }: any) => ; + const Spin = ({ children }: any) =>
{children}
; + const Empty = ({ description }: any) =>
{description}
; + const Dropdown = ({ children }: any) =>
{children}
; + const Tooltip = ({ children }: any) =>
{children}
; + const Modal: any = ({ children }: any) =>
{children}
; + Modal.confirm = vi.fn(); + return { + Button, + Dropdown, + Empty, + Input, + Modal, + Spin, + Tooltip, + message: messageApi, + }; +}); + +const flushPromises = async () => { + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + }); +}; + +const collectText = (node: any): string => { + if (!node) { + return ''; + } + if (typeof node === 'string') { + return node; + } + if (Array.isArray(node)) { + return node.map((item) => collectText(item)).join(''); + } + return collectText(node.children || []); +}; + +describe('TableOverview tdengine compatibility', () => { + beforeEach(() => { + vi.clearAllMocks(); + backendApp.DBGetTables.mockResolvedValue({ + success: true, + data: [ + { Table: 'd001' }, + { Table: 'meters' }, + ], + }); + backendApp.DBQuery.mockResolvedValue({ + success: false, + message: '[0x2600] syntax error near', + }); + }); + + it('loads tdengine overview rows through DBGetTables instead of direct metadata SQL', async () => { + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + await flushPromises(); + + expect(backendApp.DBGetTables).toHaveBeenCalledWith(expect.any(Object), 'metrics'); + expect(backendApp.DBQuery).not.toHaveBeenCalled(); + expect(messageApi.error).not.toHaveBeenCalled(); + const renderedText = collectText(renderer!.toJSON()); + expect(renderedText).toContain('meters'); + expect(renderedText).toContain('d001'); + }); +}); diff --git a/frontend/src/components/TableOverview.tsx b/frontend/src/components/TableOverview.tsx index 1085424..ba1c9c7 100644 --- a/frontend/src/components/TableOverview.tsx +++ b/frontend/src/components/TableOverview.tsx @@ -4,7 +4,7 @@ import { Input, Spin, Empty, Dropdown, message, Tooltip, Modal, Button } from 'a import type { MenuProps } from 'antd'; import { TableOutlined, SearchOutlined, ReloadOutlined, SortAscendingOutlined, DatabaseOutlined, ConsoleSqlOutlined, EditOutlined, CopyOutlined, SaveOutlined, DeleteOutlined, ExportOutlined, AppstoreOutlined, UnorderedListOutlined, WarningOutlined } from '@ant-design/icons'; import { buildSidebarTablePinKey, useStore } from '../store'; -import { DBQuery, DBShowCreateTable, ExportTable, DropTable, RenameTable } from '../../wailsjs/go/app/App'; +import { DBGetTables, DBQuery, DBShowCreateTable, ExportTable, DropTable, RenameTable } from '../../wailsjs/go/app/App'; import type { TabData } from '../types'; import { useAutoFetchVisibility } from '../utils/autoFetchVisibility'; import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig'; @@ -231,7 +231,7 @@ const parseTableStats = (dialect: string, rows: Record[]): TableSta }; return { - name: strVal(['Name', 'name', 'table_name', 'tablename', 'TABLE_NAME', 'Device', 'device']), + name: strVal(['Name', 'name', 'table_name', 'tablename', 'TABLE_NAME', 'Table', 'table', 'Device', 'device']), comment: strVal(['Comment', 'table_comment', 'TABLE_COMMENT', 'comments']), rows: numVal(['Rows', 'table_rows', 'TABLE_ROWS', 'num_rows', 'reltuples', 'total_rows']), dataSize: numVal(['Data_length', 'data_length', 'DATA_LENGTH', 'total_bytes']), @@ -290,6 +290,15 @@ const TableOverview: React.FC = ({ tab }) => { useSSH: connection.config.useSSH || false, ssh: connection.config.ssh || { host: '', port: 22, user: '', password: '', keyPath: '' }, }; + if (metadataDialect === 'tdengine') { + const res = await DBGetTables(buildRpcConnectionConfig(config) as any, tab.dbName || ''); + if (res.success && Array.isArray(res.data)) { + setTables(parseTableStats(metadataDialect, res.data)); + } else { + message.error('获取表信息失败: ' + (res.message || '未知错误')); + } + return; + } const sql = buildTableStatusSQL(metadataDialect, tab.dbName || '', schemaName); const res = await DBQuery(buildRpcConnectionConfig(config) as any, tab.dbName || '', sql); if (res.success && Array.isArray(res.data)) { diff --git a/frontend/wailsjs/go/aiservice/Service.d.ts b/frontend/wailsjs/go/aiservice/Service.d.ts index 74c6134..4210500 100755 --- a/frontend/wailsjs/go/aiservice/Service.d.ts +++ b/frontend/wailsjs/go/aiservice/Service.d.ts @@ -1,4 +1,3 @@ -// @ts-nocheck // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT import {ai} from '../models'; diff --git a/frontend/wailsjs/go/aiservice/Service.js b/frontend/wailsjs/go/aiservice/Service.js index 563bc76..dbac75a 100755 --- a/frontend/wailsjs/go/aiservice/Service.js +++ b/frontend/wailsjs/go/aiservice/Service.js @@ -1,4 +1,4 @@ -// @ts-nocheck +// @ts-check // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT diff --git a/frontend/wailsjs/go/app/App.d.ts b/frontend/wailsjs/go/app/App.d.ts index 00ab9b9..57d822e 100755 --- a/frontend/wailsjs/go/app/App.d.ts +++ b/frontend/wailsjs/go/app/App.d.ts @@ -1,4 +1,3 @@ -// @ts-nocheck // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT import {connection} from '../models'; diff --git a/frontend/wailsjs/go/app/App.js b/frontend/wailsjs/go/app/App.js index 5a39a38..779c853 100755 --- a/frontend/wailsjs/go/app/App.js +++ b/frontend/wailsjs/go/app/App.js @@ -1,4 +1,4 @@ -// @ts-nocheck +// @ts-check // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT diff --git a/internal/db/tdengine_applychanges_test.go b/internal/db/tdengine_applychanges_test.go index e2ab794..8a61f21 100644 --- a/internal/db/tdengine_applychanges_test.go +++ b/internal/db/tdengine_applychanges_test.go @@ -252,6 +252,56 @@ func TestTDengineGetTablesIncludesSuperTables(t *testing.T) { } } +func TestTDengineGetTablesFallsBackToLegacyFromSyntax(t *testing.T) { + t.Parallel() + + dbConn, state := openTDengineRecordingDB(t) + state.mu.Lock() + state.queryResults["SHOW TABLES FROM `metrics`"] = tdengineQueryResult{ + err: fmt.Errorf("[0x2600] syntax error near '`metrics`'"), + } + state.queryResults["SHOW STABLES FROM `metrics`"] = tdengineQueryResult{ + err: fmt.Errorf("[0x2600] syntax error near '`metrics`'"), + } + state.queryResults["SHOW TABLES FROM metrics"] = tdengineQueryResult{ + columns: []string{"name"}, + rows: [][]driver.Value{ + {"d001"}, + }, + } + state.queryResults["SHOW STABLES FROM metrics"] = tdengineQueryResult{ + columns: []string{"name"}, + rows: [][]driver.Value{ + {"meters"}, + }, + } + state.mu.Unlock() + + td := &TDengineDB{conn: dbConn} + tables, err := td.GetTables("metrics") + if err != nil { + t.Fatalf("GetTables returned error: %v", err) + } + + wantTables := []string{"d001", "meters"} + if !reflect.DeepEqual(tables, wantTables) { + t.Fatalf("unexpected tables: got=%v want=%v", tables, wantTables) + } + + queries := state.snapshotQueries() + wantQueries := []string{ + "SHOW TABLES FROM `metrics`", + "SHOW STABLES FROM `metrics`", + "SHOW TABLES FROM metrics", + "SHOW STABLES FROM metrics", + "SHOW TABLES", + "SHOW STABLES", + } + if !reflect.DeepEqual(queries, wantQueries) { + t.Fatalf("unexpected query sequence: got=%v want=%v", queries, wantQueries) + } +} + func TestTDengineGetColumnsFallsBackToLegacyDescribeSyntax(t *testing.T) { t.Parallel() diff --git a/internal/db/tdengine_impl.go b/internal/db/tdengine_impl.go index b43d249..69782e8 100644 --- a/internal/db/tdengine_impl.go +++ b/internal/db/tdengine_impl.go @@ -211,13 +211,7 @@ func (t *TDengineDB) GetDatabases() ([]string, error) { } func (t *TDengineDB) GetTables(dbName string) ([]string, error) { - queries := make([]string, 0, 4) - if strings.TrimSpace(dbName) != "" { - queries = append(queries, fmt.Sprintf("SHOW TABLES FROM `%s`", escapeBacktickIdent(dbName))) - queries = append(queries, fmt.Sprintf("SHOW STABLES FROM `%s`", escapeBacktickIdent(dbName))) - } - queries = append(queries, "SHOW TABLES") - queries = append(queries, "SHOW STABLES") + queries := tdengineShowTablesQueries(dbName) var lastErr error tableSet := make(map[string]struct{}) @@ -515,6 +509,35 @@ func escapeBacktickIdent(ident string) string { return strings.ReplaceAll(strings.TrimSpace(ident), "`", "``") } +func tdengineShowTablesQueries(dbName string) []string { + queries := make([]string, 0, 6) + appendQuery := func(query string) { + query = strings.TrimSpace(query) + if query == "" { + return + } + for _, existing := range queries { + if existing == query { + return + } + } + queries = append(queries, query) + } + + db := strings.TrimSpace(dbName) + if db != "" { + escaped := escapeBacktickIdent(db) + appendQuery(fmt.Sprintf("SHOW TABLES FROM `%s`", escaped)) + appendQuery(fmt.Sprintf("SHOW STABLES FROM `%s`", escaped)) + appendQuery(fmt.Sprintf("SHOW TABLES FROM %s", db)) + appendQuery(fmt.Sprintf("SHOW STABLES FROM %s", db)) + } + + appendQuery("SHOW TABLES") + appendQuery("SHOW STABLES") + return queries +} + func tdengineDescribeQueries(dbName, tableName string) []string { qualified := quoteTDengineTable(dbName, tableName) legacyQualified := quoteTDengineTableLegacy(dbName, tableName)