118 lines
3.7 KiB
TypeScript
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} />
|
|
</>
|
|
);
|
|
}
|