mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-06 18:41:42 +08:00
✨ feat(elasticsearch): 生产级 ES 驱动 — 写入支持、分页、多版本兼容
- go-elasticsearch/v8 官方 SDK,支持 ES 6.x/7.x/8.x - SQL → ES DSL 转换、DevTools 查询、JSON DSL - _bulk API 批量写入(INSERT/UPDATE/DELETE) - 别名写入、精确分页、COUNT 统计 - API Key / SSH / SSL 连接支持 - 表设计器只读适配、ES 品牌图标 - 30+ 单元测试全部通过
This commit is contained in:
@@ -284,12 +284,12 @@ const singleHostUriSchemesByType: Record<string, string[]> = {
|
||||
sqlserver: ["sqlserver"],
|
||||
iris: ["iris", "intersystems"],
|
||||
redis: ["redis"],
|
||||
elasticsearch: ["http", "https"],
|
||||
tdengine: ["tdengine"],
|
||||
dameng: ["dameng", "dm"],
|
||||
kingbase: ["kingbase"],
|
||||
highgo: ["highgo"],
|
||||
vastbase: ["vastbase"],
|
||||
elasticsearch: ["http", "https"],
|
||||
};
|
||||
|
||||
const sslSupportedTypes = new Set([
|
||||
@@ -311,8 +311,8 @@ const sslSupportedTypes = new Set([
|
||||
"opengauss",
|
||||
"mongodb",
|
||||
"redis",
|
||||
"elasticsearch",
|
||||
"tdengine",
|
||||
"elasticsearch",
|
||||
]);
|
||||
|
||||
const supportsSSLForType = (type: string) =>
|
||||
@@ -357,7 +357,6 @@ const sslClientCertificateSupportedTypes = new Set([
|
||||
"opengauss",
|
||||
"mongodb",
|
||||
"redis",
|
||||
"elasticsearch",
|
||||
]);
|
||||
|
||||
const supportsSSLCAPathForType = (type: string) =>
|
||||
@@ -411,9 +410,9 @@ const supportsConnectionParamsForType = (type: string) =>
|
||||
type === "iris" ||
|
||||
type === "clickhouse" ||
|
||||
type === "mongodb" ||
|
||||
type === "elasticsearch" ||
|
||||
type === "dameng" ||
|
||||
type === "tdengine";
|
||||
type === "tdengine" ||
|
||||
type === "elasticsearch";
|
||||
|
||||
type DriverStatusSnapshot = {
|
||||
type: string;
|
||||
@@ -431,6 +430,7 @@ const normalizeDriverType = (value: string): string => {
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (normalized === "postgresql") return "postgres";
|
||||
if (normalized === "elastic") return "elasticsearch";
|
||||
if (normalized === "doris") return "diros";
|
||||
if (
|
||||
normalized === "intersystems" ||
|
||||
@@ -1974,15 +1974,6 @@ const ConnectionModal: React.FC<{
|
||||
parsedValues.useSSL = false;
|
||||
parsedValues.sslMode = "disable";
|
||||
}
|
||||
} else if (type === "elasticsearch") {
|
||||
const isHTTPS = trimmedUri.toLowerCase().startsWith("https://");
|
||||
const skipVerify = normalizeBool(parsed.params.get("skip_verify"));
|
||||
parsedValues.useSSL = isHTTPS;
|
||||
parsedValues.sslMode = isHTTPS
|
||||
? skipVerify
|
||||
? "skip-verify"
|
||||
: "required"
|
||||
: "disable";
|
||||
}
|
||||
}
|
||||
return parsedValues;
|
||||
@@ -2048,9 +2039,6 @@ const ConnectionModal: React.FC<{
|
||||
if (dbType === "redis") {
|
||||
return "redis://:pass@127.0.0.1:6379,127.0.0.2:6379/0?topology=cluster";
|
||||
}
|
||||
if (dbType === "elasticsearch") {
|
||||
return "http://elastic:pass@127.0.0.1:9200/logs-*";
|
||||
}
|
||||
if (dbType === "oracle") {
|
||||
return "oracle://user:pass@127.0.0.1:1521/ORCLPDB1";
|
||||
}
|
||||
@@ -2270,10 +2258,6 @@ const ConnectionModal: React.FC<{
|
||||
? values.useSSL
|
||||
? "https"
|
||||
: "http"
|
||||
: type === "elasticsearch"
|
||||
? values.useSSL
|
||||
? "https"
|
||||
: "http"
|
||||
: type;
|
||||
const dbPath = database ? `/${encodeURIComponent(database)}` : "";
|
||||
const params = new URLSearchParams();
|
||||
@@ -2320,11 +2304,6 @@ const ConnectionModal: React.FC<{
|
||||
if (mode === "skip-verify" || mode === "preferred") {
|
||||
params.set("skip_verify", "true");
|
||||
}
|
||||
} else if (type === "elasticsearch") {
|
||||
if (mode === "skip-verify" || mode === "preferred") {
|
||||
params.set("skip_verify", "true");
|
||||
}
|
||||
appendSSLPathParamsForUri(params, type, values);
|
||||
}
|
||||
} else if (supportsSSLForType(type)) {
|
||||
if (isPostgresCompatibleSSLType(type)) {
|
||||
@@ -3841,13 +3820,7 @@ const ConnectionModal: React.FC<{
|
||||
});
|
||||
} else if (type !== "custom") {
|
||||
const defaultUser =
|
||||
type === "clickhouse"
|
||||
? "default"
|
||||
: type === "redis"
|
||||
? ""
|
||||
: type === "elasticsearch"
|
||||
? "elastic"
|
||||
: "root";
|
||||
type === "clickhouse" ? "default" : (type === "redis" || type === "elasticsearch") ? "" : "root";
|
||||
const sslCapableType = supportsSSLForType(type);
|
||||
setUseSSL(false);
|
||||
setUseHttpTunnel(false);
|
||||
@@ -4085,7 +4058,7 @@ const ConnectionModal: React.FC<{
|
||||
case "mongodb":
|
||||
return "单机 / 副本集";
|
||||
case "elasticsearch":
|
||||
return "索引 / JSON DSL";
|
||||
return "支持索引浏览、Mapping 检查、JSON DSL 和 query_string 查询";
|
||||
case "oceanbase":
|
||||
return "MySQL / Oracle 租户";
|
||||
case "sqlite":
|
||||
@@ -5128,25 +5101,6 @@ const ConnectionModal: React.FC<{
|
||||
),
|
||||
})}
|
||||
|
||||
{dbType === "elasticsearch" &&
|
||||
renderConfigSectionCard({
|
||||
sectionKey: "service",
|
||||
icon: <DatabaseOutlined />,
|
||||
children: (
|
||||
<Form.Item
|
||||
name="database"
|
||||
label="默认索引(可选)"
|
||||
help="留空时 JSON DSL 和 query_string 会默认查询所有可见索引;也可以填写 logs-* 这类索引通配符。"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
placeholder="例如:logs-*"
|
||||
/>
|
||||
</Form.Item>
|
||||
),
|
||||
})}
|
||||
|
||||
{(dbType === "oracle" || isOceanBaseOracle) &&
|
||||
renderConfigSectionCard({
|
||||
sectionKey: "service",
|
||||
@@ -5677,13 +5631,13 @@ const ConnectionModal: React.FC<{
|
||||
name="user"
|
||||
label="用户名"
|
||||
rules={
|
||||
dbType === "mongodb"
|
||||
(dbType === "mongodb" || dbType === "elasticsearch")
|
||||
? []
|
||||
: [createUriAwareRequiredRule("请输入用户名")]
|
||||
}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Input {...noAutoCapInputProps} />
|
||||
<Input {...noAutoCapInputProps} placeholder={dbType === "elasticsearch" ? "未开启认证可留空" : undefined} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
@@ -5763,25 +5717,13 @@ const ConnectionModal: React.FC<{
|
||||
children: (
|
||||
<Form.Item
|
||||
name="includeDatabases"
|
||||
label={
|
||||
dbType === "elasticsearch"
|
||||
? "显示索引 (留空显示全部)"
|
||||
: "显示数据库 (留空显示全部)"
|
||||
}
|
||||
help={
|
||||
dbType === "elasticsearch"
|
||||
? "连接测试成功后可选择需要展示的索引"
|
||||
: "连接测试成功后可选择"
|
||||
}
|
||||
label="显示数据库 (留空显示全部)"
|
||||
help="连接测试成功后可选择"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={
|
||||
dbType === "elasticsearch"
|
||||
? "选择显示的索引"
|
||||
: "选择显示的数据库"
|
||||
}
|
||||
placeholder="选择显示的数据库"
|
||||
allowClear
|
||||
>
|
||||
{dbList.map((db) => (
|
||||
|
||||
@@ -35,7 +35,7 @@ const DB_DEFAULT_COLORS: Record<string, string> = {
|
||||
postgres: '#336791',
|
||||
redis: '#DC382D',
|
||||
mongodb: '#47A248',
|
||||
elasticsearch: '#005571',
|
||||
elasticsearch: '#FEC514',
|
||||
jvm: '#1677FF',
|
||||
kingbase: '#1890FF',
|
||||
dameng: '#E6002D',
|
||||
@@ -62,7 +62,7 @@ export const getDbDefaultColor = (type: string): string =>
|
||||
|
||||
const BRAND_SVG_TYPES = new Set([
|
||||
'mysql', 'mariadb', 'postgres', 'redis', 'mongodb', 'clickhouse', 'sqlite',
|
||||
'diros', 'sphinx', 'duckdb', 'sqlserver',
|
||||
'diros', 'sphinx', 'duckdb', 'sqlserver', 'elasticsearch',
|
||||
]);
|
||||
|
||||
/** 品牌 SVG 图标:用 <img> 加载 /db-icons/*.svg */
|
||||
@@ -131,9 +131,6 @@ const RedisIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
|
||||
const MongoDBIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
|
||||
<BrandSvgIcon type="mongodb" size={size} color={color} />
|
||||
);
|
||||
const ElasticsearchIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
|
||||
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.elasticsearch} label="ES" />
|
||||
);
|
||||
const ClickHouseIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
|
||||
<BrandSvgIcon type="clickhouse" size={size} color={color} />
|
||||
);
|
||||
@@ -184,6 +181,9 @@ const TDengineIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
|
||||
const JVMIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
|
||||
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.jvm} label="JVM" />
|
||||
);
|
||||
const ElasticsearchIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
|
||||
<BrandSvgIcon type="elasticsearch" size={size} color={color} />
|
||||
);
|
||||
|
||||
/** Custom — 齿轮图标 */
|
||||
const CustomIcon: React.FC<DbIconProps> = ({ size = 16, color }) => {
|
||||
@@ -218,7 +218,6 @@ const DB_ICON_MAP: Record<string, React.FC<DbIconProps>> = {
|
||||
postgres: PostgresIcon,
|
||||
redis: RedisIcon,
|
||||
mongodb: MongoDBIcon,
|
||||
elasticsearch: ElasticsearchIcon,
|
||||
jvm: JVMIcon,
|
||||
kingbase: KingBaseIcon,
|
||||
dameng: DamengIcon,
|
||||
@@ -232,14 +231,15 @@ const DB_ICON_MAP: Record<string, React.FC<DbIconProps>> = {
|
||||
highgo: HighGoIcon,
|
||||
iris: IrisIcon,
|
||||
tdengine: TDengineIcon,
|
||||
elasticsearch: ElasticsearchIcon,
|
||||
custom: CustomIcon,
|
||||
};
|
||||
|
||||
/** 可选图标类型列表(用于图标选择器 UI) */
|
||||
export const DB_ICON_TYPES: string[] = [
|
||||
'mysql', 'mariadb', 'oceanbase', 'postgres', 'redis', 'mongodb', 'elasticsearch', 'jvm',
|
||||
'mysql', 'mariadb', 'oceanbase', 'postgres', 'redis', 'mongodb', 'jvm',
|
||||
'oracle', 'sqlserver', 'sqlite', 'duckdb', 'clickhouse', 'starrocks',
|
||||
'kingbase', 'dameng', 'vastbase', 'opengauss', 'highgo', 'iris', 'tdengine', 'custom',
|
||||
'kingbase', 'dameng', 'vastbase', 'opengauss', 'highgo', 'iris', 'tdengine', 'elasticsearch', 'custom',
|
||||
];
|
||||
|
||||
/** 该类型是否有品牌 SVG 文件 */
|
||||
@@ -256,12 +256,13 @@ export const getDbIcon = (type: string, color?: string, size?: number): React.Re
|
||||
export const getDbIconLabel = (type: string): string => {
|
||||
const labels: Record<string, string> = {
|
||||
mysql: 'MySQL', mariadb: 'MariaDB', oceanbase: 'OceanBase', postgres: 'PostgreSQL',
|
||||
redis: 'Redis', mongodb: 'MongoDB', elasticsearch: 'Elasticsearch', jvm: 'JVM',
|
||||
redis: 'Redis', mongodb: 'MongoDB', jvm: 'JVM',
|
||||
oracle: 'Oracle',
|
||||
sqlserver: 'SQL Server', clickhouse: 'ClickHouse', sqlite: 'SQLite',
|
||||
starrocks: 'StarRocks',
|
||||
duckdb: 'DuckDB', kingbase: '金仓', dameng: '达梦',
|
||||
vastbase: 'VastBase', opengauss: 'OpenGauss', highgo: '瀚高', iris: 'InterSystems IRIS', tdengine: 'TDengine',
|
||||
elasticsearch: 'Elasticsearch',
|
||||
custom: '自定义',
|
||||
};
|
||||
return labels[type?.toLowerCase()] || type;
|
||||
|
||||
@@ -62,7 +62,7 @@ import { getTableDataDangerActionMeta, supportsTableTruncateAction, type TableDa
|
||||
import { useAutoFetchVisibility } from '../utils/autoFetchVisibility';
|
||||
import FindInDatabaseModal from './FindInDatabaseModal';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { getDataSourceCapabilities } from '../utils/dataSourceCapabilities';
|
||||
import { getDataSourceCapabilities, resolveDataSourceType } from '../utils/dataSourceCapabilities';
|
||||
import { noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
import { normalizeSidebarViewName, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata';
|
||||
import { splitQualifiedNameLast } from '../utils/qualifiedName';
|
||||
@@ -935,6 +935,7 @@ const DRIVER_STATUS_CACHE_TTL_MS = 30_000;
|
||||
const normalizeDriverType = (value: string): string => {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === 'postgresql' || normalized === 'pg' || normalized === 'pq' || normalized === 'pgx') return 'postgres';
|
||||
if (normalized === 'elastic') return 'elasticsearch';
|
||||
if (normalized === 'doris') return 'diros';
|
||||
if (
|
||||
normalized === 'open_gauss' ||
|
||||
@@ -3279,17 +3280,25 @@ const Sidebar: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
const isNonRelationalDbType = (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';
|
||||
};
|
||||
|
||||
const openDesign = (node: any, initialTab: string, readOnly: boolean = false) => {
|
||||
const { tableName, dbName, id } = node.dataRef;
|
||||
const forceReadOnly = readOnly || isNonRelationalDbType(id);
|
||||
addTab({
|
||||
id: `design-${id}-${dbName}-${tableName}`,
|
||||
title: `${readOnly ? '表结构' : '设计表'} (${tableName})`,
|
||||
title: `${forceReadOnly ? '表结构' : '设计表'} (${tableName})`,
|
||||
type: 'design',
|
||||
connectionId: id,
|
||||
dbName: dbName,
|
||||
tableName: tableName,
|
||||
initialTab: initialTab,
|
||||
readOnly: readOnly
|
||||
readOnly: forceReadOnly
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1403,7 +1403,7 @@ ${selectedTrigger.statement}`;
|
||||
const isOracleLikeDialect = (dbType: string): boolean => isOracleLikeSqlDialect(dbType);
|
||||
const isSqlServerDialect = (dbType: string): boolean => isSqlServerSqlDialect(dbType);
|
||||
const isMysqlLikeDialect = (dbType: string): boolean => isMysqlFamilySqlDialect(dbType);
|
||||
const isNonRelationalDialect = (dbType: string): boolean => dbType === 'redis' || dbType === 'mongodb';
|
||||
const isNonRelationalDialect = (dbType: string): boolean => dbType === 'redis' || dbType === 'mongodb' || dbType === 'elasticsearch';
|
||||
const lacksAlterForeignKeySupport = (dbType: string): boolean => dbType === 'sqlite' || dbType === 'duckdb' || dbType === 'tdengine';
|
||||
const lacksTableCommentSupport = (dbType: string): boolean => dbType === 'sqlite';
|
||||
|
||||
@@ -2885,9 +2885,13 @@ END;`;
|
||||
>
|
||||
查看语句
|
||||
</Button>
|
||||
<Button size="small" icon={<PlusOutlined />} onClick={handleCreateTrigger}>新增</Button>
|
||||
<Button size="small" icon={<EditOutlined />} disabled={!selectedTrigger} onClick={handleEditTrigger}>修改</Button>
|
||||
<Button size="small" icon={<DeleteOutlined />} danger disabled={!selectedTrigger} onClick={handleDeleteTrigger}>删除</Button>
|
||||
{!readOnly && (
|
||||
<>
|
||||
<Button size="small" icon={<PlusOutlined />} onClick={handleCreateTrigger}>新增</Button>
|
||||
<Button size="small" icon={<EditOutlined />} disabled={!selectedTrigger} onClick={handleEditTrigger}>修改</Button>
|
||||
<Button size="small" icon={<DeleteOutlined />} danger disabled={!selectedTrigger} onClick={handleDeleteTrigger}>删除</Button>
|
||||
</>
|
||||
)}
|
||||
<span style={{ marginLeft: 'auto', color: '#888', fontSize: 12, alignSelf: 'center' }}>
|
||||
{selectedTrigger ? `已选择: ${selectedTrigger.name}` : '请点击选择触发器'}
|
||||
</span>
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface BuildIndexCreateSqlResult {
|
||||
severity?: 'error' | 'warning';
|
||||
}
|
||||
|
||||
const isNonRelationalDialect = (dbType: string): boolean => dbType === 'redis' || dbType === 'mongodb';
|
||||
const isNonRelationalDialect = (dbType: string): boolean => dbType === 'redis' || dbType === 'mongodb' || dbType === 'elasticsearch';
|
||||
|
||||
export const buildIndexCreateSqlPreview = (input: BuildIndexCreateSqlInput): BuildIndexCreateSqlResult => {
|
||||
const dbType = input.dbType;
|
||||
|
||||
Reference in New Issue
Block a user