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
} disabled={batchBusy} loading={loadingInstallOrRemove} onClick={() => installDriver(row)}>
重装驱动
+ ) : versionSwitchPending ? (
+ } disabled={batchBusy} loading={loadingInstallOrRemove} onClick={() => installDriver(row)}>
+ 切换版本
+
) : row.connectable ? (
} disabled={batchBusy} loading={loadingInstallOrRemove} onClick={() => removeDriver(row)}>
移除
diff --git a/frontend/src/components/TableOverview.tdengine.test.tsx b/frontend/src/components/TableOverview.tdengine.test.tsx
new file mode 100644
index 0000000..89aa300
--- /dev/null
+++ b/frontend/src/components/TableOverview.tdengine.test.tsx
@@ -0,0 +1,162 @@
+import React from 'react';
+import { act, create, type ReactTestRenderer } from 'react-test-renderer';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import TableOverview from './TableOverview';
+
+const storeState = vi.hoisted(() => ({
+ theme: 'light',
+ appearance: { uiVersion: 'legacy' as const },
+ connections: [
+ {
+ id: 'conn-1',
+ config: {
+ type: 'tdengine',
+ host: '127.0.0.1',
+ port: 6041,
+ user: 'root',
+ password: 'taosdata',
+ database: 'metrics',
+ useSSH: false,
+ ssh: { host: '', port: 22, user: '', password: '', keyPath: '' },
+ },
+ },
+ ],
+ addTab: vi.fn(),
+ setActiveContext: vi.fn(),
+ setAIPanelVisible: vi.fn(),
+ addAIContext: vi.fn(),
+ pinnedSidebarTables: [] as string[],
+ setSidebarTablePinned: vi.fn(),
+}));
+
+const backendApp = vi.hoisted(() => ({
+ DBGetTables: vi.fn(),
+ DBQuery: vi.fn(),
+ DBShowCreateTable: vi.fn(),
+ ExportTable: vi.fn(),
+ DropTable: vi.fn(),
+ RenameTable: vi.fn(),
+}));
+
+const messageApi = vi.hoisted(() => ({
+ error: vi.fn(),
+}));
+
+vi.mock('../store', () => ({
+ useStore: (selector: (state: typeof storeState) => any) => selector(storeState),
+ buildSidebarTablePinKey: (connectionId: string, dbName: string, tableName: string, schemaName: string) =>
+ `${connectionId}:${dbName}:${schemaName}:${tableName}`,
+}));
+
+vi.mock('../../wailsjs/go/app/App', () => backendApp);
+vi.mock('../utils/autoFetchVisibility', () => ({
+ useAutoFetchVisibility: () => true,
+}));
+vi.mock('../utils/connectionRpcConfig', () => ({
+ buildRpcConnectionConfig: (config: unknown) => config,
+}));
+vi.mock('./V2TableContextMenu', () => ({
+ V2TableContextMenuView: () => null,
+}));
+
+vi.mock('@ant-design/icons', () => {
+ const Icon = () => ;
+ return {
+ TableOutlined: Icon,
+ SearchOutlined: Icon,
+ ReloadOutlined: Icon,
+ SortAscendingOutlined: Icon,
+ DatabaseOutlined: Icon,
+ ConsoleSqlOutlined: Icon,
+ EditOutlined: Icon,
+ CopyOutlined: Icon,
+ SaveOutlined: Icon,
+ DeleteOutlined: Icon,
+ ExportOutlined: Icon,
+ AppstoreOutlined: Icon,
+ UnorderedListOutlined: Icon,
+ WarningOutlined: Icon,
+ };
+});
+
+vi.mock('antd', () => {
+ const Button = ({ children, onClick, ...rest }: any) => ;
+ 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)