mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-21 12:51:47 +08:00
@@ -385,6 +385,7 @@ const ConnectionModal: React.FC<{
|
||||
);
|
||||
const disableLocalBackdropFilter = isMacLikePlatform();
|
||||
const mysqlTopology = Form.useWatch("mysqlTopology", form) || "single";
|
||||
const kafkaTopology = Form.useWatch("kafkaTopology", form) || "single";
|
||||
const mongoTopology = Form.useWatch("mongoTopology", form) || "single";
|
||||
const mongoSrv = Form.useWatch("mongoSrv", form) || false;
|
||||
const redisTopology = Form.useWatch("redisTopology", form) || "single";
|
||||
@@ -418,6 +419,7 @@ const ConnectionModal: React.FC<{
|
||||
);
|
||||
const isOceanBaseOracle = dbType === "oceanbase" && oceanBaseProtocol === "oracle";
|
||||
const isMySQLLike = isMySQLCompatibleType(dbType) && !isOceanBaseOracle;
|
||||
const isKafka = dbType === "kafka";
|
||||
const supportsConnectionParams = supportsConnectionParamsForType(dbType);
|
||||
const isSSLType = supportsSSLForType(dbType);
|
||||
const supportsSSLCAPath = supportsSSLCAPathForType(dbType);
|
||||
@@ -1629,6 +1631,62 @@ const ConnectionModal: React.FC<{
|
||||
};
|
||||
}
|
||||
|
||||
if (type === "kafka") {
|
||||
const defaultPort = getDefaultPortByType(type);
|
||||
const parsed =
|
||||
parseMultiHostUri(trimmedUri, "kafka") ||
|
||||
parseMultiHostUri(trimmedUri, "apache-kafka") ||
|
||||
parseMultiHostUri(trimmedUri, "apache_kafka");
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
if (!parsed.hosts.length || parsed.hosts.length > MAX_URI_HOSTS) {
|
||||
return null;
|
||||
}
|
||||
if (parsed.hosts.some((entry) => !isValidUriHostEntry(entry))) {
|
||||
return null;
|
||||
}
|
||||
const hostList = normalizeAddressList(parsed.hosts, defaultPort);
|
||||
if (!hostList.length) {
|
||||
return null;
|
||||
}
|
||||
const primary = parseHostPort(
|
||||
hostList[0] || `localhost:${defaultPort}`,
|
||||
defaultPort,
|
||||
);
|
||||
const tlsEnabled = normalizeUriBool(
|
||||
parsed.params.get("tls") ||
|
||||
parsed.params.get("ssl") ||
|
||||
parsed.params.get("useSSL") ||
|
||||
parsed.params.get("use_ssl"),
|
||||
);
|
||||
const skipVerify = normalizeUriBool(
|
||||
parsed.params.get("skip_verify") || parsed.params.get("skipVerify"),
|
||||
);
|
||||
const topology = String(parsed.params.get("topology") || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const timeoutValue = Number(parsed.params.get("timeout"));
|
||||
return {
|
||||
host: primary?.host || "localhost",
|
||||
port: primary?.port || defaultPort,
|
||||
user: parsed.username,
|
||||
password: parsed.password,
|
||||
database: parsed.database || "",
|
||||
useSSL: tlsEnabled,
|
||||
sslMode: tlsEnabled ? (skipVerify ? "skip-verify" : "required") : "disable",
|
||||
...extractSSLPathValuesFromParams(parsed.params, type),
|
||||
kafkaTopology:
|
||||
topology === "cluster" || hostList.length > 1 ? "cluster" : "single",
|
||||
kafkaHosts: hostList.slice(1),
|
||||
connectionParams: serializeConnectionParams(parsed.params),
|
||||
timeout:
|
||||
Number.isFinite(timeoutValue) && timeoutValue > 0
|
||||
? Math.min(MAX_TIMEOUT_SECONDS, Math.trunc(timeoutValue))
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (type === "clickhouse") {
|
||||
const httpValues = parseClickHouseHTTPUriToValues(trimmedUri);
|
||||
if (httpValues) {
|
||||
@@ -1877,6 +1935,9 @@ const ConnectionModal: React.FC<{
|
||||
if (dbType === "iotdb") {
|
||||
return "iotdb://root:root@127.0.0.1:6667/root.sg";
|
||||
}
|
||||
if (dbType === "kafka") {
|
||||
return "kafka://user:pass@127.0.0.1:9092,127.0.0.2:9092/orders.events?topology=cluster&groupId=analytics&mechanism=scram-sha-256";
|
||||
}
|
||||
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";
|
||||
}
|
||||
@@ -1932,6 +1993,8 @@ const ConnectionModal: React.FC<{
|
||||
return "timezone=Asia%2FShanghai";
|
||||
case "iotdb":
|
||||
return "fetchSize=1024&timeZone=Asia%2FShanghai";
|
||||
case "kafka":
|
||||
return "groupId=gonavi&mechanism=scram-sha-256&clientId=gonavi-desktop&startOffset=latest";
|
||||
default:
|
||||
return "key=value&another=value";
|
||||
}
|
||||
@@ -1994,6 +2057,36 @@ const ConnectionModal: React.FC<{
|
||||
return `${scheme}://${encodedAuth}${hosts.join(",")}${dbPath}${query ? `?${query}` : ""}`;
|
||||
}
|
||||
|
||||
if (type === "kafka") {
|
||||
const primary = toAddress(host, port, defaultPort);
|
||||
const brokers =
|
||||
values.kafkaTopology === "cluster"
|
||||
? normalizeAddressList(values.kafkaHosts, defaultPort)
|
||||
: [];
|
||||
const allBrokers = normalizeAddressList([primary, ...brokers], defaultPort);
|
||||
const params = new URLSearchParams();
|
||||
if (allBrokers.length > 1 || values.kafkaTopology === "cluster") {
|
||||
params.set("topology", "cluster");
|
||||
}
|
||||
if (values.useSSL) {
|
||||
const mode = String(values.sslMode || "preferred")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
params.set("tls", "true");
|
||||
if (mode === "skip-verify" || mode === "preferred") {
|
||||
params.set("skip_verify", "true");
|
||||
}
|
||||
appendSSLPathParamsForUri(params, type, values);
|
||||
}
|
||||
if (Number.isFinite(timeout) && timeout > 0) {
|
||||
params.set("timeout", String(timeout));
|
||||
}
|
||||
mergeConnectionParams(params, values.connectionParams);
|
||||
const topicPath = database ? `/${encodeURIComponent(database)}` : "";
|
||||
const query = params.toString();
|
||||
return `kafka://${encodedAuth}${allBrokers.join(",")}${topicPath}${query ? `?${query}` : ""}`;
|
||||
}
|
||||
|
||||
if (type === "redis") {
|
||||
return buildRedisUriFromValues(values);
|
||||
}
|
||||
@@ -2364,6 +2457,8 @@ const ConnectionModal: React.FC<{
|
||||
configType === "sphinx"
|
||||
? normalizedHosts.slice(1)
|
||||
: [];
|
||||
const kafkaHosts =
|
||||
configType === "kafka" ? normalizedHosts.slice(1) : [];
|
||||
const mongoHosts =
|
||||
configType === "mongodb" ? normalizedHosts.slice(1) : [];
|
||||
const redisHosts =
|
||||
@@ -2371,6 +2466,9 @@ const ConnectionModal: React.FC<{
|
||||
const mysqlIsReplica =
|
||||
String(config.topology || "").toLowerCase() === "replica" ||
|
||||
mysqlReplicaHosts.length > 0;
|
||||
const kafkaIsCluster =
|
||||
String(config.topology || "").toLowerCase() === "cluster" ||
|
||||
kafkaHosts.length > 0;
|
||||
const mongoIsReplica =
|
||||
String(config.topology || "").toLowerCase() === "replica" ||
|
||||
mongoHosts.length > 0 ||
|
||||
@@ -2443,6 +2541,8 @@ const ConnectionModal: React.FC<{
|
||||
timeout: resolvedJvmTimeout,
|
||||
mysqlTopology: mysqlIsReplica ? "replica" : "single",
|
||||
mysqlReplicaHosts: mysqlReplicaHosts,
|
||||
kafkaTopology: kafkaIsCluster ? "cluster" : "single",
|
||||
kafkaHosts: kafkaHosts,
|
||||
mysqlReplicaUser: config.mysqlReplicaUser || "",
|
||||
mysqlReplicaPassword: config.mysqlReplicaPassword || "",
|
||||
mongoTopology: mongoIsReplica ? "replica" : "single",
|
||||
@@ -3401,6 +3501,23 @@ const ConnectionModal: React.FC<{
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "kafka") {
|
||||
const brokers =
|
||||
mergedValues.kafkaTopology === "cluster"
|
||||
? normalizeAddressList(mergedValues.kafkaHosts, defaultPort)
|
||||
: [];
|
||||
const allHosts = normalizeAddressList(
|
||||
[`${primaryHost}:${primaryPort}`, ...brokers],
|
||||
defaultPort,
|
||||
);
|
||||
if (mergedValues.kafkaTopology === "cluster" || allHosts.length > 1) {
|
||||
hosts = allHosts;
|
||||
topology = "cluster";
|
||||
} else {
|
||||
topology = "single";
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "mongodb") {
|
||||
mongoSrvEnabled = !!mergedValues.mongoSrv;
|
||||
const extraHosts =
|
||||
@@ -3637,6 +3754,7 @@ const ConnectionModal: React.FC<{
|
||||
includeDatabases: undefined,
|
||||
includeRedisDatabases: undefined,
|
||||
mysqlTopology: "single",
|
||||
kafkaTopology: "single",
|
||||
redisTopology: "single",
|
||||
mongoTopology: "single",
|
||||
mongoSrv: false,
|
||||
@@ -3646,6 +3764,7 @@ const ConnectionModal: React.FC<{
|
||||
mongoAuthMechanism: "",
|
||||
savePassword: true,
|
||||
mysqlReplicaHosts: [],
|
||||
kafkaHosts: [],
|
||||
redisHosts: [],
|
||||
redisSentinelMaster: "",
|
||||
redisSentinelUser: "",
|
||||
@@ -3699,6 +3818,7 @@ const ConnectionModal: React.FC<{
|
||||
httpTunnelUser: "",
|
||||
httpTunnelPassword: "",
|
||||
mysqlTopology: "single",
|
||||
kafkaTopology: "single",
|
||||
redisTopology: "single",
|
||||
mongoTopology: "single",
|
||||
mongoSrv: false,
|
||||
@@ -3708,6 +3828,7 @@ const ConnectionModal: React.FC<{
|
||||
mongoAuthMechanism: "",
|
||||
savePassword: true,
|
||||
mysqlReplicaHosts: [],
|
||||
kafkaHosts: [],
|
||||
redisHosts: [],
|
||||
redisSentinelMaster: "",
|
||||
redisSentinelUser: "",
|
||||
@@ -3722,7 +3843,7 @@ const ConnectionModal: React.FC<{
|
||||
});
|
||||
} else if (type !== "custom") {
|
||||
const defaultUser =
|
||||
type === "clickhouse" ? "default" : (type === "redis" || type === "elasticsearch" || type === "chroma" || type === "qdrant") ? "" : "root";
|
||||
type === "clickhouse" ? "default" : (type === "redis" || type === "elasticsearch" || type === "chroma" || type === "qdrant" || type === "kafka") ? "" : "root";
|
||||
const sslCapableType = supportsSSLForType(type);
|
||||
setUseSSL(false);
|
||||
setUseHttpTunnel(false);
|
||||
@@ -3741,6 +3862,7 @@ const ConnectionModal: React.FC<{
|
||||
httpTunnelUser: "",
|
||||
httpTunnelPassword: "",
|
||||
mysqlTopology: "single",
|
||||
kafkaTopology: "single",
|
||||
redisTopology: "single",
|
||||
mongoTopology: "single",
|
||||
mongoSrv: false,
|
||||
@@ -3750,6 +3872,7 @@ const ConnectionModal: React.FC<{
|
||||
mongoAuthMechanism: "",
|
||||
savePassword: true,
|
||||
mysqlReplicaHosts: [],
|
||||
kafkaHosts: [],
|
||||
redisHosts: [],
|
||||
redisSentinelMaster: "",
|
||||
redisSentinelUser: "",
|
||||
@@ -4850,6 +4973,22 @@ const ConnectionModal: React.FC<{
|
||||
),
|
||||
})}
|
||||
|
||||
{dbType === "kafka" &&
|
||||
renderConfigSectionCard({
|
||||
sectionKey: "service",
|
||||
icon: <DatabaseOutlined />,
|
||||
children: (
|
||||
<Form.Item
|
||||
name="database"
|
||||
label="默认 Topic(可选)"
|
||||
help="留空时必须在 SQL 中显式指定 Topic;填写后可直接执行 SHOW、CONSUME 或 SELECT 预览。"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Input {...noAutoCapInputProps} placeholder="例如:orders.events" />
|
||||
</Form.Item>
|
||||
),
|
||||
})}
|
||||
|
||||
{(dbType === "oracle" || isOceanBaseOracle) &&
|
||||
renderConfigSectionCard({
|
||||
sectionKey: "service",
|
||||
@@ -4902,6 +5041,48 @@ const ConnectionModal: React.FC<{
|
||||
}),
|
||||
})}
|
||||
|
||||
{isKafka &&
|
||||
renderConfigSectionCard({
|
||||
sectionKey: "connectionMode",
|
||||
icon: <ClusterOutlined />,
|
||||
children: renderChoiceCards({
|
||||
fieldName: "kafkaTopology",
|
||||
value: String(kafkaTopology),
|
||||
options: [
|
||||
{
|
||||
value: "single",
|
||||
label: "单 Broker",
|
||||
description: "只配置一个 bootstrap broker,适合本地或简单环境。",
|
||||
},
|
||||
{
|
||||
value: "cluster",
|
||||
label: "集群模式",
|
||||
description: "配置多个 bootstrap broker,提高发现与故障切换成功率。",
|
||||
},
|
||||
],
|
||||
}),
|
||||
})}
|
||||
|
||||
{isKafka &&
|
||||
kafkaTopology === "cluster" &&
|
||||
renderConfigSectionCard({
|
||||
sectionKey: "replica",
|
||||
icon: <ClusterOutlined />,
|
||||
children: (
|
||||
<Form.Item
|
||||
name="kafkaHosts"
|
||||
label="额外 Broker 地址"
|
||||
help="可输入多个 broker 地址,格式:host:port(回车确认)"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="例如:10.10.0.12:9092、10.10.0.13:9092"
|
||||
tokenSeparators={[",", ";", " "]}
|
||||
/>
|
||||
</Form.Item>
|
||||
),
|
||||
})}
|
||||
|
||||
{isMySQLLike &&
|
||||
mysqlTopology === "replica" &&
|
||||
renderConfigSectionCard({
|
||||
@@ -5019,13 +5200,13 @@ const ConnectionModal: React.FC<{
|
||||
name="user"
|
||||
label="用户名"
|
||||
rules={
|
||||
(dbType === "mongodb" || dbType === "elasticsearch" || dbType === "chroma" || dbType === "qdrant")
|
||||
(dbType === "mongodb" || dbType === "elasticsearch" || dbType === "chroma" || dbType === "qdrant" || dbType === "kafka")
|
||||
? []
|
||||
: [createUriAwareRequiredRule("请输入用户名")]
|
||||
}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Input {...noAutoCapInputProps} placeholder={(dbType === "elasticsearch" || dbType === "chroma" || dbType === "qdrant") ? "未开启认证可留空" : undefined} />
|
||||
<Input {...noAutoCapInputProps} placeholder={(dbType === "elasticsearch" || dbType === "chroma" || dbType === "qdrant" || dbType === "kafka") ? "未开启认证可留空" : undefined} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
@@ -5099,6 +5280,7 @@ const ConnectionModal: React.FC<{
|
||||
|
||||
{!isFileDb &&
|
||||
!isRedis &&
|
||||
!isKafka &&
|
||||
renderConfigSectionCard({
|
||||
sectionKey: "databaseScope",
|
||||
icon: <DatabaseOutlined />,
|
||||
@@ -5946,6 +6128,7 @@ const ConnectionModal: React.FC<{
|
||||
connectionParams: "",
|
||||
oceanBaseProtocol: "mysql",
|
||||
mysqlTopology: "single",
|
||||
kafkaTopology: "single",
|
||||
redisTopology: "single",
|
||||
mongoTopology: "single",
|
||||
mongoSrv: false,
|
||||
@@ -5953,6 +6136,7 @@ const ConnectionModal: React.FC<{
|
||||
mongoAuthMechanism: "",
|
||||
savePassword: true,
|
||||
mysqlReplicaHosts: [],
|
||||
kafkaHosts: [],
|
||||
redisHosts: [],
|
||||
redisSentinelMaster: "",
|
||||
redisSentinelUser: "",
|
||||
|
||||
Reference in New Issue
Block a user