177 lines
7.2 KiB
TypeScript
177 lines
7.2 KiB
TypeScript
import { useMemo } from "react";
|
||
|
||
import { useAdminOverview } from "~/shared/services/admin";
|
||
import { Section, Badge, InfoRow } from "../components";
|
||
import { useAdminContext } from "../context";
|
||
import { formatBytes, formatDate, formatUnknown, numberFormatter } from "../utils/format";
|
||
|
||
export const AdminOverviewPage = () => {
|
||
const { isAuthorized, handleRequestError } = useAdminContext();
|
||
|
||
const overviewQuery = useAdminOverview({
|
||
enabled: isAuthorized,
|
||
retry: false,
|
||
refetchInterval: 60_000,
|
||
onError: (error) => {
|
||
handleRequestError(error, "Не удалось загрузить обзор");
|
||
},
|
||
});
|
||
|
||
const overviewCards = useMemo(() => {
|
||
const data = overviewQuery.data;
|
||
if (!data) {
|
||
return [];
|
||
}
|
||
const { project, content, node, ipfs, codebase, runtime } = data;
|
||
return [
|
||
{
|
||
label: "Хост",
|
||
value: project.host || "локальный",
|
||
helper: project.name,
|
||
},
|
||
{
|
||
label: "TON Master",
|
||
value: node.ton_master,
|
||
helper: "Платформа",
|
||
},
|
||
{
|
||
label: "Service Wallet",
|
||
value: node.service_wallet,
|
||
helper: node.id,
|
||
},
|
||
{
|
||
label: "Контент",
|
||
value: `${numberFormatter.format(content.encrypted_total)} зашифр.`,
|
||
helper: `${numberFormatter.format(content.derivatives_ready)} деривативов`,
|
||
},
|
||
{
|
||
label: "IPFS Repo",
|
||
value: formatBytes(Number((ipfs.repo as Record<string, unknown>)?.RepoSize ?? 0)),
|
||
helper: "Размер репозитория",
|
||
},
|
||
{
|
||
label: "Bitswap",
|
||
value: numberFormatter.format(Number((ipfs.bitswap as Record<string, unknown>)?.Peers ?? 0)),
|
||
helper: "Пиры",
|
||
},
|
||
{
|
||
label: "Билд",
|
||
value: codebase.commit ?? "n/a",
|
||
helper: codebase.branch ?? "",
|
||
},
|
||
{
|
||
label: "Python",
|
||
value: runtime.python,
|
||
helper: runtime.implementation,
|
||
},
|
||
];
|
||
}, [overviewQuery.data]);
|
||
|
||
if (!overviewQuery.data) {
|
||
return overviewQuery.isLoading ? (
|
||
<Section id="overview" title="Обзор" description="Краткий срез состояния узла, окружения и служб">
|
||
Загрузка…
|
||
</Section>
|
||
) : null;
|
||
}
|
||
|
||
const { project, services, ton, runtime, ipfs } = overviewQuery.data;
|
||
|
||
return (
|
||
<Section
|
||
id="overview"
|
||
title="Обзор"
|
||
description="Краткий срез состояния узла, окружения и служб"
|
||
actions={
|
||
<button
|
||
type="button"
|
||
className="rounded-lg bg-slate-800 px-3 py-2 text-xs font-semibold text-slate-200 ring-1 ring-slate-700 transition hover:bg-slate-700"
|
||
onClick={() => overviewQuery.refetch()}
|
||
disabled={overviewQuery.isFetching}
|
||
>
|
||
{overviewQuery.isFetching ? "Обновляем…" : "Обновить"}
|
||
</button>
|
||
}
|
||
>
|
||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||
{overviewCards.map((card) => (
|
||
<div key={card.label} className="rounded-xl bg-slate-950/60 p-4 shadow-inner shadow-black/40 ring-1 ring-slate-800">
|
||
<p className="text-xs uppercase tracking-wide text-slate-500">{card.label}</p>
|
||
<p className="mt-2 break-all text-lg font-semibold text-slate-100">{card.value}</p>
|
||
{card.helper ? <p className="mt-1 text-xs text-slate-500">{card.helper}</p> : null}
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="grid gap-6 lg:grid-cols-2">
|
||
<div className="space-y-3">
|
||
<h3 className="text-lg font-semibold text-slate-100">Проект</h3>
|
||
<dl className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||
<InfoRow label="Хост">{project.host || "—"}</InfoRow>
|
||
<InfoRow label="Имя">{project.name}</InfoRow>
|
||
<InfoRow label="Приватность">{project.privacy}</InfoRow>
|
||
<InfoRow label="TON Testnet">{ton.testnet ? "Да" : "Нет"}</InfoRow>
|
||
<InfoRow label="TON API Key">{ton.api_key_configured ? "Настроен" : "Нет"}</InfoRow>
|
||
<InfoRow label="TON Host">{ton.host || "—"}</InfoRow>
|
||
</dl>
|
||
</div>
|
||
<div className="space-y-3">
|
||
<h3 className="text-lg font-semibold text-slate-100">Среда выполнения</h3>
|
||
<dl className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||
<InfoRow label="Python">{runtime.python}</InfoRow>
|
||
<InfoRow label="Имплементация">{runtime.implementation}</InfoRow>
|
||
<InfoRow label="Платформа">{runtime.platform}</InfoRow>
|
||
<InfoRow label="UTC сейчас">{formatDate(runtime.utc_now)}</InfoRow>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid gap-6 lg:grid-cols-2">
|
||
<div className="space-y-3">
|
||
<h3 className="text-lg font-semibold text-slate-100">IPFS</h3>
|
||
<div className="grid gap-3 rounded-xl bg-slate-950/60 p-4 ring-1 ring-slate-800">
|
||
<InfoRow label="ID">{formatUnknown((ipfs.identity as Record<string, unknown>)?.ID)}</InfoRow>
|
||
<InfoRow label="Agent">{formatUnknown((ipfs.identity as Record<string, unknown>)?.AgentVersion)}</InfoRow>
|
||
<InfoRow label="Peers">{numberFormatter.format(Number((ipfs.bitswap as Record<string, unknown>)?.Peers ?? 0))}</InfoRow>
|
||
<InfoRow label="Repo size">{formatBytes(Number((ipfs.repo as Record<string, unknown>)?.RepoSize ?? 0))}</InfoRow>
|
||
<InfoRow label="Storage max">{formatBytes(Number((ipfs.repo as Record<string, unknown>)?.StorageMax ?? 0))}</InfoRow>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-3">
|
||
<h3 className="text-lg font-semibold text-slate-100">Сервисы</h3>
|
||
<ul className="space-y-2">
|
||
{services.length === 0 ? (
|
||
<li className="text-sm text-slate-400">Нет зарегистрированных сервисов</li>
|
||
) : (
|
||
services.map((service) => {
|
||
const status = service.status ?? "—";
|
||
const tone: "success" | "warn" | "danger" | "neutral" = status.includes("working")
|
||
? "success"
|
||
: status.includes("timeout")
|
||
? "danger"
|
||
: "neutral";
|
||
return (
|
||
<li
|
||
key={service.name}
|
||
className="flex items-center justify-between rounded-lg bg-slate-950/40 px-3 py-2 ring-1 ring-slate-800"
|
||
>
|
||
<span className="text-sm font-medium text-slate-100">{service.name}</span>
|
||
<div className="flex items-center gap-3 text-xs text-slate-400">
|
||
<span>
|
||
{service.last_reported_seconds != null
|
||
? `Обновлён ${Math.round(service.last_reported_seconds)} сек назад`
|
||
: "Нет данных"}
|
||
</span>
|
||
<Badge tone={tone}>{status}</Badge>
|
||
</div>
|
||
</li>
|
||
);
|
||
})
|
||
)}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</Section>
|
||
);
|
||
};
|