One endpoint, hub/servers/run (operate), runs a fixed set of actions on the chosen servers in parallel with each server's admin token: add or remove a domain in Blocked or Allowed, disable blocking for a time or enable it, update block lists, replace the block list URLs, flush the cache or delete a domain from it. Each server gets its own result and the console offers Retry failed. In "All servers", Blocking, Cache and Query Logs rows open this flow instead of being disabled. The hub validates domains, minutes and the server's list grammar (allow lists with !, switched-off lines with #) and logs no URL list. Operators can now use the blocking switch. Co-Authored-By: Claude Opus 5.5 <[email protected]>
93 lines
2.4 KiB
TypeScript
93 lines
2.4 KiB
TypeScript
import { useState, type ReactNode } from 'react'
|
|
import { Button } from './Button'
|
|
import { Dialog } from './Dialog'
|
|
import { Field, Input, Textarea } from './Field'
|
|
|
|
/*
|
|
One question in a dialog: a field (a line, or several with `multiline`) and a button. For the
|
|
step before a change on several servers that needs a value, a domain or a list of URLs, which
|
|
the next step (the server picker) does not ask for. `check` returns the sentence that says what
|
|
is wrong with the text, or `null` when it is fine; it is shown under the field and the answer
|
|
is not passed on.
|
|
*/
|
|
export function AskDialog({
|
|
title,
|
|
label,
|
|
intro,
|
|
initial = '',
|
|
placeholder,
|
|
multiline = false,
|
|
confirmLabel,
|
|
check,
|
|
onSubmit,
|
|
onClose,
|
|
}: {
|
|
title: string
|
|
label: string
|
|
intro?: ReactNode
|
|
initial?: string
|
|
placeholder?: string
|
|
multiline?: boolean
|
|
confirmLabel: string
|
|
check?: (text: string) => string | null
|
|
onSubmit: (text: string) => void
|
|
onClose: () => void
|
|
}) {
|
|
const [text, setText] = useState(initial)
|
|
const [problem, setProblem] = useState<string | null>(null)
|
|
|
|
function submit() {
|
|
const wrong = check?.(text) ?? null
|
|
setProblem(wrong)
|
|
if (wrong === null) onSubmit(text)
|
|
}
|
|
|
|
return (
|
|
<Dialog
|
|
open
|
|
onOpenChange={onClose}
|
|
title={title}
|
|
close="Cancel"
|
|
actions={
|
|
<Button variant="primary" onClick={submit}>
|
|
{confirmLabel}
|
|
</Button>
|
|
}
|
|
>
|
|
{intro != null && <p className="m-0 text-sm text-muted-foreground">{intro}</p>}
|
|
<Field label={label}>
|
|
{(id) =>
|
|
multiline ? (
|
|
<Textarea
|
|
id={id}
|
|
mono
|
|
rows={8}
|
|
placeholder={placeholder}
|
|
value={text}
|
|
aria-invalid={problem !== null || undefined}
|
|
onChange={(e) => setText(e.target.value)}
|
|
/>
|
|
) : (
|
|
<Input
|
|
id={id}
|
|
mono
|
|
placeholder={placeholder}
|
|
value={text}
|
|
aria-invalid={problem !== null || undefined}
|
|
onChange={(e) => setText(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') submit()
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
</Field>
|
|
{problem !== null && (
|
|
<p className="m-0 text-xs text-destructive" role="alert">
|
|
{problem}
|
|
</p>
|
|
)}
|
|
</Dialog>
|
|
)
|
|
}
|