From ae96ca475628d12b684eb77106ad56d25897a40c Mon Sep 17 00:00:00 2001 From: George Date: Wed, 30 Sep 2026 12:59:37 +0530 Subject: [PATCH] --feat --ADDED MAX CONNECTIONS BUTTON ADDED --ADDED POP UP TO ADJ THIS CONNECTION --- src/App.css | 183 ++++++++++++++++++++++++++++++++++ src/NodeActiveConnections.tsx | 146 ++++++++++++++++++++++++++- 2 files changed, 328 insertions(+), 1 deletion(-) diff --git a/src/App.css b/src/App.css index 2343246..debf14f 100644 --- a/src/App.css +++ b/src/App.css @@ -936,6 +936,47 @@ width: 100%; } +.max-connections-value-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 4%; + min-width: 0; +} + +.max-connections-value-row strong { + min-width: 0; + overflow-wrap: anywhere; +} + +.adjust-max-connections-button { + box-sizing: border-box; + flex: 0 1 auto; + max-width: 65%; + min-width: 0; + margin-left: auto; + padding: 2% 3%; + border: 1px solid var(--accent-border); + border-radius: 0.4em; + background: var(--accent-bg); + color: var(--accent); + font: inherit; + font-size: 65%; + line-height: 1.2; + white-space: normal; + overflow-wrap: anywhere; + cursor: pointer; +} + +.adjust-max-connections-button:hover { + border-color: var(--accent); +} + +.adjust-max-connections-button:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + .largest-tables .page-header { flex-wrap: wrap; align-items: flex-start; @@ -1298,3 +1339,145 @@ .nodes-page .status-message { margin-bottom: 0.5%; } + + +.max-connections-dialog { + box-sizing: border-box; + width: 40%; + max-width: 90%; + max-height: 90%; + margin: auto; + padding: 2%; + font-size: 100%; + line-height: 140%; + overflow: auto; + border: 1px solid var(--border); + border-radius: 0.5em; + background: var(--bg); + color: var(--text-h); +} + +.max-connections-dialog::backdrop { + background: rgb(0 0 0 / 50%); +} + +.max-connections-dialog-header, +.max-connections-dialog-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 3%; +} + +.max-connections-dialog .max-connections-dialog-header h2 { + margin: 0; + font-size: 110%; + font-weight: 600; + line-height: 125%; +} + +.max-connections-recommendations { + width: 100%; + margin: 4% 0; + border-collapse: collapse; + font-size: 100%; +} + +.max-connections-recommendations th, +.max-connections-recommendations td { + padding: 2% 3%; + border-bottom: 1px solid var(--border); + text-align: left; +} + +.max-connections-recommendations th { + background: var(--surface); + font-size: 90%; + font-weight: 600; +} + +.max-connections-recommendations th:last-child, +.max-connections-recommendations td:last-child { + text-align: right; + white-space: nowrap; +} + +.max-connections-form { + display: block; +} + +.max-connections-form > * { + display: block; + margin: 0; +} + +.max-connections-form > * + * { + margin-top: 3%; +} + +.max-connections-form label { + font-size: 100%; + font-weight: 500; +} + +.max-connections-dialog .max-connections-form p { + padding: 2% 0; + font-size: 100%; + line-height: 140%; +} + +.max-connections-form input { + box-sizing: border-box; + width: 100%; + min-width: 0; + padding: 2% 3%; + border: 1px solid var(--border); + border-radius: 0.4em; + background: var(--surface); + color: var(--text-h); + font: inherit; +} + +.max-connections-form input:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.max-connections-form .max-connections-dialog-actions { + display: flex; + justify-content: flex-end; + flex-wrap: wrap; + margin-top: 4%; +} + +.max-connections-dialog .primary-button, +.max-connections-dialog .secondary-button { + min-height: 2.2em; + max-width: 100%; + padding: 2% 3%; + font-size: 100%; + line-height: 125%; +} + +.max-connections-dialog-header .secondary-button { + flex-shrink: 0; + padding: 1% 3%; +} + +.max-connections-dialog-actions > button { + margin-top: 2%; +} + +@media (max-width: 1024px) { + .max-connections-dialog { + width: 60%; + padding: 3%; + } +} + +@media (max-width: 600px) { + .max-connections-dialog { + width: 90%; + padding: 4%; + } +} diff --git a/src/NodeActiveConnections.tsx b/src/NodeActiveConnections.tsx index 1f88dca..8cd89e1 100644 --- a/src/NodeActiveConnections.tsx +++ b/src/NodeActiveConnections.tsx @@ -1,3 +1,4 @@ +import { useEffect, useRef, useState } from 'react' import useNodeDatabaseMetric from './useNodeDatabaseMetric' type ActiveConnections = { @@ -24,6 +25,97 @@ function isActiveConnections(value: unknown): value is ActiveConnections { export default function NodeActiveConnections({ nodeId, nodeName }: { nodeId: string; nodeName: string }) { const { data, connection } = useNodeDatabaseMetric(nodeId, nodeName, 'active-connections', isActiveConnections) + const dialogRef = useRef(null) + const [maxConnections, setMaxConnections] = useState('') + const [submitMessage, setSubmitMessage] = useState('') + const [saving, setSaving] = useState(false) + const [submitError, setSubmitError] = useState(false) + const requestRef = useRef(null) + + useEffect(() => () => { + requestRef.current?.abort() + requestRef.current = null + dialogRef.current?.close() + }, [nodeId, nodeName]) + + const updateMaxConnections = async () => { + if (requestRef.current) return + const value = Number(maxConnections) + if (!/^[1-9][0-9]*$/.test(maxConnections) || !Number.isSafeInteger(value) || value < 1) { + setSubmitError(true) + setSubmitMessage('Enter a positive whole number.') + return + } + const controller = new AbortController() + requestRef.current = controller + setSaving(true) + setSubmitError(false) + setSubmitMessage('') + try { + const apiBaseUrl = import.meta.env.VITE_API_BASE_URL + const databaseApiBaseUrl = import.meta.env.VITE_DATABASE_API_BASE_URL + if (!apiBaseUrl) throw new Error('API URL is not configured.') + if (!databaseApiBaseUrl && !import.meta.env.DEV) throw new Error('Database API URL is not configured.') + const databaseBaseUrl = import.meta.env.DEV ? '/database-api' : databaseApiBaseUrl.replace(/\/$/, '') + const token = localStorage.getItem('access_token') + if (!token) throw new Error('Authentication required. Get a token from the Nodes page, then retry.') + const headers = { Authorization: `Bearer ${token}` } + const nodeResponse = await fetch(`${apiBaseUrl.replace(/\/$/, '')}/spock/node-interfaces/${encodeURIComponent(nodeId)}`, { + headers, + signal: controller.signal, + }) + if (!nodeResponse.ok) throw new Error(`Failed to load node details (${nodeResponse.status}).`) + const result = await nodeResponse.json() + const node = result?.node + if (result?.status !== 'success' || !node || node.uuid !== nodeId + || typeof node.host !== 'string' || typeof node.port !== 'number' + || typeof node.dbname !== 'string' || typeof node.user !== 'string') { + throw new Error('Invalid node details.') + } + const response = await fetch(`${databaseBaseUrl}/database/max-connections`, { + method: 'POST', + headers: { ...headers, 'Content-Type': 'application/json' }, + signal: controller.signal, + body: JSON.stringify({ + node_name: nodeName, + host: node.host, + port: node.port, + dbname: node.dbname, + user: node.user, + password: 'postgres', + max_connections: value, + }), + }) + if (!response.ok) throw new Error(`Failed to update max connections (${response.status}).`) + const updated = await response.json() + if (updated?.status !== 'success' + || !Number.isSafeInteger(updated.previous_max_connections) || updated.previous_max_connections < 1 + || !Number.isSafeInteger(updated.new_max_connections) || updated.new_max_connections < 1 + || typeof updated.restart_required !== 'boolean' || typeof updated.message !== 'string') { + throw new Error('Unable to confirm the update: invalid server response. Check the configuration before retrying.') + } + if (controller.signal.aborted) return + setSubmitMessage(`Configured max connections: ${updated.previous_max_connections.toLocaleString()} → ${updated.new_max_connections.toLocaleString()}. ${updated.message}${updated.restart_required ? ' Restart required; the live value will change after PostgreSQL restarts.' : ''}`) + } catch (cause) { + if (controller.signal.aborted) return + setSubmitError(true) + setSubmitMessage(cause instanceof Error ? cause.message : 'Unable to update max connections.') + } finally { + if (requestRef.current === controller) { + requestRef.current = null + setSaving(false) + } + } + } + + const openAdjustment = () => { + setMaxConnections(data ? String(data.max_connections) : '') + setSubmitMessage('') + setSubmitError(false) + setSaving(false) + dialogRef.current?.showModal() + } + const metrics = [ { label: 'Active connections', value: data?.active_connections }, { label: 'Current connections', value: data?.current_connections }, @@ -43,7 +135,14 @@ export default function NodeActiveConnections({ nodeId, nodeName }: { nodeId: st {metrics.map(({ label, value }) => (

{label}

- {value?.toLocaleString() ?? '—'} + {label === 'Max connections' ? ( +
+ {value?.toLocaleString() ?? '—'} + +
+ ) : {value?.toLocaleString() ?? '—'}}
))}
@@ -52,6 +151,51 @@ export default function NodeActiveConnections({ nodeId, nodeName }: { nodeId: st {data && }
+ { if (saving) event.preventDefault() }}> +
+

Adjust max connection

+ +
+ + + + + + + + + + +
RECOMMENDED RAM SIZECONNECTIONS
2–4 GB RAM50–100
8 GB RAM100–300
16 GB RAM200–500
32+ GB RAM300–1000+
+
{ + event.preventDefault() + void updateMaxConnections() + }}> + + { + const nextValue = event.target.value + if (nextValue !== '' && (!/^[1-9][0-9]*$/.test(nextValue) || !Number.isSafeInteger(Number(nextValue)))) return + setSubmitError(false) + setMaxConnections(nextValue) + setSubmitMessage('') + }} + /> + {submitMessage &&

{submitMessage}

} +
+ + +
+
+
) }