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]>
228 lines
7.5 KiB
TypeScript
228 lines
7.5 KiB
TypeScript
import { useContext, useState } from 'react'
|
||
import { getSettings, setSettings, temporaryDisableBlocking } from '../../api/settings'
|
||
import { Confirm } from '../../ui/Confirm'
|
||
import { Menu } from '../../ui/Menu'
|
||
import { Button } from '../../ui/Button'
|
||
import { Tooltip } from '../../ui/Tooltip'
|
||
import { nodeArg, useHubWrite } from '../../lib/hub-write'
|
||
import { HubAll } from '../../lib/hub-all'
|
||
import type { AlertType } from '../../ui/Alert'
|
||
import { noticeFromFailure } from '../../lib/notice'
|
||
|
||
/*
|
||
The Dashboard's "Blocking" menu.
|
||
|
||
Upstream puts it in the header of "Top Blocked Domains"
|
||
(`btnDashboardBlockingOptions`, index.html) and here it was missing: turning
|
||
blocking off for a while from the Dashboard is one of the things most often done
|
||
in this console (the typical case is a page not loading because of a list), and it
|
||
forced a trip to Settings › Blocking to find it.
|
||
|
||
Two behavioural details copied as they are:
|
||
|
||
- The state is asked for ON OPENING the menu, not when drawing the screen
|
||
(`main.js:2429`): between the two the setting may have changed from another
|
||
tab, and showing "Enable Blocking" with blocking already on is worse than
|
||
taking 100 ms.
|
||
- Either "Enable" or "Disable" comes out, never both. While it is not known,
|
||
neither.
|
||
|
||
The confirmation and alert texts are upstream's literals
|
||
(`main.js:2448-2496`). What changes is the way of asking: there it is a browser
|
||
`confirm()` and here the console's dialog.
|
||
*/
|
||
|
||
/** Upstream's eight durations, with their exact labels. */
|
||
const DURATIONS: { minutes: number; text: string }[] = [
|
||
{ minutes: 1, text: 'Disable Blocking For 1 Minute' },
|
||
{ minutes: 2, text: 'Disable Blocking For 2 Minutes' },
|
||
{ minutes: 5, text: 'Disable Blocking For 5 Minutes' },
|
||
{ minutes: 10, text: 'Disable Blocking For 10 Minutes' },
|
||
{ minutes: 15, text: 'Disable Blocking For 15 Minutes' },
|
||
{ minutes: 30, text: 'Disable Blocking For 30 Minutes' },
|
||
{ minutes: 60, text: 'Disable Blocking For 1 Hour' },
|
||
{ minutes: 180, text: 'Disable Blocking For 3 Hours' },
|
||
]
|
||
|
||
interface Pending {
|
||
title: string
|
||
text: string
|
||
label: string
|
||
variant: 'primary' | 'danger'
|
||
perform: () => Promise<void>
|
||
}
|
||
|
||
export function BlockingMenu({
|
||
token,
|
||
onNotice,
|
||
text = 'Blocking',
|
||
onChanged,
|
||
node = '',
|
||
allowed = true,
|
||
}: {
|
||
token: string | null
|
||
/** Behind a hub: the server the change is made on (empty or "cluster": none chosen, so it is switched off). */
|
||
node?: string
|
||
/** Behind a hub: what the role may do. */
|
||
allowed?: boolean
|
||
onNotice: (a: { type: AlertType; title: string; text: string }) => void
|
||
/** The trigger text. The Dashboard keeps upstream's "Blocking". */
|
||
text?: string
|
||
/** After a successful change, for a caller that draws the state. */
|
||
onChanged?: () => void
|
||
}) {
|
||
const [active, setActive] = useState<boolean | null>(null)
|
||
const [pending, setPending] = useState<Pending | null>(null)
|
||
const [busy, setBusy] = useState(false)
|
||
/* Behind a hub with "All servers" chosen: the choice opens the flow that makes it on the servers picked next. */
|
||
const all = useContext(HubAll)
|
||
|
||
async function loadState() {
|
||
setActive(null)
|
||
const s = await getSettings(token, ...nodeArg(node))
|
||
if (s != null) setActive(s.enableBlocking)
|
||
}
|
||
|
||
function flip(turnOn: boolean): Pending {
|
||
return {
|
||
title: turnOn ? 'Enable Blocking' : 'Disable Blocking',
|
||
text: `Are you sure you want to ${turnOn ? 'enable' : 'disable'} blocking?`,
|
||
label: turnOn ? 'Enable' : 'Disable',
|
||
variant: turnOn ? 'primary' : 'danger',
|
||
perform: async () => {
|
||
const r = await setSettings(token, { enableBlocking: String(turnOn), ...(node ? { node } : {}) })
|
||
// The same text as the other thirty-six: it was the only one that said
|
||
// a bare "Session expired." for this very condition.
|
||
if (r.kind !== 'ok') throw new Error(noticeFromFailure(r).text)
|
||
setActive(turnOn)
|
||
onNotice({
|
||
type: 'success',
|
||
title: turnOn ? 'Blocking Enabled!' : 'Blocking Disabled!',
|
||
text: `Blocking was ${turnOn ? 'enabled' : 'disabled'} successfully.`,
|
||
})
|
||
},
|
||
}
|
||
}
|
||
|
||
function forAWhile(minutes: number): Pending {
|
||
return {
|
||
title: 'Temporarily Disable Blocking',
|
||
text: `Are you sure to temporarily disable blocking for ${minutes} minute(s)?`,
|
||
label: 'Disable',
|
||
variant: 'danger',
|
||
perform: async () => {
|
||
const till = await temporaryDisableBlocking(token, String(minutes), ...nodeArg(node))
|
||
if (till == null) throw new Error('The request failed.')
|
||
setActive(false)
|
||
onNotice({
|
||
type: 'success',
|
||
title: 'Blocking Disabled!',
|
||
text: `Blocking was successfully disabled temporarily for ${minutes} minute(s).`,
|
||
})
|
||
},
|
||
}
|
||
}
|
||
|
||
async function confirm() {
|
||
if (pending == null) return
|
||
setBusy(true)
|
||
try {
|
||
await pending.perform()
|
||
onChanged?.()
|
||
setPending(null)
|
||
} catch (e) {
|
||
onNotice({ type: 'danger', title: 'Error!', text: (e as Error).message })
|
||
setPending(null)
|
||
} finally {
|
||
setBusy(false)
|
||
}
|
||
}
|
||
|
||
/* Behind a hub, only on a chosen server the hub can write to; the tooltip says why not. */
|
||
const write = useHubWrite(node, allowed)
|
||
if (all !== null) {
|
||
return (
|
||
<Menu label="Blocking options" text={text}>
|
||
{(close) => (
|
||
<>
|
||
<button
|
||
role="menuitem"
|
||
onClick={() => {
|
||
close()
|
||
all({ action: 'blocking.enable', title: 'Enable blocking on several servers', confirmLabel: 'Enable blocking' })
|
||
}}
|
||
>
|
||
Enable Blocking
|
||
</button>
|
||
{DURATIONS.map((p) => (
|
||
<button
|
||
key={p.minutes}
|
||
role="menuitem"
|
||
onClick={() => {
|
||
close()
|
||
all({
|
||
action: 'blocking.disable',
|
||
params: { minutes: String(p.minutes) },
|
||
title: `Disable blocking for ${p.minutes} minute(s) on several servers`,
|
||
confirmLabel: 'Disable blocking',
|
||
})
|
||
}}
|
||
>
|
||
{p.text}
|
||
</button>
|
||
))}
|
||
</>
|
||
)}
|
||
</Menu>
|
||
)
|
||
}
|
||
if (!write.allowed) {
|
||
return (
|
||
<Tooltip text={write.reason ?? 'Requires Settings: Modify'}>
|
||
<Button disabled>{text}</Button>
|
||
</Tooltip>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<Menu label="Blocking options" text={text} onOpen={() => void loadState()}>
|
||
{(close) => (
|
||
<>
|
||
{active === false && (
|
||
<button role="menuitem" onClick={() => { close(); setPending(flip(true)) }}>
|
||
Enable Blocking
|
||
</button>
|
||
)}
|
||
{active === true && (
|
||
<button role="menuitem" onClick={() => { close(); setPending(flip(false)) }}>
|
||
Disable Blocking
|
||
</button>
|
||
)}
|
||
{DURATIONS.map((p) => (
|
||
<button
|
||
key={p.minutes}
|
||
role="menuitem"
|
||
onClick={() => { close(); setPending(forAWhile(p.minutes)) }}
|
||
>
|
||
{p.text}
|
||
</button>
|
||
))}
|
||
</>
|
||
)}
|
||
</Menu>
|
||
|
||
<Confirm
|
||
open={pending != null}
|
||
title={pending?.title ?? ''}
|
||
text={pending?.text ?? ''}
|
||
label={pending?.label ?? ''}
|
||
variant={pending?.variant}
|
||
busy={busy}
|
||
onClose={() => setPending(null)}
|
||
onConfirm={() => void confirm()}
|
||
/>
|
||
</>
|
||
)
|
||
}
|