mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-05 10:01:30 +08:00
@@ -64,6 +64,18 @@ describe('ConnectionModal data source registry', () => {
|
||||
expect(source).toContain('return "http://127.0.0.1:6333";');
|
||||
expect(source).toContain('return "apiKey=...";');
|
||||
});
|
||||
|
||||
it('exposes Apache IoTDB in the create-connection picker with timeseries defaults', () => {
|
||||
expect(source).toContain("case 'iotdb':");
|
||||
expect(source).toContain('return 6667;');
|
||||
expect(source).toContain('iotdb: ["iotdb"]');
|
||||
expect(source).toContain("key: 'iotdb'");
|
||||
expect(source).toContain("name: 'Apache IoTDB'");
|
||||
expect(source).toContain('dbType === "iotdb"');
|
||||
expect(source).toContain("return 'Storage Group / Device / Timeseries';");
|
||||
expect(source).toContain('return "iotdb://root:root@127.0.0.1:6667/root.sg";');
|
||||
expect(source).toContain('return "fetchSize=1024&timeZone=Asia%2FShanghai";');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConnectionModal Redis Sentinel configuration', () => {
|
||||
|
||||
@@ -1873,6 +1873,9 @@ const ConnectionModal: React.FC<{
|
||||
if (dbType === "qdrant") {
|
||||
return "http://127.0.0.1:6333";
|
||||
}
|
||||
if (dbType === "iotdb") {
|
||||
return "iotdb://root:root@127.0.0.1:6667/root.sg";
|
||||
}
|
||||
if (dbType === "redis") {
|
||||
return "redis://:pass@127.0.0.1:6379,127.0.0.2:6379/0?topology=cluster 或 redis://:pass@10.0.0.1:26379,10.0.0.2:26379/0?topology=sentinel&master=mymaster";
|
||||
}
|
||||
@@ -1922,6 +1925,8 @@ const ConnectionModal: React.FC<{
|
||||
return "schema=SYSDBA";
|
||||
case "tdengine":
|
||||
return "timezone=Asia%2FShanghai";
|
||||
case "iotdb":
|
||||
return "fetchSize=1024&timeZone=Asia%2FShanghai";
|
||||
default:
|
||||
return "key=value&another=value";
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SavedConnection } from '../types';
|
||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues, resolveTextInputSafeBackdropFilter } from '../utils/appearance';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { quoteIdentPart, quoteQualifiedIdent } from '../utils/sql';
|
||||
import { formatLocalDateTimeLiteral, normalizeTemporalLiteralText } from './dataGridCopyInsert';
|
||||
import { buildDataSyncRequest, type SourceDatasetMode, validateDataSyncSelection } from './dataSyncRequest';
|
||||
const { Title, Text } = Typography;
|
||||
@@ -46,26 +47,11 @@ type TableOps = {
|
||||
type WorkflowType = 'sync' | 'migration';
|
||||
|
||||
const quoteSqlIdent = (dbType: string, ident: string): string => {
|
||||
const raw = String(ident || '').trim();
|
||||
if (!raw) return raw;
|
||||
const t = String(dbType || '').toLowerCase();
|
||||
if (t === 'mysql' || t === 'mariadb' || t === 'oceanbase' || t === 'diros' || t === 'starrocks' || t === 'sphinx' || t === 'clickhouse' || t === 'tdengine') {
|
||||
return `\`${raw.replace(/`/g, '``')}\``;
|
||||
}
|
||||
if (t === 'sqlserver') {
|
||||
return `[${raw.replace(/]/g, ']]')}]`;
|
||||
}
|
||||
return `"${raw.replace(/"/g, '""')}"`;
|
||||
return quoteIdentPart(dbType, String(ident || '').trim());
|
||||
};
|
||||
|
||||
const quoteSqlTable = (dbType: string, tableName: string): string => {
|
||||
const raw = String(tableName || '').trim();
|
||||
if (!raw) return raw;
|
||||
if (!raw.includes('.')) return quoteSqlIdent(dbType, raw);
|
||||
return raw
|
||||
.split('.')
|
||||
.map((part) => quoteSqlIdent(dbType, part))
|
||||
.join('.');
|
||||
return quoteQualifiedIdent(dbType, String(tableName || '').trim());
|
||||
};
|
||||
|
||||
const toSqlLiteral = (value: any, dbType: string): string => {
|
||||
|
||||
@@ -32,6 +32,13 @@ describe('DatabaseIcons', () => {
|
||||
expect(markup).toContain('>Qd</text>');
|
||||
});
|
||||
|
||||
it('includes Apache IoTDB in the selectable database icons', () => {
|
||||
expect(DB_ICON_TYPES).toContain('iotdb');
|
||||
expect(getDbIconLabel('iotdb')).toBe('Apache IoTDB');
|
||||
const markup = renderToStaticMarkup(<>{getDbIcon('iotdb', undefined, 22)}</>);
|
||||
expect(markup).toContain('>Io</text>');
|
||||
});
|
||||
|
||||
it('wraps database icons in a consistent frame for sidebar sizing', () => {
|
||||
const mysqlMarkup = renderToStaticMarkup(<>{getDbIcon('mysql', undefined, 22)}</>);
|
||||
const jvmMarkup = renderToStaticMarkup(<>{getDbIcon('jvm', undefined, 22)}</>);
|
||||
|
||||
@@ -49,6 +49,7 @@ const DB_DEFAULT_COLORS: Record<string, string> = {
|
||||
highgo: '#00A86B',
|
||||
iris: '#1F6FEB',
|
||||
tdengine: '#2962FF',
|
||||
iotdb: '#0F766E',
|
||||
chroma: '#7C3AED',
|
||||
qdrant: '#DC244C',
|
||||
diros: '#0050B3',
|
||||
@@ -180,6 +181,9 @@ const IrisIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
|
||||
const TDengineIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
|
||||
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.tdengine} label="TD" />
|
||||
);
|
||||
const IoTDBIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
|
||||
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.iotdb} label="Io" />
|
||||
);
|
||||
const ChromaIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
|
||||
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.chroma} label="Ch" />
|
||||
);
|
||||
@@ -239,6 +243,7 @@ const DB_ICON_MAP: Record<string, React.FC<DbIconProps>> = {
|
||||
highgo: HighGoIcon,
|
||||
iris: IrisIcon,
|
||||
tdengine: TDengineIcon,
|
||||
iotdb: IoTDBIcon,
|
||||
chroma: ChromaIcon,
|
||||
qdrant: QdrantIcon,
|
||||
elasticsearch: ElasticsearchIcon,
|
||||
@@ -249,7 +254,7 @@ const DB_ICON_MAP: Record<string, React.FC<DbIconProps>> = {
|
||||
export const DB_ICON_TYPES: string[] = [
|
||||
'mysql', 'mariadb', 'oceanbase', 'postgres', 'redis', 'mongodb', 'jvm',
|
||||
'oracle', 'sqlserver', 'sqlite', 'duckdb', 'clickhouse', 'starrocks',
|
||||
'kingbase', 'dameng', 'vastbase', 'opengauss', 'highgo', 'iris', 'tdengine', 'chroma', 'qdrant', 'elasticsearch', 'custom',
|
||||
'kingbase', 'dameng', 'vastbase', 'opengauss', 'highgo', 'iris', 'tdengine', 'iotdb', 'chroma', 'qdrant', 'elasticsearch', 'custom',
|
||||
];
|
||||
|
||||
/** 该类型是否有品牌 SVG 文件 */
|
||||
@@ -271,7 +276,7 @@ export const getDbIconLabel = (type: string): string => {
|
||||
sqlserver: 'SQL Server', clickhouse: 'ClickHouse', sqlite: 'SQLite',
|
||||
starrocks: 'StarRocks',
|
||||
duckdb: 'DuckDB', kingbase: '金仓', dameng: '达梦',
|
||||
vastbase: 'VastBase', opengauss: 'OpenGauss', highgo: '瀚高', iris: 'InterSystems IRIS', tdengine: 'TDengine',
|
||||
vastbase: 'VastBase', opengauss: 'OpenGauss', highgo: '瀚高', iris: 'InterSystems IRIS', tdengine: 'TDengine', iotdb: 'Apache IoTDB',
|
||||
chroma: 'Chroma',
|
||||
qdrant: 'Qdrant',
|
||||
elasticsearch: 'Elasticsearch',
|
||||
|
||||
@@ -2540,16 +2540,16 @@ const Sidebar: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
const isNonRelationalDbType = (connectionId: string): boolean => {
|
||||
const isStructureOnlyDbType = (connectionId: string): boolean => {
|
||||
const conn = connections.find(c => c.id === connectionId);
|
||||
if (!conn) return false;
|
||||
const dbType = resolveDataSourceType(conn.config);
|
||||
return dbType === 'elasticsearch' || dbType === 'mongodb' || dbType === 'redis';
|
||||
return dbType === 'elasticsearch' || dbType === 'mongodb' || dbType === 'redis' || dbType === 'iotdb';
|
||||
};
|
||||
|
||||
const openDesign = (node: any, initialTab: string, readOnly: boolean = false) => {
|
||||
const { tableName, dbName, id } = node.dataRef;
|
||||
const forceReadOnly = readOnly || isNonRelationalDbType(id);
|
||||
const forceReadOnly = readOnly || isStructureOnlyDbType(id);
|
||||
addTab({
|
||||
id: `design-${id}-${dbName}-${tableName}`,
|
||||
title: `${forceReadOnly ? '表结构' : '设计表'} (${tableName})`,
|
||||
@@ -2564,6 +2564,10 @@ const Sidebar: React.FC<{
|
||||
|
||||
const openNewTableDesign = (node: any) => {
|
||||
const { dbName, id } = node.dataRef;
|
||||
if (isStructureOnlyDbType(id)) {
|
||||
message.warning('当前数据源暂不支持可视化新建表');
|
||||
return;
|
||||
}
|
||||
addTab({
|
||||
id: `new-table-${id}-${dbName}-${Date.now()}`,
|
||||
title: `新建表 - ${dbName}`,
|
||||
@@ -6405,14 +6409,15 @@ const Sidebar: React.FC<{
|
||||
const groupData = node.dataRef; // { ...conn, dbName, groupKey }
|
||||
const sortPreferenceKey = `${groupData.id}-${groupData.dbName}`;
|
||||
const currentSort = tableSortPreference[sortPreferenceKey] || 'name';
|
||||
const canCreateTable = !isStructureOnlyDbType(String(groupData.id || ''));
|
||||
|
||||
return [
|
||||
{
|
||||
...(canCreateTable ? [{
|
||||
key: 'new-table',
|
||||
label: '新建表',
|
||||
icon: <TableOutlined />,
|
||||
onClick: () => openNewTableDesign(node)
|
||||
},
|
||||
}] : []),
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: 'sort-by-name',
|
||||
@@ -6845,13 +6850,14 @@ const Sidebar: React.FC<{
|
||||
const capabilities = getDataSourceCapabilities(databaseConn?.config);
|
||||
const isStarRocks = dialect === 'starrocks';
|
||||
const supportsSchemaActions = isPostgresSchemaDialect(dialect);
|
||||
const canCreateTable = !isStructureOnlyDbType(String(databaseConn?.id || ''));
|
||||
return [
|
||||
{
|
||||
...(canCreateTable ? [{
|
||||
key: 'new-table',
|
||||
label: '新建表',
|
||||
icon: <TableOutlined />,
|
||||
onClick: () => openNewTableDesign(node)
|
||||
},
|
||||
}] : []),
|
||||
...(supportsSchemaActions ? [
|
||||
{
|
||||
key: 'new-schema',
|
||||
@@ -7116,7 +7122,7 @@ const Sidebar: React.FC<{
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: 'design-table',
|
||||
label: '设计表',
|
||||
label: isStructureOnlyDbType(String(node.dataRef?.id || '')) ? '表结构' : '设计表',
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => openDesign(node, 'columns', false)
|
||||
},
|
||||
|
||||
@@ -132,6 +132,11 @@ const getMetadataDialect = (connType: string, driver?: string, oceanBaseProtocol
|
||||
|
||||
const buildTableStatusSQL = (dialect: string, dbName: string, schemaName?: string): string => {
|
||||
const escapeLiteral = (s: string) => s.replace(/'/g, "''");
|
||||
const iotdbDevicePattern = (name: string) => {
|
||||
const normalized = String(name || '').trim().replace(/[`"]/g, '');
|
||||
if (!normalized) return '';
|
||||
return normalized.endsWith('.**') ? normalized : `${normalized}.**`;
|
||||
};
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
@@ -190,6 +195,10 @@ ORDER BY s.name, t.name`;
|
||||
return `SELECT name AS table_name, comment AS table_comment, total_rows AS table_rows, total_bytes AS data_length, 0 AS index_length FROM system.tables WHERE database = '${escapeLiteral(dbName)}' AND engine NOT IN ('View', 'MaterializedView') ORDER BY name`;
|
||||
case 'tdengine':
|
||||
return `SHOW TABLES FROM \`${dbName.replace(/`/g, '``')}\``;
|
||||
case 'iotdb': {
|
||||
const pattern = iotdbDevicePattern(dbName);
|
||||
return pattern ? `SHOW DEVICES ${pattern}` : 'SHOW DEVICES';
|
||||
}
|
||||
case 'dm':
|
||||
case 'oracle': {
|
||||
const owner = (schemaName || dbName).toUpperCase();
|
||||
@@ -219,7 +228,7 @@ const parseTableStats = (dialect: string, rows: Record<string, any>[]): TableSta
|
||||
};
|
||||
|
||||
return {
|
||||
name: strVal(['Name', 'name', 'table_name', 'tablename', 'TABLE_NAME']),
|
||||
name: strVal(['Name', 'name', 'table_name', 'tablename', 'TABLE_NAME', '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']),
|
||||
@@ -263,6 +272,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
[connection?.config?.driver, connection?.config?.oceanBaseProtocol, connection?.config?.type]
|
||||
);
|
||||
const schemaName = String((tab as any).schemaName || '').trim();
|
||||
const supportsDesignWrite = metadataDialect !== 'iotdb';
|
||||
const autoFetchVisible = useAutoFetchVisibility();
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
@@ -415,17 +425,18 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
const openDesign = useCallback((tableName: string) => {
|
||||
if (!connection) return;
|
||||
setActiveContext({ connectionId: connection.id, dbName: tab.dbName || '' });
|
||||
const structureOnly = !supportsDesignWrite;
|
||||
addTab({
|
||||
id: `design-${connection.id}-${tab.dbName}-${tableName}`,
|
||||
title: `设计表 (${tableName})`,
|
||||
title: `${structureOnly ? '表结构' : '设计表'} (${tableName})`,
|
||||
type: 'design',
|
||||
connectionId: connection.id,
|
||||
dbName: tab.dbName,
|
||||
tableName,
|
||||
initialTab: 'columns',
|
||||
readOnly: false,
|
||||
readOnly: structureOnly,
|
||||
});
|
||||
}, [connection, tab.dbName, addTab, setActiveContext]);
|
||||
}, [connection, tab.dbName, addTab, setActiveContext, supportsDesignWrite]);
|
||||
|
||||
const openTableDdl = useCallback((tableName: string) => {
|
||||
if (!connection) return;
|
||||
@@ -827,7 +838,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
const buildLegacyTableContextMenuItems = useCallback((table: TableStatRow): MenuProps['items'] => [
|
||||
{ key: 'new-query', label: '新建查询', icon: <ConsoleSqlOutlined />, onClick: () => openQueryForTable(table.name) },
|
||||
{ type: 'divider' },
|
||||
{ key: 'design-table', label: '设计表', icon: <EditOutlined />, onClick: () => openDesign(table.name) },
|
||||
{ key: 'design-table', label: supportsDesignWrite ? '设计表' : '表结构', icon: <EditOutlined />, onClick: () => openDesign(table.name) },
|
||||
{ key: 'copy-table-name', label: '复制表名', icon: <CopyOutlined />, onClick: () => handleCopyTableName(table.name) },
|
||||
{ key: 'copy-structure', label: '复制表结构', icon: <CopyOutlined />, onClick: () => handleCopyStructure(table.name) },
|
||||
{ key: 'backup-table', label: '备份表 (SQL)', icon: <SaveOutlined />, onClick: () => handleExport(table.name, 'sql') },
|
||||
@@ -855,6 +866,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
|
||||
handleTableDataDangerAction,
|
||||
openDesign,
|
||||
openQueryForTable,
|
||||
supportsDesignWrite,
|
||||
]);
|
||||
|
||||
const renderOverviewSectionTitle = (section: OverviewTableSection) => (
|
||||
|
||||
Reference in New Issue
Block a user