netx/web/src/pages/network/PortTrafficBoardPanelCell.tsx
oliver c4526e36d8 Polish HeroUI chrome, topology toolbar, and session client IP.
Checkpoint before workbench facade redesign: list defaults, API key quota, toast portal, topology canvas editor toolbar with More menu, and trusted-proxy client IP for sessions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 15:10:57 +08:00

139 lines
4.4 KiB
TypeScript

import { Button } from "@heroui/react";
import { useQuery, keepPreviousData } from "@tanstack/react-query";
import { fetchPortTrafficCompare } from "../../services/api";
import { queryKeys } from "../../constants/queryKeys";
import { useI18n } from "../../i18n";
import type { PortTrafficBoardPanel } from "../../types";
import { PortTrafficWall, type WallYMode } from "./PortTrafficWall";
const POLL_MS = 5000;
const EMPTY: never[] = [];
type Props = {
boardId: string;
panel: PortTrafficBoardPanel;
editing?: boolean;
dense?: boolean;
onEdit?: () => void;
onRemove?: () => void;
onMove?: (dir: -1 | 1) => void;
};
export function PortTrafficBoardPanelCell({
boardId,
panel,
editing,
dense,
onEdit,
onRemove,
onMove,
}: Props) {
const { t } = useI18n();
const baseline = panel.baseline || "off";
const aheadHours =
baseline === "off" && !panel.baseline_target_id
? 0
: Math.max(0, Number(panel.ahead_hours ?? 1) || 0);
const compareQuery = useQuery({
queryKey: [
...queryKeys.portTrafficCompare(
panel.target_id,
panel.range_hours,
baseline,
baseline === "custom" ? panel.offset_hours : 0,
panel.baseline_target_id || "",
aheadHours,
),
boardId,
panel.id,
],
queryFn: () =>
fetchPortTrafficCompare({
targetId: panel.target_id,
rangeHours: panel.range_hours,
baseline,
offsetHours: baseline === "custom" ? panel.offset_hours : undefined,
aheadHours: aheadHours || undefined,
baselineTargetId: panel.baseline_target_id || undefined,
}),
enabled: Boolean(panel.target_id) && !panel.stale,
staleTime: 1000,
placeholderData: keepPreviousData,
refetchInterval: POLL_MS,
});
const points = compareQuery.data?.current ?? EMPTY;
const baselinePoints =
baseline === "off" && !panel.baseline_target_id
? EMPTY
: (compareQuery.data?.baseline ?? EMPTY);
const target = panel.target || compareQuery.data?.meta?.current_target || null;
const baselineTarget =
compareQuery.data?.meta?.baseline_target || panel.baseline_target || null;
const title =
panel.title ||
(target ? `${target.ne_name || target.ne_ip || "—"} · ${target.ifname}` : t("portTraffic.pickPort"));
const rangeLabel = `${panel.range_hours}h${
baseline === "off"
? ""
: baseline === "day"
? ` · ${t("portTraffic.compareDay")}`
: baseline === "week"
? ` · ${t("portTraffic.compareWeek")}`
: baseline === "shift"
? ` · ${t("portTraffic.compareShift")}`
: ` · ${t("portTraffic.compareCustom")}`
}${panel.baseline_target_id ? ` · ${t("portTraffic.mapBaselinePort")}` : ""}`;
return (
<div
className={`pt-board-panel${panel.stale ? " is-stale" : ""}`}
style={{ gridColumn: `span ${Math.max(1, panel.col_span || 1)}` }}
>
{editing ? (
<div className="pt-board-panel__editbar">
<span className="pt-board-panel__edit-title" title={title}>
{title}
</span>
<div className="btn-row">
<Button size="sm" variant="secondary" isIconOnly onPress={() => onMove?.(-1)}>
↑
</Button>
<Button size="sm" variant="secondary" isIconOnly onPress={() => onMove?.(1)}>
↓
</Button>
<Button size="sm" variant="secondary" onPress={() => onEdit?.()}>
{t("portTraffic.edit")}
</Button>
<Button size="sm" variant="danger" onPress={() => onRemove?.()}>
{t("portTraffic.delete")}
</Button>
</div>
</div>
) : null}
{panel.stale ? (
<div className="pt-board-panel__stale">
<p>{t("portTraffic.boardPanelStale")}</p>
{editing ? (
<Button variant="primary" onPress={() => onEdit?.()}>
{t("portTraffic.boardRetarget")}
</Button>
) : null}
</div>
) : (
<PortTrafficWall
target={target}
baselineTarget={baselineTarget}
points={points}
baselinePoints={baselinePoints}
yMode={(panel.y_mode as WallYMode) || "auto"}
rangeLabel={rangeLabel}
loading={compareQuery.isLoading || compareQuery.isFetching}
hint={t("portTraffic.waitSamples")}
dense={dense}
/>
)}
</div>
);
}