--ADDED MAX CONNECTIONS BUTTON ADDED
	--ADDED POP UP TO ADJ THIS CONNECTION
This commit is contained in:
george committed 2026-09-30 12:59:37 +05:30
1 parent 45d9e22ae2
commit ae96ca4756
2 files changed
+327

No files matched your search

+183
View File
@@ -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%;
}
}
+144
View File
@@ -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<HTMLDialogElement>(null)
const [maxConnections, setMaxConnections] = useState('')
const [submitMessage, setSubmitMessage] = useState('')
const [saving, setSaving] = useState(false)
const [submitError, setSubmitError] = useState(false)
const requestRef = useRef<AbortController | null>(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 }) => (
<div className="database-connection-metric" key={label}>
<h3>{label}</h3>
{label === 'Max connections' ? (
<div className="max-connections-value-row">
<strong>{value?.toLocaleString() ?? '—'}</strong>
<button className="adjust-max-connections-button" type="button" onClick={openAdjustment}>
Adjust max connection
</button>
</div>
) : <strong>{value?.toLocaleString() ?? '—'}</strong>}
</div>
))}
<div className="database-connection-metric">
@@ -52,6 +151,51 @@ export default function NodeActiveConnections({ nodeId, nodeName }: { nodeId: st
{data && <meter aria-label="Connection usage" min={0} max={100} value={data.connection_usage_percent} />}
</div>
</div>
<dialog className="max-connections-dialog" ref={dialogRef} aria-labelledby="max-connections-dialog-title" onCancel={(event) => { if (saving) event.preventDefault() }}>
<div className="max-connections-dialog-header">
<h2 id="max-connections-dialog-title">Adjust max connection</h2>
<button className="secondary-button" type="button" disabled={saving} aria-label="Close popup" onClick={() => dialogRef.current?.close()}>×</button>
</div>
<table className="max-connections-recommendations">
<thead>
<tr><th scope="col">RECOMMENDED RAM SIZE</th><th scope="col">CONNECTIONS</th></tr>
</thead>
<tbody>
<tr><td>2–4 GB RAM</td><td>50–100</td></tr>
<tr><td>8 GB RAM</td><td>100–300</td></tr>
<tr><td>16 GB RAM</td><td>200–500</td></tr>
<tr><td>32+ GB RAM</td><td>300–1000+</td></tr>
</tbody>
</table>
<form className="max-connections-form" onSubmit={(event) => {
event.preventDefault()
void updateMaxConnections()
}}>
<label htmlFor="max-connections-input">Max connections</label>
<input
id="max-connections-input"
type="text"
inputMode="numeric"
pattern="[1-9][0-9]*"
title="Enter a positive whole number (1 or greater)."
required
disabled={saving}
value={maxConnections}
onChange={(event) => {
const nextValue = event.target.value
if (nextValue !== '' && (!/^[1-9][0-9]*$/.test(nextValue) || !Number.isSafeInteger(Number(nextValue)))) return
setSubmitError(false)
setMaxConnections(nextValue)
setSubmitMessage('')
}}
/>
{submitMessage && <p className={submitError ? 'status-message error' : undefined} role={submitError ? 'alert' : 'status'}>{submitMessage}</p>}
<div className="max-connections-dialog-actions">
<button className="secondary-button" type="button" disabled={saving} onClick={() => dialogRef.current?.close()}>Cancel</button>
<button className="primary-button" type="submit" disabled={saving}>{saving ? 'Updating…' : 'Update max connections'}</button>
</div>
</form>
</dialog>
</section>
)
}