CI / changes (push) Successful in 10s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 50s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 3m58s
Updated multiple components to utilize the new FormDrawer for modal dialogs, enhancing the user interface and streamlining the layout. This change includes the ConfirmDialog, ApiKeyCreateDialog, FirewallRuleCreateDialog, and others, ensuring a more cohesive and modern design across the application. Additionally, refactored the ConfirmDialog to improve confirmation handling and user feedback during actions.
254 lines
7.9 KiB
TypeScript
254 lines
7.9 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
||
import { toast } from 'sonner'
|
||
|
||
import { Button } from '@evobgp/ui/components/button'
|
||
import { Input } from '@evobgp/ui/components/input'
|
||
import { Label } from '@evobgp/ui/components/label'
|
||
|
||
import { FormDrawer } from '@/components/form-drawer'
|
||
import { LoadingButton } from '@/components/loading-button'
|
||
import { CommunitySelect } from '@/components/modules/community-select'
|
||
import { SelectField } from '@/components/select-field'
|
||
import { ApiError, apiMutate } from '@/lib/api-client'
|
||
import { normalizeCdnSourceKind } from '@/lib/modules/helpers'
|
||
import type { BgpCommunity, CdnPreviewResponse, CdnSource, CdnSourceCreate } from '@/types/api'
|
||
|
||
const CDN_KIND_ITEMS = [
|
||
{ value: 'plaintext', label: 'plaintext' },
|
||
{ value: 'json', label: 'json' },
|
||
] as const
|
||
|
||
interface ModuleCdnSourceDialogProps {
|
||
open: boolean
|
||
moduleId: string
|
||
edit: CdnSource | null
|
||
communities: BgpCommunity[]
|
||
onOpenChange: (open: boolean) => void
|
||
onSaved: () => void | Promise<void>
|
||
}
|
||
|
||
type CdnForm = CdnSourceCreate & { refresh_interval_sec?: number | null }
|
||
|
||
export function ModuleCdnSourceDialog({
|
||
open,
|
||
moduleId,
|
||
edit,
|
||
communities,
|
||
onOpenChange,
|
||
onSaved,
|
||
}: ModuleCdnSourceDialogProps) {
|
||
const [saving, setSaving] = useState(false)
|
||
const [previewLoading, setPreviewLoading] = useState(false)
|
||
const [previewItems, setPreviewItems] = useState<string[]>([])
|
||
const [previewTotal, setPreviewTotal] = useState(0)
|
||
const [previewTruncated, setPreviewTruncated] = useState(false)
|
||
const [previewError, setPreviewError] = useState<string | null>(null)
|
||
const [previewOk, setPreviewOk] = useState(false)
|
||
const [form, setForm] = useState<CdnForm>({
|
||
url: '',
|
||
source_kind: 'plaintext',
|
||
prefix_path: '',
|
||
community_id: null,
|
||
})
|
||
|
||
const kindItems = useMemo(() => [...CDN_KIND_ITEMS], [])
|
||
|
||
function clearPreview() {
|
||
setPreviewLoading(false)
|
||
setPreviewItems([])
|
||
setPreviewTotal(0)
|
||
setPreviewTruncated(false)
|
||
setPreviewError(null)
|
||
setPreviewOk(false)
|
||
}
|
||
|
||
useEffect(() => {
|
||
if (!open) {
|
||
clearPreview()
|
||
return
|
||
}
|
||
setForm(
|
||
edit
|
||
? {
|
||
url: edit.url,
|
||
source_kind: normalizeCdnSourceKind(edit.source_kind),
|
||
prefix_path: edit.prefix_path ?? '',
|
||
community_id: edit.community_id,
|
||
refresh_interval_sec: edit.refresh_interval_sec,
|
||
}
|
||
: { url: '', source_kind: 'plaintext', prefix_path: '', community_id: null },
|
||
)
|
||
clearPreview()
|
||
}, [open, edit])
|
||
|
||
async function previewCdn() {
|
||
const urlTrim = form.url.trim()
|
||
if (!urlTrim) {
|
||
toast.error('Укажите URL')
|
||
return
|
||
}
|
||
setPreviewLoading(true)
|
||
setPreviewError(null)
|
||
setPreviewOk(false)
|
||
try {
|
||
const res = await apiMutate<CdnPreviewResponse>(
|
||
`/v1/modules/${moduleId}/cdn-sources/preview`,
|
||
'POST',
|
||
{
|
||
url: urlTrim,
|
||
source_kind: form.source_kind,
|
||
prefix_path: form.prefix_path?.trim() ?? '',
|
||
},
|
||
)
|
||
setPreviewItems(res.items)
|
||
setPreviewTotal(res.total)
|
||
setPreviewTruncated(res.truncated)
|
||
setPreviewOk(true)
|
||
} catch (e) {
|
||
setPreviewError(e instanceof ApiError ? e.message : String(e))
|
||
setPreviewItems([])
|
||
setPreviewTotal(0)
|
||
setPreviewTruncated(false)
|
||
setPreviewOk(false)
|
||
} finally {
|
||
setPreviewLoading(false)
|
||
}
|
||
}
|
||
|
||
async function save() {
|
||
const urlTrim = form.url.trim()
|
||
if (!urlTrim) {
|
||
toast.error('Укажите URL')
|
||
return
|
||
}
|
||
setSaving(true)
|
||
try {
|
||
const body = {
|
||
...form,
|
||
url: urlTrim,
|
||
source_kind: form.source_kind,
|
||
prefix_path: form.prefix_path?.trim() ?? '',
|
||
}
|
||
if (edit) {
|
||
await apiMutate(`/v1/modules/${moduleId}/cdn-sources/${edit.id}`, 'PATCH', body)
|
||
toast.success('Источник обновлён')
|
||
} else {
|
||
await apiMutate(`/v1/modules/${moduleId}/cdn-sources`, 'POST', body)
|
||
toast.success('Источник добавлен')
|
||
}
|
||
clearPreview()
|
||
onOpenChange(false)
|
||
await onSaved()
|
||
} catch (e) {
|
||
toast.error(e instanceof ApiError ? e.message : String(e))
|
||
} finally {
|
||
setSaving(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<FormDrawer
|
||
open={open}
|
||
onOpenChange={onOpenChange}
|
||
title={edit ? 'Редактировать источник' : 'Новый CDN-источник'}
|
||
className="sm:max-w-lg"
|
||
footer={
|
||
<>
|
||
<LoadingButton variant="outline" onClick={() => onOpenChange(false)}>
|
||
Отмена
|
||
</LoadingButton>
|
||
<LoadingButton loading={saving} onClick={() => void save()}>
|
||
{edit ? 'Сохранить' : 'Добавить'}
|
||
</LoadingButton>
|
||
</>
|
||
}
|
||
>
|
||
<div className="flex flex-col gap-1.5">
|
||
<Label htmlFor="cdn-url">URL</Label>
|
||
<Input
|
||
id="cdn-url"
|
||
placeholder="https://example.com/list.txt"
|
||
value={form.url}
|
||
onChange={(e) => setForm((s) => ({ ...s, url: e.target.value }))}
|
||
/>
|
||
</div>
|
||
<SelectField
|
||
id="cdn-kind"
|
||
label="Тип источника"
|
||
items={kindItems}
|
||
value={form.source_kind}
|
||
onValueChange={(v) => v && setForm((s) => ({ ...s, source_kind: v }))}
|
||
/>
|
||
<div className="flex flex-col gap-1.5">
|
||
<Label htmlFor="cdn-prefix-path">JSON path (prefix_path)</Label>
|
||
<Input
|
||
id="cdn-prefix-path"
|
||
placeholder="напр. prefixes[] или data.items[].cidr"
|
||
value={form.prefix_path ?? ''}
|
||
onChange={(e) => setForm((s) => ({ ...s, prefix_path: e.target.value }))}
|
||
/>
|
||
{form.source_kind === 'json' && !form.prefix_path?.trim() ? (
|
||
<p className="text-xs text-muted-foreground">
|
||
Для JSON укажите путь к полям с CIDR; пустой путь может не дать префиксов.
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
<CommunitySelect
|
||
id="cdn-comm"
|
||
label="Community"
|
||
value={form.community_id ?? null}
|
||
onValueChange={(v) => setForm((s) => ({ ...s, community_id: v }))}
|
||
communities={communities}
|
||
nullable
|
||
/>
|
||
<div className="flex flex-col gap-1.5">
|
||
<Label htmlFor="cdn-interval">Интервал обновления (сек)</Label>
|
||
<Input
|
||
id="cdn-interval"
|
||
type="number"
|
||
placeholder="3600"
|
||
value={form.refresh_interval_sec ?? ''}
|
||
onChange={(e) =>
|
||
setForm((s) => ({
|
||
...s,
|
||
refresh_interval_sec: e.target.value ? Number(e.target.value) : null,
|
||
}))
|
||
}
|
||
/>
|
||
</div>
|
||
<div className="flex flex-col gap-2 rounded-lg border border-border p-3">
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
<Button
|
||
type="button"
|
||
variant="secondary"
|
||
size="sm"
|
||
onClick={() => void previewCdn()}
|
||
disabled={previewLoading}
|
||
>
|
||
{previewLoading ? 'Загрузка…' : 'Предпросмотр'}
|
||
</Button>
|
||
{previewError ? (
|
||
<span className="text-sm text-destructive">{previewError}</span>
|
||
) : previewOk ? (
|
||
<span className="text-sm text-muted-foreground">
|
||
Всего: {previewTotal}
|
||
{previewTruncated ? (
|
||
<span className="text-amber-600 dark:text-amber-500"> (обрезано)</span>
|
||
) : null}
|
||
</span>
|
||
) : null}
|
||
</div>
|
||
{previewItems.length > 0 ? (
|
||
<ul className="max-h-48 overflow-y-auto rounded-md border bg-muted/40 p-2 font-mono text-xs">
|
||
{previewItems.map((item, i) => (
|
||
<li key={`${i}-${item}`} className="py-0.5">
|
||
{item}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : null}
|
||
</div>
|
||
</FormDrawer>
|
||
)
|
||
}
|