Files
MikrotikManager/components/mcp/plan-review.tsx
T

19 lines
3.8 KiB
TypeScript

import { Badge } from "@/components/reui/badge"
const object = (value: unknown): Record<string, unknown> => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {}
const list = (value: unknown): unknown[] => Array.isArray(value) ? value : []
const json = (value: unknown) => JSON.stringify(value, null, 2)
export function PlanReview({ plan }: { plan: Record<string, unknown> }) {
const changes = object(plan.changes), target = object(changes.target)
const objects = list(changes.objects), steps = list(changes.steps), recovery = object(changes.recovery)
return <div className="flex min-w-0 flex-col gap-4">
<div className="flex flex-wrap gap-2"><Badge variant="info-light">{String(plan.operationId ?? "Операция")}</Badge><Badge variant="warning-light">{String(plan.state ?? "Неизвестно")}</Badge><Badge variant="invert-light">Шагов: {steps.length}</Badge></div>
<dl className="grid gap-2 text-sm sm:grid-cols-2"><div><dt className="text-muted-foreground">Область</dt><dd>{target.scope === "servers" ? "Серверы: " + list(target.serverIds).join(", ") : target.scope === "global" ? "Всё приложение" : "Подключение"}</dd></div><div><dt className="text-muted-foreground">Срок плана</dt><dd>{plan.expiresAt ? new Date(String(plan.expiresAt)).toLocaleString("ru-RU") : "Не указан"}</dd></div></dl>
{!!list(changes.warnings).length && <div className="rounded-md border border-warning/30 p-3 text-sm"><p className="mb-2 font-medium">Условия и последствия</p><ul className="list-disc space-y-1 pl-5">{list(changes.warnings).map((warning, index) => <li key={index}>{String(warning)}</li>)}</ul></div>}
{!!objects.length && <div><h3 className="mb-2 text-sm font-medium">Объекты и защищённые зависимости</h3><div className="flex flex-wrap gap-2">{objects.map((value, index) => { const item = object(value); return <Badge key={index} variant="invert-light">{item.serverId ? `Сервер ${item.serverId} · ` : ""}{String(item.kind)} · {String(item.id)}</Badge> })}</div></div>}
<div className="grid min-w-0 gap-3 md:grid-cols-2"><div className="min-w-0"><h3 className="mb-2 text-sm font-medium">Исходное состояние</h3><pre className="max-h-72 overflow-auto whitespace-pre-wrap break-all rounded-md bg-muted p-3 text-xs">{json(changes.before ?? changes.snapshots ?? null)}</pre></div><div className="min-w-0"><h3 className="mb-2 text-sm font-medium">Запланированные шаги</h3><ol className="flex flex-col gap-2">{steps.map((value, index) => { const step = object(value); return <li key={index} className="min-w-0 rounded-md border p-3"><p className="mb-2 text-sm font-medium">{index + 1}. {step.kind === "ros" ? String(object(step.step).action) + " · " + String(object(step.step).resource) : String(step.service)}</p><pre className="max-h-52 overflow-auto whitespace-pre-wrap break-all text-xs">{json(step.kind === "ros" ? step.step : step.input)}</pre></li> })}</ol></div></div>
{!!Object.keys(recovery).length && <p className="rounded-md border p-3 text-sm">Зарезервировано восстановление до {new Date(String(recovery.validUntil)).toLocaleString("ru-RU")}. Оно выполняется только при подтверждённой исходной записи, неуспешной проверке и действующем допуске; неопределённую запись исполнитель не повторяет.</p>}
<details><summary className="cursor-pointer text-sm text-muted-foreground">Полные данные плана</summary><pre className="mt-2 max-h-72 overflow-auto whitespace-pre-wrap break-all text-xs">{json(plan)}</pre></details>
</div>
}