import { useState, useMemo, useEffect } from 'react' import { TextField, IconButton, Button, Switch, FormControlLabel, Autocomplete, Box, Tooltip, useMediaQuery, } from '@mui/material' import { Done, Cancel, Add, Delete, Visibility, VisibilityOff, Save, } from '@mui/icons-material' import { useI18nStore } from '../stores/i18n.store' import { useRedisStateStore } from '../stores/redis-state.store' import { useSettingsStore } from '../stores/settings.store' import { useCommonStore } from '../stores/common.store' import { useOverlayStore } from '../stores/overlay.store' import { request } from '../stores/socket.service' import P3xrDialog from '../components/P3xrDialog' interface ConnectionDialogProps { open: boolean type: 'new' | 'edit' model?: any onClose: () => void } function initModel(type: string, source?: any): any { let model: any if (source) { model = structuredClone(source) model.password = source.id model.tlsCrt = source.id model.tlsKey = source.id model.tlsCa = source.id model.sshPassword = source.id model.sshPrivateKey = source.id } else { model = { name: '', host: '', port: 6379, askAuth: false, password: '', username: '', id: undefined, group: '', readonly: false, tlsWithoutCert: false, tlsRejectUnauthorized: false, tlsCrt: '', tlsKey: '', tlsCa: '', } } if (!model.ssh) { model.ssh = false; model.sshHost = model.sshHost || '' model.sshPort = model.sshPort || 22; model.sshUsername = model.sshUsername || '' model.sshPassword = model.sshPassword || source?.id || '' model.sshPrivateKey = model.sshPrivateKey || source?.id || '' } if (!model.cluster) model.cluster = false if (!model.sentinel) model.sentinel = false if (!model.nodes) model.nodes = [] for (const node of model.nodes) { node.password = node.id } return model } export default function ConnectionDialog({ open, type, model: sourceModel, onClose }: ConnectionDialogProps) { const strings = useI18nStore(s => s.strings) const cfg = useRedisStateStore(s => s.cfg) const connectionsList = useRedisStateStore(s => s.connections)?.list ?? [] const { generateId } = useSettingsStore() const { toast, generalHandleError } = useCommonStore() const overlay = useOverlayStore() const isWide = useMediaQuery('(min-width: 600px)') const readonlyConnections = cfg?.readonlyConnections === true const [model, setModel] = useState(() => initModel(type, sourceModel)) const [pwVisible, setPwVisible] = useState(false) const [sshPwVisible, setSshPwVisible] = useState(false) const [nodePwVisible, setNodePwVisible] = useState>({}) useEffect(() => { if (open) { setModel(initModel(type, sourceModel)) setPwVisible(false); setSshPwVisible(false); setNodePwVisible({}) } }, [open, type, sourceModel]) const existingGroups = useMemo(() => { const groups = new Set() for (const conn of connectionsList) { if (conn.group?.trim()) groups.add(conn.group.trim()) } return [...groups].sort() }, [connectionsList]) const set = (field: string, value: any) => setModel((m: any) => ({ ...m, [field]: value })) const setNode = (idx: number, field: string, value: any) => setModel((m: any) => { const nodes = [...m.nodes] nodes[idx] = { ...nodes[idx], [field]: value } return { ...m, nodes } }) const addNode = (index?: number) => { const newNode = { host: '', port: undefined, password: '', username: '', id: generateId() } setModel((m: any) => { const nodes = [...m.nodes] if (index === undefined) nodes.push(newNode); else nodes.splice(index + 1, 0, newNode) return { ...m, nodes } }) } const removeNode = async (idx: number) => { try { await useCommonStore.getState().confirm({ message: strings?.confirm?.deleteConnectionText }) setModel((m: any) => ({ ...m, nodes: m.nodes.filter((_: any, i: number) => i !== idx) })) toast(strings?.status?.nodeRemoved) } catch {} } const testConnection = async () => { try { overlay.show({ message: strings?.title?.connectingRedis }) await request({ action: 'redis-test-connection', payload: { model: structuredClone(model) } }) toast(strings?.status?.redisConnected) } catch (e) { generalHandleError(e) } finally { overlay.hide() } } const submit = async () => { if (!model.name?.trim()) { toast(strings?.form?.error?.invalid); return } const saveModel = structuredClone(model) if (!saveModel.host) saveModel.host = 'localhost' if (!saveModel.port) saveModel.port = 6379 if (type === 'new') saveModel.id = generateId() for (const node of saveModel.nodes) { if (!node.host) node.host = 'localhost' if (!node.id) node.id = generateId() } if (typeof saveModel.group === 'string') saveModel.group = saveModel.group.trim() || undefined try { await request({ action: 'connection-save', payload: { model: saveModel } }) toast(type === 'new' ? strings?.status?.added : strings?.status?.saved) onClose() } catch (e) { generalHandleError(e) } } const title = readonlyConnections ? strings?.label?.connectiondView : type === 'new' ? strings?.label?.connectiondAdd : strings?.label?.connectiondEdit const PasswordField = ({ label, value, onChange, visible, onToggle, disabled }: any) => ( onChange(e.target.value)} disabled={disabled} autoComplete="off" slotProps={{ input: { endAdornment: !disabled && ( {visible ? : } )}}} /> ) if (!open) return null return ( {isWide ? ( ) : ( )} {!readonlyConnections && ( )} }> {model.id && type !== 'new' && ( <> {strings?.label?.id?.info} )} set('name', e.target.value)} disabled={readonlyConnections} /> set('group', v)} disabled={readonlyConnections} renderInput={params => } /> {/* SSH */} set('ssh', v)} disabled={readonlyConnections} />} label={model.ssh ? strings?.label?.ssh?.on : strings?.label?.ssh?.off} /> {model.ssh && ( SSH set('sshHost', e.target.value)} disabled={readonlyConnections} /> set('sshPort', Number(e.target.value))} disabled={readonlyConnections} slotProps={{ htmlInput: { min: 1, max: 65535 } }} /> set('sshUsername', e.target.value)} disabled={readonlyConnections} /> set('sshPassword', v)} visible={sshPwVisible} onToggle={() => setSshPwVisible(!sshPwVisible)} disabled={readonlyConnections} /> {strings?.label?.passwordSecure} set('sshPrivateKey', e.target.value)} disabled={readonlyConnections} autoComplete="off" /> {strings?.label?.secureFeature} )} {/* Node 1 */} Node 1 set('host', e.target.value)} disabled={readonlyConnections} /> set('port', Number(e.target.value))} disabled={readonlyConnections} slotProps={{ htmlInput: { min: 1, max: 65535 } }} /> set('askAuth', v)} disabled={readonlyConnections} />} label={strings?.label?.askAuth} /> set('username', e.target.value)} disabled={readonlyConnections} autoComplete="off" /> set('password', v)} visible={pwVisible} onToggle={() => setPwVisible(!pwVisible)} disabled={readonlyConnections} /> {strings?.label?.passwordSecure} {/* Readonly */} set('readonly', v)} disabled={readonlyConnections} />} label={model.readonly ? strings?.label?.readonly?.on : strings?.label?.readonly?.off} /> {/* Cluster / Sentinel */} { set('cluster', v); if (v) set('sentinel', false) }} disabled={readonlyConnections} />} label={model.cluster ? strings?.label?.cluster?.on : strings?.label?.cluster?.off} /> { set('sentinel', v); if (v) set('cluster', false) }} disabled={readonlyConnections} />} label={model.sentinel ? strings?.label?.sentinel?.on : strings?.label?.sentinel?.off} /> {(model.cluster || model.sentinel) && !readonlyConnections && ( )} {model.sentinel && ( set('sentinelName', e.target.value)} disabled={readonlyConnections} /> )} {/* Dynamic nodes */} {(model.cluster || model.sentinel) && model.nodes.map((node: any, idx: number) => ( Node {idx + 2} {!readonlyConnections && ( )} {node.id && (<>{strings?.label?.id?.info})} setNode(idx, 'host', e.target.value)} disabled={readonlyConnections} /> setNode(idx, 'port', Number(e.target.value))} disabled={readonlyConnections} slotProps={{ htmlInput: { min: 1, max: 65535 } }} /> setNode(idx, 'username', e.target.value)} disabled={readonlyConnections} autoComplete="off" /> setNode(idx, 'password', v)} visible={!!nodePwVisible[idx]} onToggle={() => setNodePwVisible(p => ({ ...p, [idx]: !p[idx] }))} disabled={readonlyConnections} /> {strings?.label?.passwordSecure} ))} {/* TLS */} set('tlsWithoutCert', v)} disabled={readonlyConnections} />} label={strings?.label?.tlsWithoutCert} /> set('tlsRejectUnauthorized', v)} disabled={readonlyConnections} />} label={strings?.label?.tlsRejectUnauthorized} /> {!model.tlsWithoutCert && ( TLS {[{ label: 'TLS (redis.crt)', field: 'tlsCrt' }, { label: 'TLS (redis.key)', field: 'tlsKey' }, { label: 'TLS (ca.crt)', field: 'tlsCa' }].map(({ label, field }) => ( set(field, e.target.value)} disabled={readonlyConnections} autoComplete="off" />{strings?.label?.tlsSecure} ))} )} ) }