Обновлены переводы на русский язык для компонентов, включая карточки мониторинга, сетевые панели и настройки. Исправлены описания и метки для улучшения пользовательского интерфейса, а также добавлены новые элементы для поддержки локализации в компонентах, таких как DataGrid и ResourcePage.
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: 'Текст' },
|
||
{ 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>
|
||
)
|
||
}
|