diff --git a/apps/desktop/src/lib/redisQueryResult.ts b/apps/desktop/src/lib/redisQueryResult.ts index 3ce555e6f6..7f188fc196 100644 --- a/apps/desktop/src/lib/redisQueryResult.ts +++ b/apps/desktop/src/lib/redisQueryResult.ts @@ -20,6 +20,17 @@ export function redisCommandResultToQueryResult(value: unknown, elapsedMs: numbe execution_time_ms: Math.max(0, Math.round(elapsedMs)), }; } + // INFO commands in cluster mode → [[node_addr, info_text], ...] pairs. + // Render as a two-column table with the addr as the index and info text as the value. + if (Array.isArray(value) && value.length > 0 && value.every((item) => Array.isArray(item) && item.length === 2 && typeof item[0] === "string" && typeof item[1] === "string")) { + const rows: (string | number | boolean | null)[][] = value.map((item: [string, string]) => [item[0], item[1]]); + return { + columns: ["(index)", "value"], + rows, + affected_rows: value.length, + execution_time_ms: Math.max(0, Math.round(elapsedMs)), + }; + } if (Array.isArray(value)) { const rows: (string | number | boolean | null)[][] = value.map((item, i) => [i + 1, formatRedisCommandResult(item)]); return { diff --git a/crates/dbx-core/src/db/redis_driver.rs b/crates/dbx-core/src/db/redis_driver.rs index cd887fe107..b2356d76ee 100644 --- a/crates/dbx-core/src/db/redis_driver.rs +++ b/crates/dbx-core/src/db/redis_driver.rs @@ -1014,27 +1014,28 @@ where // Special handling for INFO command in cluster mode. // redis-rs ClusterConnection routes INFO to all primaries and // aggregates the results as a Map(node_addr → full_info_text). - // We detect this pattern and format it as human-readable plain text - // instead of converting to a JSON array of {key, value} objects. + // We detect this pattern and return it as an array of + // [node_addr, info_text] pairs, which the frontend renders + // as a two-column (index → node_addr, value → info_text) table. if command == "INFO" { if let RedisRawValue::Map(entries) = &raw { - // Cluster-aggregated INFO has multi-line values starting with "# Server". + // Cluster-aggregated INFO has multi-line values starting with "# " + // (e.g. "# Server", "# Memory", "# Clients"). // This distinguishes it from a RESP3 standalone INFO map where values - // are single field values (e.g. "redis_version", "os"). + // are single field values (e.g. "redis_version", "os") or nested maps. let is_cluster_aggregation = - entries.iter().any(|(_, v)| redis_raw_value_as_str(v).is_some_and(|s| s.starts_with("# Server"))); + entries.iter().any(|(_, v)| redis_raw_value_as_str(v).is_some_and(|s| s.starts_with("# "))); if is_cluster_aggregation { - let mut parts: Vec = Vec::with_capacity(entries.len()); - for (key, value) in entries { - let addr = redis_raw_value_as_str(key); - let info = redis_raw_value_as_str(value); - if let (Some(addr), Some(info)) = (addr, info) { - parts.push(format!("{addr}\n{info}")); - } - } - let text = parts.join("\n\n"); - return Ok(RedisCommandResult { command, safety, value: serde_json::Value::String(text) }); + let pairs: Vec = entries + .iter() + .filter_map(|(key, value)| { + let addr = redis_raw_value_as_str(key)?; + let info = redis_raw_value_as_str(value)?; + Some(serde_json::json!([addr, info])) + }) + .collect(); + return Ok(RedisCommandResult { command, safety, value: serde_json::Value::Array(pairs) }); } } }