Files
ServiceMap-Public/frontend/src/topology/ServiceNode.tsx
T

118 lines
3.7 KiB
TypeScript

import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
import type {
ComponentRole,
Criticality,
HealthStatus,
ServiceKind,
} from "../api";
export type ServiceNodeData = {
name: string;
status: HealthStatus;
kind: ServiceKind;
latency: number | null;
dependencyCount: number;
rootCause: boolean;
impacted: boolean;
group: string | null;
criticality: Criticality;
flapping: boolean;
category: string;
application?: string;
applicationNode?: boolean;
memberIds?: string[];
nodeKind?: "domain" | "application" | "component";
role?: ComponentRole;
roles?: ComponentRole[];
explanation?: string;
monitoredComponents?: number;
unmonitoredComponents?: number;
domainId?: string;
};
export type ServiceNode = Node<ServiceNodeData, "service">;
const statusLabel = (status: HealthStatus) =>
status === "unmonitored"
? "Not monitored"
: status[0].toUpperCase() + status.slice(1);
const statusIcon = (status: HealthStatus) =>
({
healthy: "✓",
degraded: "△",
down: "!",
recovering: "↻",
maintenance: "Ⅱ",
unknown: "?",
unmonitored: "○",
})[status];
export function ServiceNodeView({ data }: NodeProps<ServiceNode>) {
const targetLabel =
data.nodeKind === "domain"
? `${data.memberIds?.length ?? 0} components`
: data.applicationNode
? `${data.monitoredComponents ?? 0} resolved · ${data.unmonitoredComponents ?? 0} unresolved`
: data.latency != null
? `${data.latency} ms · ${data.kind.toUpperCase()}`
: data.kind.toUpperCase();
return (
<>
<Handle type="target" position={Position.Left} />
<div
className={`service-node-body status-${data.status}`}
aria-label={`${data.name}, ${statusLabel(data.status)}${data.rootCause ? ", likely root cause" : ""}`}
>
<div className="node-badges">
{data.rootCause && (
<span className="node-badge root-cause-badge">ROOT CAUSE</span>
)}
{!data.rootCause && data.impacted && (
<span className="node-badge impacted-badge">IMPACTED</span>
)}
{data.flapping && (
<span className="node-badge flap-badge">FLAPPING</span>
)}
</div>
{data.nodeKind === "domain" && (
<span className="node-kicker">
DOMAIN · {data.memberIds?.length ?? 0} components
</span>
)}
{data.applicationNode && (
<span className="node-kicker">
APPLICATION · {data.memberIds?.length ?? 0} components
</span>
)}
{data.applicationNode && data.roles?.length ? (
<span
className="app-micro-topology"
aria-label={`Component roles: ${data.roles.join(", ")}`}
>
{data.roles.slice(0, 5).map((role) => (
<i key={role} className={`role-${role}`}>
{role.slice(0, 1).toUpperCase()}
</i>
))}
</span>
) : null}
{data.nodeKind === "component" && (
<span className="node-kicker">{data.role ?? data.category}</span>
)}
<strong>{data.name}</strong>
<span className={`node-status status-${data.status}`}>
<i aria-hidden="true">{statusIcon(data.status)}</i>{" "}
{statusLabel(data.status)}
</span>
<small>{targetLabel} · {data.criticality}</small>
<small>
{data.explanation ??
`${data.category.replace("_", " ")} · ${data.dependencyCount} ${data.dependencyCount === 1 ? "dependency" : "dependencies"}`}
</small>
</div>
<Handle type="source" position={Position.Right} />
</>
);
}