web2-client/src/pages/admin/sections/Overview.tsx

177 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
};