fix: add inline token editor to gateway card (#459) (#471)

The gateway card showed token status as read-only (set/none) with no
way to update it. Users with a registered gateway but missing token
had to delete and re-add the gateway.

Add [edit] link next to the token indicator that expands an inline
password input. Supports Enter to save, Escape to cancel. Calls
PUT /api/gateways with the token field (already supported by API).
This commit is contained in:
nyk 2026-03-22 16:25:04 +07:00 committed by GitHub
parent 60f6dc07a1
commit 5bc5737d56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 59 additions and 2 deletions

View File

@ -160,6 +160,15 @@ export function MultiGatewayPanel() {
fetchHistory() fetchHistory()
} }
const updateToken = async (gw: Gateway, token: string) => {
await fetch('/api/gateways', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: gw.id, token }),
})
fetchGateways()
}
const connectTo = async (gw: Gateway) => { const connectTo = async (gw: Gateway) => {
try { try {
const res = await fetch('/api/gateways/connect', { const res = await fetch('/api/gateways/connect', {
@ -285,6 +294,7 @@ export function MultiGatewayPanel() {
onDelete={() => deleteGateway(gw.id)} onDelete={() => deleteGateway(gw.id)}
onConnect={() => connectTo(gw)} onConnect={() => connectTo(gw)}
onProbe={() => probeGateway(gw)} onProbe={() => probeGateway(gw)}
onUpdateToken={(token) => updateToken(gw, token)}
/> />
))} ))}
</div> </div>
@ -437,7 +447,7 @@ export function MultiGatewayPanel() {
) )
} }
function GatewayCard({ gateway, health, historyEntries = [], isProbing, isCurrentlyConnected, onSetPrimary, onDelete, onConnect, onProbe }: { function GatewayCard({ gateway, health, historyEntries = [], isProbing, isCurrentlyConnected, onSetPrimary, onDelete, onConnect, onProbe, onUpdateToken }: {
gateway: Gateway gateway: Gateway
health?: GatewayHealthProbe health?: GatewayHealthProbe
historyEntries?: GatewayHealthLogEntry[] historyEntries?: GatewayHealthLogEntry[]
@ -447,8 +457,11 @@ function GatewayCard({ gateway, health, historyEntries = [], isProbing, isCurren
onDelete: () => void onDelete: () => void
onConnect: () => void onConnect: () => void
onProbe: () => void onProbe: () => void
onUpdateToken: (token: string) => void
}) { }) {
const t = useTranslations('multiGateway') const t = useTranslations('multiGateway')
const [editingToken, setEditingToken] = useState(false)
const [tokenInput, setTokenInput] = useState('')
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
online: 'bg-green-500', online: 'bg-green-500',
offline: 'bg-red-500', offline: 'bg-red-500',
@ -487,10 +500,54 @@ function GatewayCard({ gateway, health, historyEntries = [], isProbing, isCurren
</div> </div>
<div className="flex items-center gap-4 mt-1.5 text-xs text-muted-foreground"> <div className="flex items-center gap-4 mt-1.5 text-xs text-muted-foreground">
<span className="font-mono">{gateway.host}:{gateway.port}</span> <span className="font-mono">{gateway.host}:{gateway.port}</span>
<span>{t('token')}: {gateway.token_set ? t('tokenSet') : t('tokenNone')}</span> <button
onClick={() => { setEditingToken(!editingToken); setTokenInput('') }}
className="hover:text-foreground transition-colors cursor-pointer"
title={gateway.token_set ? 'Change gateway token' : 'Set gateway token'}
>
{t('token')}: {gateway.token_set ? t('tokenSet') : t('tokenNone')} [edit]
</button>
{gateway.latency != null && <span>{t('latency')}: {gateway.latency}ms</span>} {gateway.latency != null && <span>{t('latency')}: {gateway.latency}ms</span>}
<span>{t('last')}: {lastSeen}</span> <span>{t('last')}: {lastSeen}</span>
</div> </div>
{editingToken && (
<div className="mt-2 flex items-center gap-2">
<input
type="password"
value={tokenInput}
onChange={e => setTokenInput(e.target.value)}
placeholder="Paste gateway token..."
className="flex-1 px-2 py-1 text-xs bg-secondary border border-border rounded font-mono"
autoFocus
onKeyDown={e => {
if (e.key === 'Enter' && tokenInput.trim()) {
onUpdateToken(tokenInput.trim())
setEditingToken(false)
setTokenInput('')
} else if (e.key === 'Escape') {
setEditingToken(false)
setTokenInput('')
}
}}
/>
<Button
onClick={() => { onUpdateToken(tokenInput.trim()); setEditingToken(false); setTokenInput('') }}
disabled={!tokenInput.trim()}
size="xs"
className="text-2xs"
>
Save
</Button>
<Button
onClick={() => { setEditingToken(false); setTokenInput('') }}
variant="ghost"
size="xs"
className="text-2xs"
>
Cancel
</Button>
</div>
)}
{health?.gateway_version && ( {health?.gateway_version && (
<div className="mt-1 text-2xs text-muted-foreground"> <div className="mt-1 text-2xs text-muted-foreground">
{t('gatewayVersion')}: <span className="font-mono text-foreground/80">{health.gateway_version}</span> {t('gatewayVersion')}: <span className="font-mono text-foreground/80">{health.gateway_version}</span>