mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-03-30 23:29:45 +00:00
✨ feat: topology, and improves handling
This commit is contained in:
163
apps/web/hooks/use-graph-layout.ts
Normal file
163
apps/web/hooks/use-graph-layout.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
"use client";
|
||||
|
||||
import { Position } from "@xyflow/react";
|
||||
import { useMemo } from "react";
|
||||
import type {
|
||||
ProxyMetadata,
|
||||
ServerMetadata,
|
||||
TopologyEdge,
|
||||
TopologyNode,
|
||||
} from "@/lib/topology-types";
|
||||
|
||||
interface LayoutInput {
|
||||
nodes: TopologyNode[];
|
||||
edges: TopologyEdge[];
|
||||
}
|
||||
|
||||
export function useGraphLayout({ nodes, edges }: LayoutInput) {
|
||||
const layoutedNodes = useMemo(() => {
|
||||
const k8sNodes = nodes.filter((n) => n.data.type === "k8s-node");
|
||||
const serverNodes = nodes.filter((n) => n.data.type === "server");
|
||||
const proxyNodes = nodes.filter((n) => n.data.type === "proxy");
|
||||
|
||||
const nodeSpacing = {
|
||||
x: 350,
|
||||
y: 350,
|
||||
};
|
||||
|
||||
const nodeWidth = 320;
|
||||
const layoutedNodesList: TopologyNode[] = [];
|
||||
|
||||
const k8sNodeGroups = new Map<string, TopologyNode[]>();
|
||||
const orphanedNodes: TopologyNode[] = [];
|
||||
|
||||
const appNodes = [...serverNodes, ...proxyNodes];
|
||||
|
||||
appNodes.forEach((node) => {
|
||||
const metadata = node.data.metadata as ServerMetadata | ProxyMetadata;
|
||||
const k8sNodeNames = metadata.k8sNodes || [];
|
||||
|
||||
if (k8sNodeNames.length === 0) {
|
||||
orphanedNodes.push(node);
|
||||
} else {
|
||||
const k8sNodeName = k8sNodeNames[0];
|
||||
const k8sNodeId = `k8s-node-${k8sNodeName}`;
|
||||
if (!k8sNodeGroups.has(k8sNodeId)) {
|
||||
k8sNodeGroups.set(k8sNodeId, []);
|
||||
}
|
||||
k8sNodeGroups.get(k8sNodeId)?.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
const k8sWithApps = k8sNodes.filter((k8s) => {
|
||||
const group = k8sNodeGroups.get(k8s.id) || [];
|
||||
return group.length > 0;
|
||||
});
|
||||
|
||||
const k8sWithoutApps = k8sNodes.filter((k8s) => {
|
||||
const group = k8sNodeGroups.get(k8s.id) || [];
|
||||
return group.length === 0;
|
||||
});
|
||||
|
||||
const k8sNodeWidths = new Map<string, number>();
|
||||
k8sWithApps.forEach((k8sNode) => {
|
||||
const group = k8sNodeGroups.get(k8sNode.id) || [];
|
||||
const width = Math.max(nodeWidth, group.length * nodeWidth);
|
||||
k8sNodeWidths.set(k8sNode.id, width);
|
||||
});
|
||||
|
||||
let currentX = 0;
|
||||
const k8sNodePositions = new Map<string, { x: number; width: number }>();
|
||||
|
||||
k8sWithApps.forEach((k8sNode) => {
|
||||
const width = k8sNodeWidths.get(k8sNode.id)!;
|
||||
const centerX = currentX + width / 2;
|
||||
|
||||
k8sNodePositions.set(k8sNode.id, { x: centerX, width });
|
||||
|
||||
layoutedNodesList.push({
|
||||
...k8sNode,
|
||||
position: {
|
||||
x: centerX,
|
||||
y: 0,
|
||||
},
|
||||
targetPosition: Position.Top,
|
||||
sourcePosition: Position.Bottom,
|
||||
});
|
||||
|
||||
currentX += width + nodeSpacing.x;
|
||||
});
|
||||
|
||||
k8sWithoutApps.forEach((k8sNode) => {
|
||||
const centerX = currentX + nodeWidth / 2;
|
||||
|
||||
k8sNodePositions.set(k8sNode.id, { x: centerX, width: nodeWidth });
|
||||
|
||||
layoutedNodesList.push({
|
||||
...k8sNode,
|
||||
position: {
|
||||
x: centerX,
|
||||
y: 0,
|
||||
},
|
||||
targetPosition: Position.Top,
|
||||
sourcePosition: Position.Bottom,
|
||||
});
|
||||
|
||||
currentX += nodeWidth + nodeSpacing.x;
|
||||
});
|
||||
|
||||
k8sNodeGroups.forEach((group, k8sNodeId) => {
|
||||
const k8sPos = k8sNodePositions.get(k8sNodeId);
|
||||
if (!k8sPos) return;
|
||||
|
||||
const groupWidth = group.length * nodeWidth;
|
||||
const startX = k8sPos.x - groupWidth / 2 + nodeWidth / 2;
|
||||
|
||||
group.forEach((node, index) => {
|
||||
const xPos = startX + index * nodeWidth;
|
||||
|
||||
layoutedNodesList.push({
|
||||
...node,
|
||||
position: {
|
||||
x: xPos,
|
||||
y: nodeSpacing.y,
|
||||
},
|
||||
targetPosition: Position.Top,
|
||||
sourcePosition: Position.Bottom,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
orphanedNodes.forEach((node, index) => {
|
||||
const xPos = currentX + index * nodeWidth;
|
||||
|
||||
layoutedNodesList.push({
|
||||
...node,
|
||||
position: {
|
||||
x: xPos,
|
||||
y: nodeSpacing.y,
|
||||
},
|
||||
targetPosition: Position.Top,
|
||||
sourcePosition: Position.Bottom,
|
||||
});
|
||||
});
|
||||
|
||||
if (layoutedNodesList.length > 0) {
|
||||
const minX = Math.min(...layoutedNodesList.map((n) => n.position.x));
|
||||
const maxX = Math.max(...layoutedNodesList.map((n) => n.position.x));
|
||||
const totalWidth = maxX - minX;
|
||||
const offsetX = -totalWidth / 2;
|
||||
|
||||
layoutedNodesList.forEach((node) => {
|
||||
node.position.x += offsetX;
|
||||
});
|
||||
}
|
||||
|
||||
return layoutedNodesList;
|
||||
}, [nodes]);
|
||||
|
||||
return {
|
||||
nodes: layoutedNodes,
|
||||
edges,
|
||||
};
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
} from "@minikura/api";
|
||||
import { LABEL_PREFIX } from "@minikura/api";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { api } from "@/lib/api-client";
|
||||
|
||||
export function useK8sResources() {
|
||||
const [statefulSets, setStatefulSets] = useState<StatefulSetInfo[]>([]);
|
||||
@@ -19,15 +19,15 @@ export function useK8sResources() {
|
||||
const [configMaps, setConfigMaps] = useState<K8sConfigMapSummary[]>([]);
|
||||
const [minecraftServers, setMinecraftServers] = useState<CustomResourceSummary[]>([]);
|
||||
const [reverseProxyServers, setReverseProxyServers] = useState<CustomResourceSummary[]>([]);
|
||||
const [status, setStatus] = useState<K8sStatus>({ initialized: false });
|
||||
const [status, _setStatus] = useState<K8sStatus>({ initialized: false });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const [
|
||||
statusRes,
|
||||
podsRes,
|
||||
_statusRes,
|
||||
_podsRes,
|
||||
deploymentsRes,
|
||||
statefulSetsRes,
|
||||
servicesRes,
|
||||
@@ -47,38 +47,26 @@ export function useK8sResources() {
|
||||
|
||||
if (statefulSetsRes.status === "fulfilled" && statefulSetsRes.value.data) {
|
||||
setStatefulSets(statefulSetsRes.value.data as StatefulSetInfo[]);
|
||||
} else if (statefulSetsRes.status === "rejected") {
|
||||
console.error("Failed to fetch statefulsets:", statefulSetsRes.reason);
|
||||
}
|
||||
|
||||
if (deploymentsRes.status === "fulfilled" && deploymentsRes.value.data) {
|
||||
setDeployments(deploymentsRes.value.data as DeploymentInfo[]);
|
||||
} else if (deploymentsRes.status === "rejected") {
|
||||
console.error("Failed to fetch deployments:", deploymentsRes.reason);
|
||||
}
|
||||
|
||||
if (servicesRes.status === "fulfilled" && servicesRes.value.data) {
|
||||
setServices(servicesRes.value.data as K8sServiceSummary[]);
|
||||
} else if (servicesRes.status === "rejected") {
|
||||
console.error("Failed to fetch services:", servicesRes.reason);
|
||||
}
|
||||
|
||||
if (configMapsRes.status === "fulfilled" && configMapsRes.value.data) {
|
||||
setConfigMaps(configMapsRes.value.data as K8sConfigMapSummary[]);
|
||||
} else if (configMapsRes.status === "rejected") {
|
||||
console.error("Failed to fetch configmaps:", configMapsRes.reason);
|
||||
}
|
||||
|
||||
if (minecraftServersRes.status === "fulfilled" && minecraftServersRes.value.data) {
|
||||
setMinecraftServers(minecraftServersRes.value.data as CustomResourceSummary[]);
|
||||
} else if (minecraftServersRes.status === "rejected") {
|
||||
console.error("Failed to fetch minecraft servers:", minecraftServersRes.reason);
|
||||
}
|
||||
|
||||
if (reverseProxyServersRes.status === "fulfilled" && reverseProxyServersRes.value.data) {
|
||||
setReverseProxyServers(reverseProxyServersRes.value.data as CustomResourceSummary[]);
|
||||
} else if (reverseProxyServersRes.status === "rejected") {
|
||||
console.error("Failed to fetch reverse proxy servers:", reverseProxyServersRes.reason);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const errorMessage =
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import type { NormalServer, ReverseProxyServer } from "@minikura/api";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { getReverseProxyApi } from "@/lib/api-helpers";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
export function useServerList() {
|
||||
const [normalServers, setNormalServers] = useState<NormalServer[]>([]);
|
||||
@@ -23,8 +23,7 @@ export function useServerList() {
|
||||
if (proxyRes.data) {
|
||||
setReverseProxies(proxyRes.data as unknown as ReverseProxyServer[]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch servers:", error);
|
||||
} catch (_error) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -32,17 +31,12 @@ export function useServerList() {
|
||||
|
||||
const deleteServer = useCallback(
|
||||
async (id: string, type: "normal" | "proxy") => {
|
||||
try {
|
||||
if (type === "normal") {
|
||||
await api.api.servers({ id }).delete();
|
||||
} else {
|
||||
await getReverseProxyApi()({ id }).delete();
|
||||
}
|
||||
await fetchServers();
|
||||
} catch (error) {
|
||||
console.error("Failed to delete server:", error);
|
||||
throw error;
|
||||
if (type === "normal") {
|
||||
await api.api.servers({ id }).delete();
|
||||
} else {
|
||||
await getReverseProxyApi()({ id }).delete();
|
||||
}
|
||||
await fetchServers();
|
||||
},
|
||||
[fetchServers]
|
||||
);
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import type {
|
||||
ConnectionInfo,
|
||||
DeploymentInfo,
|
||||
PodInfo,
|
||||
StatefulSetInfo,
|
||||
} from "@minikura/api";
|
||||
import type { ConnectionInfo, DeploymentInfo, PodInfo, StatefulSetInfo } from "@minikura/api";
|
||||
import { labelKeys } from "@minikura/api";
|
||||
import { useCallback, useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { api } from "@/lib/api-client";
|
||||
|
||||
export function useServerLogs(serverId: string) {
|
||||
const [pods, setPods] = useState<PodInfo[]>([]);
|
||||
@@ -23,8 +18,7 @@ export function useServerLogs(serverId: string) {
|
||||
if (response.data) {
|
||||
setPods(response.data as PodInfo[]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch pods:", error);
|
||||
} catch (_error) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -42,9 +36,7 @@ export function useServerLogs(serverId: string) {
|
||||
setStatefulSetInfo(serverStatefulSet);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch StatefulSet info:", error);
|
||||
}
|
||||
} catch (_error) {}
|
||||
}, [serverId]);
|
||||
|
||||
const fetchDeploymentInfo = useCallback(async () => {
|
||||
@@ -59,9 +51,7 @@ export function useServerLogs(serverId: string) {
|
||||
setDeploymentInfo(serverDeployment);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch Deployment info:", error);
|
||||
}
|
||||
} catch (_error) {}
|
||||
}, [serverId]);
|
||||
|
||||
const fetchConnectionInfo = useCallback(async () => {
|
||||
@@ -70,9 +60,7 @@ export function useServerLogs(serverId: string) {
|
||||
if (response.data) {
|
||||
setConnectionInfo(response.data as ConnectionInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch connection info:", error);
|
||||
}
|
||||
} catch (_error) {}
|
||||
}, [serverId]);
|
||||
|
||||
const refreshAll = useCallback(async () => {
|
||||
|
||||
170
apps/web/hooks/use-topology-data.ts
Normal file
170
apps/web/hooks/use-topology-data.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
|
||||
import type { ConnectionInfo, K8sNodeSummary, PodInfo } from "@minikura/api";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { getReverseProxyApi } from "@/lib/api-helpers";
|
||||
import type { TopologyGraph } from "@/lib/topology-types";
|
||||
import { buildTopologyGraph } from "@/lib/topology-utils";
|
||||
import { useServerList } from "./use-server-list";
|
||||
|
||||
export function useTopologyData() {
|
||||
const { normalServers, reverseProxies, loading: serversLoading } = useServerList();
|
||||
|
||||
const [graph, setGraph] = useState<TopologyGraph | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
||||
|
||||
const fetchTopologyData = useCallback(
|
||||
async (isRefresh = false) => {
|
||||
try {
|
||||
if (!isRefresh) {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
const nodesResponse = await api.api.k8s.nodes.get();
|
||||
const k8sNodes = (nodesResponse.data as K8sNodeSummary[]) || [];
|
||||
|
||||
const serverPodPromises = normalServers.map(async (server) => {
|
||||
try {
|
||||
const response = await api.api.k8s.servers({ serverId: server.id }).pods.get();
|
||||
return {
|
||||
serverId: server.id,
|
||||
pods: (response.data as PodInfo[]) || [],
|
||||
};
|
||||
} catch (_err) {
|
||||
return { serverId: server.id, pods: [] };
|
||||
}
|
||||
});
|
||||
|
||||
const proxyPodPromises = reverseProxies.map(async (proxy) => {
|
||||
try {
|
||||
const response = await api.api.k8s["reverse-proxy"]({
|
||||
serverId: proxy.id,
|
||||
}).pods.get();
|
||||
return {
|
||||
serverId: proxy.id,
|
||||
pods: (response.data as PodInfo[]) || [],
|
||||
};
|
||||
} catch (_err) {
|
||||
return { serverId: proxy.id, pods: [] };
|
||||
}
|
||||
});
|
||||
|
||||
const [serverPodsResults, proxyPodsResults] = await Promise.all([
|
||||
Promise.all(serverPodPromises),
|
||||
Promise.all(proxyPodPromises),
|
||||
]);
|
||||
|
||||
const serverPodsMap = new Map<string, PodInfo[]>();
|
||||
for (const result of serverPodsResults) {
|
||||
serverPodsMap.set(result.serverId, result.pods);
|
||||
}
|
||||
|
||||
const proxyPodsMap = new Map<string, PodInfo[]>();
|
||||
for (const result of proxyPodsResults) {
|
||||
proxyPodsMap.set(result.serverId, result.pods);
|
||||
}
|
||||
|
||||
const serverConnectionPromises = normalServers.map(async (server) => {
|
||||
try {
|
||||
const response = await api.api.servers({ id: server.id })["connection-info"].get();
|
||||
return {
|
||||
serverId: server.id,
|
||||
connectionInfo: response.data as ConnectionInfo,
|
||||
};
|
||||
} catch (_err) {
|
||||
return { serverId: server.id, connectionInfo: null };
|
||||
}
|
||||
});
|
||||
|
||||
const reverseProxyApi = getReverseProxyApi();
|
||||
const proxyConnectionPromises = reverseProxies.map(async (proxy) => {
|
||||
try {
|
||||
const response = await reverseProxyApi({ id: proxy.id })["connection-info"].get();
|
||||
return {
|
||||
serverId: proxy.id,
|
||||
connectionInfo: response.data as ConnectionInfo,
|
||||
};
|
||||
} catch (_err) {
|
||||
return { serverId: proxy.id, connectionInfo: null };
|
||||
}
|
||||
});
|
||||
|
||||
const [serverConnectionResults, proxyConnectionResults] = await Promise.all([
|
||||
Promise.all(serverConnectionPromises),
|
||||
Promise.all(proxyConnectionPromises),
|
||||
]);
|
||||
|
||||
const serverConnectionMap = new Map<string, ConnectionInfo | null>();
|
||||
for (const result of serverConnectionResults) {
|
||||
serverConnectionMap.set(result.serverId, result.connectionInfo);
|
||||
}
|
||||
|
||||
const proxyConnectionMap = new Map<string, ConnectionInfo | null>();
|
||||
for (const result of proxyConnectionResults) {
|
||||
proxyConnectionMap.set(result.serverId, result.connectionInfo);
|
||||
}
|
||||
|
||||
let podMetrics: any = { items: [] };
|
||||
let nodeMetrics: any = { items: [] };
|
||||
try {
|
||||
const [podMetricsRes, nodeMetricsRes] = await Promise.all([
|
||||
api.api.k8s.metrics.pods.get(),
|
||||
api.api.k8s.metrics.nodes.get(),
|
||||
]);
|
||||
podMetrics = podMetricsRes.data || { items: [] };
|
||||
nodeMetrics = nodeMetricsRes.data || { items: [] };
|
||||
} catch (_err) {}
|
||||
|
||||
const topologyGraph = buildTopologyGraph({
|
||||
servers: normalServers,
|
||||
proxies: reverseProxies,
|
||||
serverPods: serverPodsMap,
|
||||
proxyPods: proxyPodsMap,
|
||||
k8sNodes,
|
||||
serverConnections: serverConnectionMap,
|
||||
proxyConnections: proxyConnectionMap,
|
||||
podMetrics,
|
||||
nodeMetrics,
|
||||
});
|
||||
|
||||
setGraph(topologyGraph);
|
||||
setIsInitialLoad(false);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : "Failed to fetch topology data";
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
if (!isRefresh) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[normalServers, reverseProxies]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!serversLoading) {
|
||||
fetchTopologyData();
|
||||
}
|
||||
}, [serversLoading, fetchTopologyData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serversLoading || isInitialLoad) return;
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
fetchTopologyData(true);
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [serversLoading, isInitialLoad, fetchTopologyData]);
|
||||
|
||||
return {
|
||||
graph,
|
||||
loading: loading || serversLoading,
|
||||
error,
|
||||
refresh: fetchTopologyData,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user