diff --git a/apps/frontend/src/components/ValkeyReconnect.tsx b/apps/frontend/src/components/ValkeyReconnect.tsx index b118d203..242bfd24 100644 --- a/apps/frontend/src/components/ValkeyReconnect.tsx +++ b/apps/frontend/src/components/ValkeyReconnect.tsx @@ -3,6 +3,7 @@ import { useDispatch, useSelector } from "react-redux" import { useNavigate, useParams } from "react-router" import { CONNECTED, CONNECTING, ERROR } from "@common/src/constants" import { Loader2, Database, AlertCircle } from "lucide-react" +import * as R from "ramda" import RetryProgress from "./ui/retry-progress" import { PasswordPromptModal } from "./ui/password-prompt-modal" import type { RootState } from "@/store" @@ -20,7 +21,7 @@ export function ValkeyReconnect() { const { status, errorMessage, reconnect } = connection || {} const [showPasswordPrompt, setShowPasswordPrompt] = useState(false) - const needsPassword = connection?.connectionDetails.password === undefined + const needsPassword = R.isNil(connection?.connectionDetails.password) // Close password prompt on successful connection useEffect(() => { diff --git a/apps/frontend/src/components/connection/ClusterConnectionGroup.tsx b/apps/frontend/src/components/connection/ClusterConnectionGroup.tsx index 78d815d3..ab812daf 100644 --- a/apps/frontend/src/components/connection/ClusterConnectionGroup.tsx +++ b/apps/frontend/src/components/connection/ClusterConnectionGroup.tsx @@ -96,7 +96,8 @@ export const ClusterConnectionGroup = ({ clusterId, connections, highlight = "", const handleConnectLatest = () => { if (!lastOpenedNode) return - if (lastOpenedNode.connection.connectionDetails.password === undefined && onPasswordRequired) { + const { password, authType } = lastOpenedNode.connection.connectionDetails + if (authType !== "iam" && R.isNil(password) && onPasswordRequired) { onPasswordRequired(lastOpenedNode.connectionId) return } diff --git a/apps/frontend/src/components/connection/ConnectionEntry.tsx b/apps/frontend/src/components/connection/ConnectionEntry.tsx index bfbd7e66..86fe8f1c 100644 --- a/apps/frontend/src/components/connection/ConnectionEntry.tsx +++ b/apps/frontend/src/components/connection/ConnectionEntry.tsx @@ -1,4 +1,5 @@ import { CONNECTED, ERROR, CONNECTING } from "@common/src/constants.ts" +import * as R from "ramda" import { CircleChevronRight, Server } from "lucide-react" import { Link } from "react-router" import { @@ -41,7 +42,8 @@ export const ConnectionEntry = ({ const handleDisconnect = () => dispatch(closeConnection({ connectionId })) const handleConnect = () => { - if (connection.connectionDetails.password === undefined && onPasswordRequired) { + const { password, authType } = connection.connectionDetails + if (authType !== "iam" && R.isNil(password) && onPasswordRequired) { onPasswordRequired(connectionId) return } diff --git a/apps/frontend/src/state/epics/valkeyEpics.ts b/apps/frontend/src/state/epics/valkeyEpics.ts index c0c184ba..21f910dc 100644 --- a/apps/frontend/src/state/epics/valkeyEpics.ts +++ b/apps/frontend/src/state/epics/valkeyEpics.ts @@ -166,7 +166,7 @@ export const valkeyRetryEpic = (store: Store) => return EMPTY } - if (connection.connectionDetails.password === undefined) { + if (R.isNil(connection.connectionDetails.password)) { console.debug(`Password unavailable for ${connectionId}, skipping auto-retry`) store.dispatch(stopRetry({ connectionId })) return EMPTY @@ -223,7 +223,7 @@ export const autoReconnectEpic = (store: Store) => const disconnectedConnections = Object.entries(connections) .filter(([, connection]) => connection.status === DISCONNECTED) - .filter(([, connection]) => connection.connectionDetails.password !== undefined && connection.connectionDetails.password !== "") + .filter(([, connection]) => R.isNotNil(connection.connectionDetails.password)) if (disconnectedConnections.length > 0) { console.log(`Auto-reconnecting ${disconnectedConnections.length} connection(s)`) diff --git a/apps/frontend/src/state/valkey-features/connection/connectionSlice.ts b/apps/frontend/src/state/valkey-features/connection/connectionSlice.ts index dee2247c..bc0775b4 100644 --- a/apps/frontend/src/state/valkey-features/connection/connectionSlice.ts +++ b/apps/frontend/src/state/valkey-features/connection/connectionSlice.ts @@ -108,8 +108,10 @@ const connectionSlice = createSlice({ errorMessage: isRetry && existingConnection?.errorMessage ? existingConnection.errorMessage : null, connectionDetails: { ...connectionDetails, - // Strip password from state if secure storage is unavailable to prevent unencrypted persistence. - password: connectionDetails.password && secureStorage.isAvailable() ? connectionDetails.password : undefined, + // Preserve "" (no-password connections) but strip real passwords if secure storage is unavailable + password: (R.isNotNil(connectionDetails.password) && secureStorage.isAvailable()) || R.isEmpty(connectionDetails.password) + ? connectionDetails.password + : undefined, clusterSlotStatsEnabled: false, jsonModuleAvailable: false, }, diff --git a/apps/metrics/src/valkey-client.js b/apps/metrics/src/valkey-client.js index 19988267..e1e91ff0 100644 --- a/apps/metrics/src/valkey-client.js +++ b/apps/metrics/src/valkey-client.js @@ -40,13 +40,14 @@ export const createValkeyClient = async (cfg = {}) => { addresses, credentials, useTLS, - ...(useTLS && process.env.VALKEY_VERIFY_CERT === "false" && { - advancedConfiguration: { + advancedConfiguration: { + ...(useTLS && process.env.VALKEY_VERIFY_CERT === "false" && { tlsAdvancedConfiguration: { insecure: true, }, - }, - }), + }), + connectionTimeout: 30000, + }, requestTimeout: 5000, } diff --git a/apps/metrics/src/valkey-client.test.js b/apps/metrics/src/valkey-client.test.js index befd9922..680f65e0 100644 --- a/apps/metrics/src/valkey-client.test.js +++ b/apps/metrics/src/valkey-client.test.js @@ -83,6 +83,7 @@ describe("valkey client mode selection", () => { tlsAdvancedConfiguration: { insecure: true, }, + connectionTimeout: 30000, }, requestTimeout: 5000, clientName: "valkey_admin_metrics_cluster_client", diff --git a/apps/server/src/__tests__/connection.test.ts b/apps/server/src/__tests__/connection.test.ts index dfd4b7a4..d4d24e9a 100644 --- a/apps/server/src/__tests__/connection.test.ts +++ b/apps/server/src/__tests__/connection.test.ts @@ -309,6 +309,7 @@ describe("connectToValkey", () => { tls: false, verifyTlsCertificate: false, connectionId: "conn-456", + endpointType: "node", } as ConnectionDetails, connectionId: "", } diff --git a/apps/server/src/connection.ts b/apps/server/src/connection.ts index d8793236..4014f4c6 100644 --- a/apps/server/src/connection.ts +++ b/apps/server/src/connection.ts @@ -226,13 +226,7 @@ export async function connectToCluster( const { connectionId } = payload const { verifyTlsCertificate, tls: useTLS } = payload.connectionDetails try { - const CONNECTION_TIMEOUT_MS = 10000 - let clusterClient = await Promise.race([ - createClusterValkeyClient({ addresses, credentials, useTLS, verifyTlsCertificate }), - new Promise((_, reject) => - setTimeout(() => reject(new Error("Connection timed out")), CONNECTION_TIMEOUT_MS), - ), - ]) + let clusterClient = await createClusterValkeyClient({ addresses, credentials, useTLS, verifyTlsCertificate }) // TODO: Optimize to not call discoverCluster when configEndpointId is available // It implies we already discovered cluster nodes once diff --git a/apps/server/src/valkey-client.ts b/apps/server/src/valkey-client.ts index cd11b529..18894ac8 100644 --- a/apps/server/src/valkey-client.ts +++ b/apps/server/src/valkey-client.ts @@ -21,13 +21,14 @@ const buildSharedOptions = ({ addresses, credentials, useTLS, - ...(useTLS && verifyTlsCertificate === false && { - advancedConfiguration: { + advancedConfiguration: { + ...(useTLS && verifyTlsCertificate === false && { tlsAdvancedConfiguration: { insecure: true, }, - }, - }), + }), + connectionTimeout: 30000, + }, requestTimeout: 5000, })