web: give the toast helper a real type and keep the dist bundle out of git
Blocklists, Clients, Domains and Upstreams each carried a ToastLike shim
that duck-typed a react-hot-toast style API. The console uses its own
Toast context, so notify() was calling toast('some message') with a string
where ToastOptions is expected: every mutation toast rendered an empty
title. They now take the toast function from useToast() and send a real
{ title, tone } payload, mapping 'fail' onto the danger tone.
This commit is contained in:
1 parent
85453852d5
commit
f24fd63e9a
4 files changed
+102
-96
No files matched your search
@@ -10,12 +10,12 @@ import { useMemo, useState } from 'react'
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { AlertTriangle, Clock, Layers, RefreshCw, ShieldCheck, Split, Trash2 } from 'lucide-react'
|
import { AlertTriangle, Clock, Layers, RefreshCw, ShieldCheck, Split, Trash2 } from 'lucide-react'
|
||||||
|
|
||||||
import { checkDomain, getBlocklists, refreshBlocklists, toggleBlocklist } from '../../lib/api'
|
import { checkDomain, getBlocklists, refreshBlocklists, toggleBlocklist } from '../lib/api'
|
||||||
import type { BlocklistItem, BlocklistStats, DomainCheck } from '../../lib/types'
|
import type { BlocklistItem, BlocklistStats, DomainCheck } from '../lib/types'
|
||||||
import { useApi, useAsyncAction } from '../../hooks/useApi'
|
import { useApi, useAsyncAction } from '../hooks/useApi'
|
||||||
import { useAutoRefresh } from '../../hooks/useAutoRefresh'
|
import { useAutoRefresh } from '../hooks/useAutoRefresh'
|
||||||
import { useDebounced } from '../../hooks/useDebounced'
|
import { useDebounced } from '../hooks/useDebounced'
|
||||||
import { formatBytes, formatMs, formatNumber, formatRelative, toDate } from '../../lib/format'
|
import { formatBytes, formatMs, formatNumber, formatRelative, toDate } from '../lib/format'
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -29,21 +29,21 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
useToast,
|
useToast,
|
||||||
} from '../components/ui'
|
} from '../components/ui'
|
||||||
|
import type { ToastOptions } from '../components/ui'
|
||||||
import { PageError, PageHeader } from '../components/layout'
|
import { PageError, PageHeader } from '../components/layout'
|
||||||
|
|
||||||
type Row = Record<string, ReactNode>
|
type Row = Record<string, ReactNode>
|
||||||
|
|
||||||
type ToastLike = {
|
/**
|
||||||
success?: (message: string) => void
|
* One line for every mutation on this page: the message plus the tone it
|
||||||
error?: (message: string) => void
|
* earned. `fail` is the danger tone, `ok` the success one.
|
||||||
toast?: (message: string) => void
|
*/
|
||||||
push?: (message: string) => void
|
function notify(
|
||||||
}
|
toast: (options: ToastOptions) => void,
|
||||||
|
kind: 'ok' | 'fail',
|
||||||
function notify(toast: unknown, kind: 'ok' | 'fail', message: string): void {
|
message: string,
|
||||||
const t = toast as ToastLike
|
): void {
|
||||||
const fn = kind === 'ok' ? (t.success ?? t.toast ?? t.push) : (t.error ?? t.toast ?? t.push)
|
toast({ title: message, tone: kind === 'ok' ? 'ok' : 'danger' })
|
||||||
fn?.call(t, message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_STATS: BlocklistStats = {
|
const EMPTY_STATS: BlocklistStats = {
|
||||||
@@ -79,7 +79,7 @@ function truncate(value: string, max = 42): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Blocklists() {
|
export default function Blocklists() {
|
||||||
const toast = useToast()
|
const { toast } = useToast()
|
||||||
const { data, error, loading, refetch } = useApi(() => getBlocklists(), [])
|
const { data, error, loading, refetch } = useApi(() => getBlocklists(), [])
|
||||||
useAutoRefresh(refetch, 15000)
|
useAutoRefresh(refetch, 15000)
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ export default function Blocklists() {
|
|||||||
),
|
),
|
||||||
refresh: (
|
refresh: (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<RefreshCw size={14} />}
|
icon={RefreshCw}
|
||||||
label={`refresh ${item.name}`}
|
label={`refresh ${item.name}`}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
@@ -221,7 +221,7 @@ export default function Blocklists() {
|
|||||||
description="Compiled rule sources, their freshness and their health."
|
description="Compiled rule sources, their freshness and their health."
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<Button
|
||||||
icon={<RefreshCw size={14} />}
|
icon={RefreshCw}
|
||||||
onClick={() => void onRefresh()}
|
onClick={() => void onRefresh()}
|
||||||
loading={refresh.pending}
|
loading={refresh.pending}
|
||||||
>
|
>
|
||||||
@@ -231,24 +231,24 @@ export default function Blocklists() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-3 xl:grid-cols-5">
|
<div className="grid grid-cols-2 gap-3 lg:grid-cols-3 xl:grid-cols-5">
|
||||||
<StatCard label="Total rules" value={formatNumber(stats.total)} icon={<Layers size={16} />} />
|
<StatCard label="Total rules" value={formatNumber(stats.total)} icon={Layers} />
|
||||||
<StatCard label="Sources" value={formatNumber(stats.sources)} hint={`${items.filter((i) => i.enabled).length} enabled`} />
|
<StatCard label="Sources" value={formatNumber(stats.sources)} hint={`${items.filter((i) => i.enabled).length} enabled`} />
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Allow rules"
|
label="Allow rules"
|
||||||
value={formatNumber(stats.allow)}
|
value={formatNumber(stats.allow)}
|
||||||
icon={<ShieldCheck size={16} />}
|
icon={ShieldCheck}
|
||||||
tone={stats.allow > 0 ? 'default' : 'warn'}
|
tone={stats.allow > 0 ? 'default' : 'warn'}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Duplicates"
|
label="Duplicates"
|
||||||
value={formatNumber(stats.duplicates)}
|
value={formatNumber(stats.duplicates)}
|
||||||
icon={<Split size={16} />}
|
icon={Split}
|
||||||
tone={stats.duplicates > 0 ? 'warn' : 'ok'}
|
tone={stats.duplicates > 0 ? 'warn' : 'ok'}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Last compile"
|
label="Last compile"
|
||||||
value={compiledAgo}
|
value={compiledAgo}
|
||||||
icon={<Clock size={16} />}
|
icon={Clock}
|
||||||
hint={stats.built_at ? undefined : 'no build recorded'}
|
hint={stats.built_at ? undefined : 'no build recorded'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -265,7 +265,7 @@ export default function Blocklists() {
|
|||||||
placeholder="ads.example.com"
|
placeholder="ads.example.com"
|
||||||
value={probe}
|
value={probe}
|
||||||
onChange={(e) => setProbe(e.target.value)}
|
onChange={(e) => setProbe(e.target.value)}
|
||||||
icon={<ShieldCheck size={14} />}
|
icon={ShieldCheck}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+23
-23
@@ -10,10 +10,10 @@
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { Copy, Pencil, Plus, Trash2, Users } from 'lucide-react'
|
import { Copy, Pencil, Plus, Trash2, Users } from 'lucide-react'
|
||||||
|
|
||||||
import { ApiError, getClients, putClients } from '../../lib/api'
|
import { ApiError, getClients, putClients } from '../lib/api'
|
||||||
import type { ClientGroup, ClientMatch } from '../../lib/types'
|
import type { ClientGroup, ClientMatch } from '../lib/types'
|
||||||
import { useApi, useAsyncAction } from '../../hooks/useApi'
|
import { useApi, useAsyncAction } from '../hooks/useApi'
|
||||||
import { fromLines, toLines } from '../../lib/format'
|
import { fromLines, toLines } from '../lib/format'
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -28,19 +28,19 @@ import {
|
|||||||
Textarea,
|
Textarea,
|
||||||
useToast,
|
useToast,
|
||||||
} from '../components/ui'
|
} from '../components/ui'
|
||||||
|
import type { ToastOptions } from '../components/ui'
|
||||||
import { PageError, PageHeader } from '../components/layout'
|
import { PageError, PageHeader } from '../components/layout'
|
||||||
|
|
||||||
type ToastLike = {
|
/**
|
||||||
success?: (message: string) => void
|
* One line for every mutation on this page: the message plus the tone it
|
||||||
error?: (message: string) => void
|
* earned. `fail` is the danger tone, `ok` the success one.
|
||||||
toast?: (message: string) => void
|
*/
|
||||||
push?: (message: string) => void
|
function notify(
|
||||||
}
|
toast: (options: ToastOptions) => void,
|
||||||
|
kind: 'ok' | 'fail',
|
||||||
function notify(toast: unknown, kind: 'ok' | 'fail', message: string): void {
|
message: string,
|
||||||
const t = toast as ToastLike
|
): void {
|
||||||
const fn = kind === 'ok' ? (t.success ?? t.toast ?? t.push) : (t.error ?? t.toast ?? t.push)
|
toast({ title: message, tone: kind === 'ok' ? 'ok' : 'danger' })
|
||||||
fn?.call(t, message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const POLICIES = [
|
const POLICIES = [
|
||||||
@@ -173,11 +173,11 @@ function fieldLeaf(path: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Clients() {
|
export default function Clients() {
|
||||||
const toast = useToast()
|
const { toast } = useToast()
|
||||||
const { data, error, loading, refetch } = useApi(() => getClients(), [])
|
const { data, error, loading, refetch } = useApi(() => getClients(), [])
|
||||||
|
|
||||||
const groups = useMemo<ClientGroup[]>(() => data?.groups ?? [], [data])
|
const groups = useMemo<ClientGroup[]>(() => data?.groups ?? [], [data])
|
||||||
const [defaultPolicy, setDefaultPolicy] = useState<string>('')
|
const [defaultPolicy] = useState<string>('')
|
||||||
const [defaultDraft, setDefaultDraft] = useState<string | null>(null)
|
const [defaultDraft, setDefaultDraft] = useState<string | null>(null)
|
||||||
const [editorOpen, setEditorOpen] = useState(false)
|
const [editorOpen, setEditorOpen] = useState(false)
|
||||||
const [draft, setDraft] = useState<GroupDraft>(EMPTY_DRAFT)
|
const [draft, setDraft] = useState<GroupDraft>(EMPTY_DRAFT)
|
||||||
@@ -302,7 +302,7 @@ export default function Clients() {
|
|||||||
title="Clients"
|
title="Clients"
|
||||||
description="Group clients by network, reverse-DNS name or tag and give each group its own policy."
|
description="Group clients by network, reverse-DNS name or tag and give each group its own policy."
|
||||||
actions={
|
actions={
|
||||||
<Button icon={<Plus size={14} />} onClick={openNew}>
|
<Button icon={Plus} onClick={openNew}>
|
||||||
Add group
|
Add group
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -322,11 +322,11 @@ export default function Clients() {
|
|||||||
|
|
||||||
{groups.length === 0 ? (
|
{groups.length === 0 ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={<Users size={20} />}
|
icon={Users}
|
||||||
title="No client groups"
|
title="No client groups"
|
||||||
description="Without a group every client gets the default policy."
|
description="Without a group every client gets the default policy."
|
||||||
action={
|
action={
|
||||||
<Button icon={<Plus size={14} />} onClick={openNew}>
|
<Button icon={Plus} onClick={openNew}>
|
||||||
Add the first group
|
Add the first group
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -362,10 +362,10 @@ export default function Clients() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 gap-1">
|
<div className="flex shrink-0 gap-1">
|
||||||
<IconButton icon={<Pencil size={14} />} label={`edit ${group.name}`} variant="ghost" onClick={() => openEdit(i)} />
|
<IconButton icon={Pencil} label={`edit ${group.name}`} variant="ghost" onClick={() => openEdit(i)} />
|
||||||
<IconButton icon={<Copy size={14} />} label={`duplicate ${group.name}`} variant="ghost" onClick={() => duplicate(i)} />
|
<IconButton icon={Copy} label={`duplicate ${group.name}`} variant="ghost" onClick={() => duplicate(i)} />
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<Trash2 size={14} />}
|
icon={Trash2}
|
||||||
label={`delete ${group.name}`}
|
label={`delete ${group.name}`}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setPendingDelete(i)}
|
onClick={() => setPendingDelete(i)}
|
||||||
|
|||||||
+22
-21
@@ -10,15 +10,16 @@ import { useEffect, useMemo, useState } from 'react'
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { Plus, Search, Trash2 } from 'lucide-react'
|
import { Plus, Search, Trash2 } from 'lucide-react'
|
||||||
|
|
||||||
import { addDomain, deleteDomain, getDomains } from '../../lib/api'
|
import { addDomain, deleteDomain, getDomains } from '../lib/api'
|
||||||
import type { AddDomainInput } from '../../lib/api'
|
import type { AddDomainInput } from '../lib/api'
|
||||||
import type { DomainItem } from '../../lib/types'
|
import type { DomainItem } from '../lib/types'
|
||||||
import { useApi, useAsyncAction } from '../../hooks/useApi'
|
import { useApi, useAsyncAction } from '../hooks/useApi'
|
||||||
import { useDebounced } from '../../hooks/useDebounced'
|
import { useDebounced } from '../hooks/useDebounced'
|
||||||
import { formatNumber, formatRelative, toLines } from '../../lib/format'
|
import { formatNumber, formatRelative, toLines } from '../lib/format'
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
|
Card,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
Dialog,
|
Dialog,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
@@ -30,21 +31,21 @@ import {
|
|||||||
Textarea,
|
Textarea,
|
||||||
useToast,
|
useToast,
|
||||||
} from '../components/ui'
|
} from '../components/ui'
|
||||||
|
import type { ToastOptions } from '../components/ui'
|
||||||
import { PageError, PageHeader } from '../components/layout'
|
import { PageError, PageHeader } from '../components/layout'
|
||||||
|
|
||||||
type Row = Record<string, ReactNode>
|
type Row = Record<string, ReactNode>
|
||||||
|
|
||||||
type ToastLike = {
|
/**
|
||||||
success?: (message: string) => void
|
* One line for every mutation on this page: the message plus the tone it
|
||||||
error?: (message: string) => void
|
* earned. `fail` is the danger tone, `ok` the success one.
|
||||||
toast?: (message: string) => void
|
*/
|
||||||
push?: (message: string) => void
|
function notify(
|
||||||
}
|
toast: (options: ToastOptions) => void,
|
||||||
|
kind: 'ok' | 'fail',
|
||||||
function notify(toast: unknown, kind: 'ok' | 'fail', message: string): void {
|
message: string,
|
||||||
const t = toast as ToastLike
|
): void {
|
||||||
const fn = kind === 'ok' ? (t.success ?? t.toast ?? t.push) : (t.error ?? t.toast ?? t.push)
|
toast({ title: message, tone: kind === 'ok' ? 'ok' : 'danger' })
|
||||||
fn?.call(t, message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PER_PAGE = 50
|
const PER_PAGE = 50
|
||||||
@@ -88,7 +89,7 @@ type AddForm = {
|
|||||||
const EMPTY_FORM: AddForm = { name: '', action: 'block', list: '', bulk: '', advanced: false }
|
const EMPTY_FORM: AddForm = { name: '', action: 'block', list: '', bulk: '', advanced: false }
|
||||||
|
|
||||||
export default function Domains() {
|
export default function Domains() {
|
||||||
const toast = useToast()
|
const { toast } = useToast()
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [action, setAction] = useState('')
|
const [action, setAction] = useState('')
|
||||||
const [list, setList] = useState('')
|
const [list, setList] = useState('')
|
||||||
@@ -220,7 +221,7 @@ export default function Domains() {
|
|||||||
updated: <span className="whitespace-nowrap text-xs">{formatRelative(item.updated_at)}</span>,
|
updated: <span className="whitespace-nowrap text-xs">{formatRelative(item.updated_at)}</span>,
|
||||||
delete: (
|
delete: (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<Trash2 size={14} />}
|
icon={Trash2}
|
||||||
label={`delete ${item.name}`}
|
label={`delete ${item.name}`}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setPendingDelete(item)}
|
onClick={() => setPendingDelete(item)}
|
||||||
@@ -234,7 +235,7 @@ export default function Domains() {
|
|||||||
title="Domains"
|
title="Domains"
|
||||||
description="Inline allow and block rules, checked before the downloaded lists."
|
description="Inline allow and block rules, checked before the downloaded lists."
|
||||||
actions={
|
actions={
|
||||||
<Button icon={<Plus size={14} />} onClick={() => setAddOpen(true)}>
|
<Button icon={Plus} onClick={() => setAddOpen(true)}>
|
||||||
Add rule
|
Add rule
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -245,7 +246,7 @@ export default function Domains() {
|
|||||||
<Input
|
<Input
|
||||||
label="Search"
|
label="Search"
|
||||||
placeholder="ads.example.com"
|
placeholder="ads.example.com"
|
||||||
icon={<Search size={14} />}
|
icon={Search}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
|||||||
+32
-27
@@ -20,10 +20,10 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
import { getUpstreams, putUpstreams } from '../../lib/api'
|
import { getUpstreams, putUpstreams } from '../lib/api'
|
||||||
import type { UpstreamBootstrap, UpstreamServer } from '../../lib/types'
|
import type { UpstreamBootstrap, UpstreamHealth, UpstreamServer } from '../lib/types'
|
||||||
import { useApi, useAsyncAction } from '../../hooks/useApi'
|
import { useApi, useAsyncAction } from '../hooks/useApi'
|
||||||
import { formatLatency, formatMs, formatNumber, formatRate } from '../../lib/format'
|
import { formatLatency, formatMs, formatNumber, formatRate } from '../lib/format'
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -40,21 +40,21 @@ import {
|
|||||||
Textarea,
|
Textarea,
|
||||||
useToast,
|
useToast,
|
||||||
} from '../components/ui'
|
} from '../components/ui'
|
||||||
|
import type { ToastOptions } from '../components/ui'
|
||||||
import { PageError, PageHeader } from '../components/layout'
|
import { PageError, PageHeader } from '../components/layout'
|
||||||
|
|
||||||
type Row = Record<string, ReactNode>
|
type Row = Record<string, ReactNode>
|
||||||
|
|
||||||
type ToastLike = {
|
/**
|
||||||
success?: (message: string) => void
|
* One line for every mutation on this page: the message plus the tone it
|
||||||
error?: (message: string) => void
|
* earned. `fail` is the danger tone, `ok` the success one.
|
||||||
toast?: (message: string) => void
|
*/
|
||||||
push?: (message: string) => void
|
function notify(
|
||||||
}
|
toast: (options: ToastOptions) => void,
|
||||||
|
kind: 'ok' | 'fail',
|
||||||
function notify(toast: unknown, kind: 'ok' | 'fail', message: string): void {
|
message: string,
|
||||||
const t = toast as ToastLike
|
): void {
|
||||||
const fn = kind === 'ok' ? (t.success ?? t.toast ?? t.push) : (t.error ?? t.toast ?? t.push)
|
toast({ title: message, tone: kind === 'ok' ? 'ok' : 'danger' })
|
||||||
fn?.call(t, message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const POLICIES = [
|
const POLICIES = [
|
||||||
@@ -167,7 +167,7 @@ function splitList(value: string): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Upstreams() {
|
export default function Upstreams() {
|
||||||
const toast = useToast()
|
const { toast } = useToast()
|
||||||
const { data, error, loading, refetch } = useApi(() => getUpstreams(), [])
|
const { data, error, loading, refetch } = useApi(() => getUpstreams(), [])
|
||||||
|
|
||||||
const [draft, setDraft] = useState<Draft | null>(null)
|
const [draft, setDraft] = useState<Draft | null>(null)
|
||||||
@@ -210,10 +210,15 @@ export default function Upstreams() {
|
|||||||
insecure_skip_verify: s.insecure_skip_verify,
|
insecure_skip_verify: s.insecure_skip_verify,
|
||||||
weight: s.weight,
|
weight: s.weight,
|
||||||
comment: s.comment,
|
comment: s.comment,
|
||||||
health: s.health,
|
health: s.health as UpstreamHealth,
|
||||||
latency_ms: s.latency_ms,
|
latency_ms: s.latency_ms,
|
||||||
fail_rate: s.fail_rate,
|
fail_rate: s.fail_rate,
|
||||||
breaker_open: s.breaker_open,
|
breaker_open: s.breaker_open,
|
||||||
|
// Runtime counters are not part of a write: the server owns them and
|
||||||
|
// ignores these fields on PUT /upstreams.
|
||||||
|
queries: 0,
|
||||||
|
avg_ms: 0,
|
||||||
|
p95_ms: 0,
|
||||||
})),
|
})),
|
||||||
bootstraps: working.bootstraps.map((b) => ({
|
bootstraps: working.bootstraps.map((b) => ({
|
||||||
name: b.name.trim(),
|
name: b.name.trim(),
|
||||||
@@ -314,10 +319,10 @@ export default function Upstreams() {
|
|||||||
comment: <span className="block max-w-[160px] truncate text-xs text-slate-500">{s.comment}</span>,
|
comment: <span className="block max-w-[160px] truncate text-xs text-slate-500">{s.comment}</span>,
|
||||||
order: (
|
order: (
|
||||||
<span className="flex justify-end gap-0.5">
|
<span className="flex justify-end gap-0.5">
|
||||||
<IconButton icon={<ArrowUp size={13} />} label={`move ${s.addr} up`} variant="ghost" disabled={i === 0} onClick={() => move(i, -1)} />
|
<IconButton icon={ArrowUp} label={`move ${s.addr} up`} variant="ghost" disabled={i === 0} onClick={() => move(i, -1)} />
|
||||||
<IconButton icon={<ArrowDown size={13} />} label={`move ${s.addr} down`} variant="ghost" disabled={i === working.servers.length - 1} onClick={() => move(i, 1)} />
|
<IconButton icon={ArrowDown} label={`move ${s.addr} down`} variant="ghost" disabled={i === working.servers.length - 1} onClick={() => move(i, 1)} />
|
||||||
<IconButton icon={<Pencil size={13} />} label={`edit ${s.addr}`} variant="ghost" onClick={() => { setEditing(s); setEditorOpen(true) }} />
|
<IconButton icon={Pencil} label={`edit ${s.addr}`} variant="ghost" onClick={() => { setEditing(s); setEditorOpen(true) }} />
|
||||||
<IconButton icon={<Trash2 size={13} />} label={`remove ${s.addr}`} variant="ghost" onClick={() => setPendingRemove(i)} />
|
<IconButton icon={Trash2} label={`remove ${s.addr}`} variant="ghost" onClick={() => setPendingRemove(i)} />
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
@@ -331,13 +336,13 @@ export default function Upstreams() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
icon={<RotateCw size={14} />}
|
icon={RotateCw}
|
||||||
onClick={() => setDraft(null)}
|
onClick={() => setDraft(null)}
|
||||||
disabled={!dirty}
|
disabled={!dirty}
|
||||||
>
|
>
|
||||||
Discard
|
Discard
|
||||||
</Button>
|
</Button>
|
||||||
<Button icon={<Save size={14} />} onClick={() => void commit()} loading={save.pending} disabled={!dirty}>
|
<Button icon={Save} onClick={() => void commit()} loading={save.pending} disabled={!dirty}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -386,7 +391,7 @@ export default function Upstreams() {
|
|||||||
actions={
|
actions={
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
icon={<Plus size={14} />}
|
icon={Plus}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditing(EMPTY_SERVER)
|
setEditing(EMPTY_SERVER)
|
||||||
setEditorOpen(true)
|
setEditorOpen(true)
|
||||||
@@ -414,7 +419,7 @@ export default function Upstreams() {
|
|||||||
actions={
|
actions={
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
icon={<Plus size={14} />}
|
icon={Plus}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setDraft({
|
setDraft({
|
||||||
...working,
|
...working,
|
||||||
@@ -428,7 +433,7 @@ export default function Upstreams() {
|
|||||||
>
|
>
|
||||||
{working.bootstraps.length === 0 ? (
|
{working.bootstraps.length === 0 ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={<Gauge size={20} />}
|
icon={Gauge}
|
||||||
title="No bootstrap resolvers"
|
title="No bootstrap resolvers"
|
||||||
description="Without them, an upstream address must already be an IP."
|
description="Without them, an upstream address must already be an IP."
|
||||||
/>
|
/>
|
||||||
@@ -457,7 +462,7 @@ export default function Upstreams() {
|
|||||||
placeholder="24h"
|
placeholder="24h"
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<Trash2 size={14} />}
|
icon={Trash2}
|
||||||
label={`remove bootstrap ${b.name}`}
|
label={`remove bootstrap ${b.name}`}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|||||||
Reference in new issue
Block a user