CI / changes (push) Successful in 17s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 26s
CI / web (push) Successful in 46s
CI / go (push) Successful in 1m1s
CI / bird2 (push) Successful in 17s
CI / release (push) Failing after 2m22s
Web UI полностью переведён с SvelteKit на новый стек: React 19, TanStack Router/Query/Table/Virtual, shadcn/ui (base-nova) и ReUI enterprise-компоненты (data-grid, filters, autocomplete). Новый код разложен по слоям: packages/ui (shadcn-примитивы), apps/web (роуты, shared-обёртки, ReUI-адаптации). BREAKING CHANGE: меняется структура и инструментинг фронтенда. - apps/web/ — новый Vite + React-проект (@evobgp/web), file-based роуты TanStack Router; экраны dashboard, modules, monitoring, network, operations, schedule, settings, tenant-settings, access, directories. - packages/ui/ — shadcn/ui-примитивы (@evobgp/ui) с общими стилями globals.css и cn-утилитой; CLI shadcn запускается из apps/web. - apps/web/src/components/reui/ — enterprise-паттерны ReUI. - pnpm workspace (pnpm-workspace.yaml, pnpm-lock.yaml, tsconfig.base.json) заменяет npm-проект в web/. - web/ переименован в web-legacy-svelte/ (архив-референс для миграции); импорты оттуда запрещены правилом WEB-22. - CI (.gitea/workflows/ci.yaml): job web переведён на Node 22 + pnpm 10 (typecheck/lint/build через pnpm --filter @evobgp/web); пути триггеров обновлены под apps/web|packages/ui. - deploy/docker/evobgp-web/Dockerfile: сборка из корня репозитория, pnpm install --frozen-lockfile, выход dist из apps/web/dist. - .cursor/rules/web-shadcn.mdc, context7-stack.mdc, engineering.mdc, AGENTS.md — обновлены под React-стек (WEB-01..WEB-22, DOC-SYNC-06/07). Проверки WEB-19 локально: typecheck, lint, build — exit 0. Co-authored-by: Cursor <[email protected]>
138 lines
3.9 KiB
Svelte
138 lines
3.9 KiB
Svelte
<script lang="ts">
|
||
import { apiMutate } from '$lib/api/client.js';
|
||
import type { AsEntry, AsEntryCreate, AsEntryPatch, BgpCommunity } from '$lib/api/types.js';
|
||
import {
|
||
communityLabel,
|
||
communityOptionLabel,
|
||
fromNullableSelect,
|
||
NONE_OPTION,
|
||
nullableSelectValue
|
||
} from '$lib/components/modules/module-helpers.js';
|
||
import { Button } from '$lib/ui/core/button/index.js';
|
||
import { Input } from '$lib/ui/core/input/index.js';
|
||
import { Label } from '$lib/ui/core/label/index.js';
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogFooter,
|
||
DialogDescription
|
||
} from '$lib/ui/core/dialog/index.js';
|
||
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js';
|
||
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
|
||
|
||
type Props = {
|
||
open: boolean;
|
||
moduleId: string;
|
||
edit: AsEntry | null;
|
||
communities: BgpCommunity[];
|
||
onSaved: () => void | Promise<void>;
|
||
onClose: () => void;
|
||
};
|
||
|
||
let { open = $bindable(), moduleId, edit, communities, onSaved, onClose }: Props = $props();
|
||
|
||
let saving = $state(false);
|
||
let form = $state<AsEntryCreate>({ asn: 0, community_id: null });
|
||
let initKey = $state('');
|
||
|
||
function resetForm() {
|
||
form = edit
|
||
? { asn: edit.asn, community_id: edit.community_id }
|
||
: { asn: 0, community_id: null };
|
||
}
|
||
|
||
$effect(() => {
|
||
if (!open) {
|
||
initKey = '';
|
||
return;
|
||
}
|
||
const nextKey = edit?.id ?? 'new';
|
||
if (nextKey !== initKey) {
|
||
initKey = nextKey;
|
||
resetForm();
|
||
}
|
||
});
|
||
|
||
async function save() {
|
||
const asn = Number(form.asn);
|
||
if (!Number.isFinite(asn) || asn < 1 || asn > 4294967295) {
|
||
notify.error('Укажите корректный ASN (1–4294967295)');
|
||
return;
|
||
}
|
||
saving = true;
|
||
try {
|
||
const body: AsEntryCreate | AsEntryPatch = { asn, community_id: form.community_id };
|
||
if (edit) {
|
||
await apiMutate(`/v1/modules/${moduleId}/as-entries/${edit.id}`, 'PATCH', body);
|
||
notify.success('Запись обновлена');
|
||
} else {
|
||
await apiMutate(`/v1/modules/${moduleId}/as-entries`, 'POST', body as AsEntryCreate);
|
||
notify.success('Запись добавлена');
|
||
}
|
||
open = false;
|
||
await onSaved();
|
||
} catch (e) {
|
||
notifyApiError(e);
|
||
} finally {
|
||
saving = false;
|
||
}
|
||
}
|
||
|
||
function handleOpenChange(next: boolean) {
|
||
open = next;
|
||
if (!next) onClose();
|
||
}
|
||
</script>
|
||
|
||
<Dialog bind:open onOpenChange={handleOpenChange}>
|
||
<DialogContent class="sm:max-w-sm">
|
||
<DialogHeader>
|
||
<DialogTitle>{edit ? 'Редактировать запись' : 'Новая AS-запись'}</DialogTitle>
|
||
<DialogDescription>
|
||
Номер автономной системы и community для политики анонса.
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div class="space-y-4 py-2">
|
||
<div class="space-y-1.5">
|
||
<Label for="as-asn">ASN</Label>
|
||
<Input
|
||
id="as-asn"
|
||
type="number"
|
||
placeholder="12345"
|
||
bind:value={form.asn}
|
||
min={1}
|
||
max={4294967295}
|
||
/>
|
||
</div>
|
||
<div class="space-y-1.5">
|
||
<Label for="as-comm">Community</Label>
|
||
<Select
|
||
type="single"
|
||
value={nullableSelectValue(form.community_id)}
|
||
onValueChange={(v) => {
|
||
form.community_id = fromNullableSelect(v);
|
||
}}
|
||
>
|
||
<SelectTrigger id="as-comm" class="w-full">
|
||
{form.community_id ? communityLabel(form.community_id, communities) : 'Не выбрано'}
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value={NONE_OPTION}>Не выбрано</SelectItem>
|
||
{#each communities as c (c.id)}
|
||
<SelectItem value={c.id}>{communityOptionLabel(c)}</SelectItem>
|
||
{/each}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button variant="outline" onclick={() => handleOpenChange(false)}>Отмена</Button>
|
||
<Button onclick={save} disabled={saving}>
|
||
{saving ? 'Сохранение…' : edit ? 'Сохранить' : 'Добавить'}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|