diff --git a/frontend/src/components/ConnectionModal.tsx b/frontend/src/components/ConnectionModal.tsx
index bf1c8ad..22aa35b 100644
--- a/frontend/src/components/ConnectionModal.tsx
+++ b/frontend/src/components/ConnectionModal.tsx
@@ -106,6 +106,7 @@ const ConnectionModal: React.FC<{
const mysqlTopology = Form.useWatch('mysqlTopology', form) || 'single';
const mongoTopology = Form.useWatch('mongoTopology', form) || 'single';
const mongoSrv = Form.useWatch('mongoSrv', form) || false;
+ const redisTopology = Form.useWatch('redisTopology', form) || 'single';
const getSectionBg = (darkHex: string) => {
if (!darkMode) {
@@ -449,6 +450,35 @@ const ConnectionModal: React.FC<{
return { host: normalizeFileDbPath(safeDecode(rawPath)) };
}
+ if (type === 'redis') {
+ const parsed = parseMultiHostUri(trimmedUri, 'redis');
+ 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, 6379);
+ if (!hostList.length) {
+ return null;
+ }
+ const primary = parseHostPort(hostList[0] || 'localhost:6379', 6379);
+ const topologyParam = String(parsed.params.get('topology') || '').toLowerCase();
+ const dbText = String(parsed.database || '').trim().replace(/^\//, '');
+ const dbIndex = Number(dbText);
+ return {
+ host: primary?.host || 'localhost',
+ port: primary?.port || 6379,
+ password: parsed.password || '',
+ redisTopology: hostList.length > 1 || topologyParam === 'cluster' ? 'cluster' : 'single',
+ redisHosts: hostList.slice(1),
+ redisDB: Number.isFinite(dbIndex) && dbIndex >= 0 && dbIndex <= 15 ? Math.trunc(dbIndex) : 0,
+ };
+ }
+
if (type === 'mongodb') {
const parsed = parseMultiHostUri(trimmedUri, 'mongodb') || parseMultiHostUri(trimmedUri, 'mongodb+srv');
if (!parsed) {
@@ -547,6 +577,9 @@ const ConnectionModal: React.FC<{
if (dbType === 'clickhouse') {
return 'clickhouse://default:pass@127.0.0.1:9000/default';
}
+ if (dbType === 'redis') {
+ return 'redis://:pass@127.0.0.1:6379,127.0.0.2:6379/0?topology=cluster';
+ }
if (dbType === 'oracle') {
return 'oracle://user:pass@127.0.0.1:1521/ORCLPDB1';
}
@@ -585,6 +618,26 @@ const ConnectionModal: React.FC<{
return `${scheme}://${encodedAuth}${hosts.join(',')}${dbPath}${query ? `?${query}` : ''}`;
}
+ if (type === 'redis') {
+ const primary = toAddress(host, port, 6379);
+ const clusterHosts = values.redisTopology === 'cluster'
+ ? normalizeAddressList(values.redisHosts, 6379)
+ : [];
+ const hosts = normalizeAddressList([primary, ...clusterHosts], 6379);
+ const params = new URLSearchParams();
+ if (hosts.length > 1 || values.redisTopology === 'cluster') {
+ params.set('topology', 'cluster');
+ }
+ const redisPassword = String(values.password || '');
+ const redisAuth = redisPassword ? `:${encodeURIComponent(redisPassword)}@` : '';
+ const redisDB = Number.isFinite(Number(values.redisDB))
+ ? Math.max(0, Math.min(15, Math.trunc(Number(values.redisDB))))
+ : 0;
+ const dbPath = `/${redisDB}`;
+ const query = params.toString();
+ return `redis://${redisAuth}${hosts.join(',')}${dbPath}${query ? `?${query}` : ''}`;
+ }
+
if (isFileDatabaseType(type)) {
const pathText = normalizeFileDbPath(String(values.host || '').trim());
if (!pathText) {
@@ -770,8 +823,10 @@ const ConnectionModal: React.FC<{
: (primaryAddress?.port || Number(config.port || defaultPort));
const mysqlReplicaHosts = (configType === 'mysql' || configType === 'mariadb' || configType === 'diros' || configType === 'sphinx') ? normalizedHosts.slice(1) : [];
const mongoHosts = configType === 'mongodb' ? normalizedHosts.slice(1) : [];
+ const redisHosts = configType === 'redis' ? normalizedHosts.slice(1) : [];
const mysqlIsReplica = String(config.topology || '').toLowerCase() === 'replica' || mysqlReplicaHosts.length > 0;
const mongoIsReplica = String(config.topology || '').toLowerCase() === 'replica' || mongoHosts.length > 0 || !!config.replicaSet;
+ const redisIsCluster = String(config.topology || '').toLowerCase() === 'cluster' || redisHosts.length > 0;
form.setFieldsValue({
type: configType,
name: initialValues.name,
@@ -804,12 +859,15 @@ const ConnectionModal: React.FC<{
mysqlReplicaPassword: config.mysqlReplicaPassword || '',
mongoTopology: mongoIsReplica ? 'replica' : 'single',
mongoHosts: mongoHosts,
+ redisTopology: redisIsCluster ? 'cluster' : 'single',
+ redisHosts: redisHosts,
mongoSrv: !!config.mongoSrv,
mongoReplicaSet: config.replicaSet || '',
mongoAuthSource: config.authSource || '',
mongoReadPreference: config.readPreference || 'primary',
mongoAuthMechanism: config.mongoAuthMechanism || '',
savePassword: config.savePassword !== false,
+ redisDB: Number.isFinite(Number(config.redisDB)) ? Number(config.redisDB) : 0,
mongoReplicaUser: config.mongoReplicaUser || '',
mongoReplicaPassword: config.mongoReplicaPassword || ''
});
@@ -924,7 +982,6 @@ const ConnectionModal: React.FC<{
if (res.success) {
setTestResult({ type: 'success', message: res.message });
if (isRedisType) {
- // Redis: generate database list 0-15
setRedisDbList(Array.from({ length: 16 }, (_, i) => i));
} else {
// Other databases: fetch database list
@@ -1033,7 +1090,7 @@ const ConnectionModal: React.FC<{
}
let hosts: string[] = [];
- let topology: 'single' | 'replica' | undefined;
+ let topology: 'single' | 'replica' | 'cluster' | undefined;
let replicaSet = '';
let authSource = '';
let readPreference = '';
@@ -1087,6 +1144,22 @@ const ConnectionModal: React.FC<{
mongoAuthMechanism = String(mergedValues.mongoAuthMechanism || '').trim().toUpperCase();
}
+ if (type === 'redis') {
+ const clusterNodes = mergedValues.redisTopology === 'cluster'
+ ? normalizeAddressList(mergedValues.redisHosts, defaultPort)
+ : [];
+ const allHosts = normalizeAddressList([`${primaryHost}:${primaryPort}`, ...clusterNodes], defaultPort);
+ if (mergedValues.redisTopology === 'cluster' || allHosts.length > 1) {
+ hosts = allHosts;
+ topology = 'cluster';
+ } else {
+ topology = 'single';
+ }
+ mergedValues.redisDB = Number.isFinite(Number(mergedValues.redisDB))
+ ? Math.max(0, Math.min(15, Math.trunc(Number(mergedValues.redisDB))))
+ : 0;
+ }
+
const sshConfig = mergedValues.useSSH ? {
host: mergedValues.sshHost,
port: Number(mergedValues.sshPort),
@@ -1128,6 +1201,9 @@ const ConnectionModal: React.FC<{
driver: mergedValues.driver,
dsn: mergedValues.dsn,
timeout: Number(mergedValues.timeout || 30),
+ redisDB: Number.isFinite(Number(mergedValues.redisDB))
+ ? Math.max(0, Math.min(15, Math.trunc(Number(mergedValues.redisDB))))
+ : 0,
uri: String(mergedValues.uri || '').trim(),
hosts: hosts,
topology: topology,
@@ -1178,6 +1254,7 @@ const ConnectionModal: React.FC<{
proxyUser: '',
proxyPassword: '',
mysqlTopology: 'single',
+ redisTopology: 'single',
mongoTopology: 'single',
mongoSrv: false,
mongoReadPreference: 'primary',
@@ -1186,11 +1263,13 @@ const ConnectionModal: React.FC<{
mongoAuthMechanism: '',
savePassword: true,
mysqlReplicaHosts: [],
+ redisHosts: [],
mongoHosts: [],
mysqlReplicaUser: '',
mysqlReplicaPassword: '',
mongoReplicaUser: '',
mongoReplicaPassword: '',
+ redisDB: 0,
});
} else if (type !== 'custom') {
const defaultUser = type === 'clickhouse' ? 'default' : 'root';
@@ -1199,6 +1278,7 @@ const ConnectionModal: React.FC<{
database: '',
port: defaultPort,
mysqlTopology: 'single',
+ redisTopology: 'single',
mongoTopology: 'single',
mongoSrv: false,
mongoReadPreference: 'primary',
@@ -1207,11 +1287,13 @@ const ConnectionModal: React.FC<{
mongoAuthMechanism: '',
savePassword: true,
mysqlReplicaHosts: [],
+ redisHosts: [],
mongoHosts: [],
mysqlReplicaUser: '',
mysqlReplicaPassword: '',
mongoReplicaUser: '',
mongoReplicaPassword: '',
+ redisDB: 0,
});
}
@@ -1346,17 +1428,20 @@ const ConnectionModal: React.FC<{
timeout: 30,
uri: '',
mysqlTopology: 'single',
+ redisTopology: 'single',
mongoTopology: 'single',
mongoSrv: false,
mongoReadPreference: 'primary',
mongoAuthMechanism: '',
savePassword: true,
mysqlReplicaHosts: [],
+ redisHosts: [],
mongoHosts: [],
mysqlReplicaUser: '',
mysqlReplicaPassword: '',
mongoReplicaUser: '',
mongoReplicaPassword: '',
+ redisDB: 0,
}}
onValuesChange={(changed) => {
if (testResult) {
@@ -1384,6 +1469,17 @@ const ConnectionModal: React.FC<{
}
// Type change handled by step 1, but keep sync if select changes (hidden now)
if (changed.type !== undefined) setDbType(changed.type);
+ if (changed.redisTopology !== undefined) {
+ const supportedDbs = Array.from({ length: 16 }, (_, i) => i);
+ setRedisDbList(supportedDbs);
+ const selectedDbsRaw = form.getFieldValue('includeRedisDatabases');
+ const selectedDbs = Array.isArray(selectedDbsRaw) ? selectedDbsRaw.map((entry: any) => Number(entry)) : [];
+ const validDbs = selectedDbs
+ .filter((entry: number) => Number.isFinite(entry))
+ .map((entry: number) => Math.trunc(entry))
+ .filter((entry: number) => supportedDbs.includes(entry));
+ form.setFieldValue('includeRedisDatabases', validDbs.length > 0 ? validDbs : undefined);
+ }
if (
changed.type !== undefined
|| changed.host !== undefined
@@ -1657,11 +1753,36 @@ const ConnectionModal: React.FC<{
{/* Redis specific: password only, no username */}
{isRedis && (
<>
+
+
+
+ {redisTopology === 'cluster' && (
+
+
+
+ )}
-
-