mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-09-13 18:59:25 +00:00
🐛 fix: align dashboard with secured APIs
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { NormalServer, ReverseProxyServer } from "@minikura/api";
|
||||
import { getErrorMessage } from "@minikura/shared/errors";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { getReverseProxyApi } from "@/lib/api-helpers";
|
||||
@@ -19,6 +20,9 @@ export function useServerList() {
|
||||
getReverseProxyApi().get(),
|
||||
]);
|
||||
|
||||
if (normalRes.error) throw normalRes.error;
|
||||
if (proxyRes.error) throw proxyRes.error;
|
||||
|
||||
if (normalRes.data) {
|
||||
setNormalServers(normalRes.data as unknown as NormalServer[]);
|
||||
}
|
||||
@@ -26,7 +30,7 @@ export function useServerList() {
|
||||
setReverseProxies(proxyRes.data as unknown as ReverseProxyServer[]);
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : "Failed to load servers");
|
||||
setError(getErrorMessage(error));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -35,9 +39,11 @@ export function useServerList() {
|
||||
const deleteServer = useCallback(
|
||||
async (id: string, type: "normal" | "proxy") => {
|
||||
if (type === "normal") {
|
||||
await api.api.servers({ id }).delete();
|
||||
const response = await api.api.servers({ id }).delete();
|
||||
if (response.error) throw response.error;
|
||||
} else {
|
||||
await getReverseProxyApi()({ id }).delete();
|
||||
const response = await getReverseProxyApi()({ id }).delete();
|
||||
if (response.error) throw response.error;
|
||||
}
|
||||
await fetchServers();
|
||||
},
|
||||
|
||||
@@ -1,23 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import type { ConnectionInfo, CustomResourceSummary, K8sNodeSummary, PodInfo } from "@minikura/api";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { getErrorMessage } from "@minikura/shared/errors";
|
||||
import { useCallback, useEffect, useRef, 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";
|
||||
|
||||
const DATABASE_ID_LABEL = "minikura.kirameki.cafe/database-id";
|
||||
|
||||
function kubernetesResourceName(id: string): string {
|
||||
return id
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9.-]+/g, "-")
|
||||
.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, "")
|
||||
.slice(0, 63);
|
||||
}
|
||||
|
||||
function assertResponse<T>(response: { data?: unknown; error?: unknown }, fallback: string): T {
|
||||
if (response.error) throw new Error(getErrorMessage(response.error));
|
||||
if (response.data === undefined || response.data === null) throw new Error(fallback);
|
||||
return response.data as T;
|
||||
}
|
||||
|
||||
export function useTopologyData() {
|
||||
const { normalServers, reverseProxies, loading: serversLoading } = useServerList();
|
||||
const {
|
||||
normalServers,
|
||||
reverseProxies,
|
||||
loading: serversLoading,
|
||||
error: serversError,
|
||||
} = 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 [refreshing, setRefreshing] = useState(false);
|
||||
const requestSequence = useRef(0);
|
||||
|
||||
const fetchTopologyData = useCallback(
|
||||
async (isRefresh = false) => {
|
||||
const sequence = ++requestSequence.current;
|
||||
if (isRefresh) setRefreshing(true);
|
||||
try {
|
||||
if (!isRefresh) {
|
||||
setLoading(true);
|
||||
@@ -25,14 +51,17 @@ export function useTopologyData() {
|
||||
setError(null);
|
||||
|
||||
const nodesResponse = await api.api.k8s.nodes.get();
|
||||
const k8sNodes = (nodesResponse.data as K8sNodeSummary[]) || [];
|
||||
const k8sNodes = assertResponse<K8sNodeSummary[]>(
|
||||
nodesResponse,
|
||||
"Failed to load Kubernetes nodes"
|
||||
);
|
||||
|
||||
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[]) || [],
|
||||
pods: assertResponse<PodInfo[]>(response, `Failed to load pods for ${server.id}`),
|
||||
};
|
||||
} catch (_err) {
|
||||
return { serverId: server.id, pods: [] };
|
||||
@@ -46,7 +75,7 @@ export function useTopologyData() {
|
||||
}).pods.get();
|
||||
return {
|
||||
serverId: proxy.id,
|
||||
pods: (response.data as PodInfo[]) || [],
|
||||
pods: assertResponse<PodInfo[]>(response, `Failed to load pods for ${proxy.id}`),
|
||||
};
|
||||
} catch (_err) {
|
||||
return { serverId: proxy.id, pods: [] };
|
||||
@@ -73,7 +102,10 @@ export function useTopologyData() {
|
||||
const response = await api.api.servers({ id: server.id })["connection-info"].get();
|
||||
return {
|
||||
serverId: server.id,
|
||||
connectionInfo: response.data as ConnectionInfo,
|
||||
connectionInfo: assertResponse<ConnectionInfo>(
|
||||
response,
|
||||
"Connection information unavailable"
|
||||
),
|
||||
};
|
||||
} catch (_err) {
|
||||
return { serverId: server.id, connectionInfo: null };
|
||||
@@ -86,7 +118,10 @@ export function useTopologyData() {
|
||||
const response = await reverseProxyApi({ id: proxy.id })["connection-info"].get();
|
||||
return {
|
||||
serverId: proxy.id,
|
||||
connectionInfo: response.data as ConnectionInfo,
|
||||
connectionInfo: assertResponse<ConnectionInfo>(
|
||||
response,
|
||||
"Connection information unavailable"
|
||||
),
|
||||
};
|
||||
} catch (_err) {
|
||||
return { serverId: proxy.id, connectionInfo: null };
|
||||
@@ -120,18 +155,49 @@ export function useTopologyData() {
|
||||
} catch (_err) {}
|
||||
|
||||
const proxyBackends = new Map<string, string[]>();
|
||||
try {
|
||||
const crResponse = await api.api.k8s["reverse-proxy-servers"].get();
|
||||
for (const cr of (crResponse.data as CustomResourceSummary[]) || []) {
|
||||
const backends = cr.status?.backends;
|
||||
if (cr.name && Array.isArray(backends)) {
|
||||
proxyBackends.set(
|
||||
cr.name,
|
||||
backends.filter((id): id is string => typeof id === "string")
|
||||
);
|
||||
}
|
||||
const [serverCrResponse, proxyCrResponse] = await Promise.all([
|
||||
api.api.k8s["minecraft-servers"].get(),
|
||||
api.api.k8s["reverse-proxy-servers"].get(),
|
||||
]);
|
||||
const serverCrs = assertResponse<CustomResourceSummary[]>(
|
||||
serverCrResponse,
|
||||
"Minecraft server status unavailable"
|
||||
);
|
||||
const proxyCrs = assertResponse<CustomResourceSummary[]>(
|
||||
proxyCrResponse,
|
||||
"Reverse proxy status unavailable"
|
||||
);
|
||||
const serverIdByK8sName = new Map(
|
||||
normalServers.map((server) => [kubernetesResourceName(server.id), server.id])
|
||||
);
|
||||
for (const cr of serverCrs) {
|
||||
if (!cr.name) continue;
|
||||
const databaseId = cr.annotations?.[DATABASE_ID_LABEL] ?? cr.labels?.[DATABASE_ID_LABEL];
|
||||
if (databaseId) serverIdByK8sName.set(cr.name, databaseId);
|
||||
}
|
||||
|
||||
const proxyIdByK8sName = new Map(
|
||||
reverseProxies.map((proxy) => [kubernetesResourceName(proxy.id), proxy.id])
|
||||
);
|
||||
for (const cr of proxyCrs) {
|
||||
if (!cr.name) continue;
|
||||
const databaseId = cr.annotations?.[DATABASE_ID_LABEL] ?? cr.labels?.[DATABASE_ID_LABEL];
|
||||
if (databaseId) proxyIdByK8sName.set(cr.name, databaseId);
|
||||
}
|
||||
|
||||
for (const cr of proxyCrs) {
|
||||
const backends = cr.status?.backends;
|
||||
const proxyId = cr.name ? proxyIdByK8sName.get(cr.name) : undefined;
|
||||
if (proxyId && Array.isArray(backends)) {
|
||||
proxyBackends.set(
|
||||
proxyId,
|
||||
backends
|
||||
.filter((id): id is string => typeof id === "string")
|
||||
.map((id) => serverIdByK8sName.get(id))
|
||||
.filter((id): id is string => Boolean(id))
|
||||
);
|
||||
}
|
||||
} catch (_err) {}
|
||||
}
|
||||
|
||||
const topologyGraph = buildTopologyGraph({
|
||||
servers: normalServers,
|
||||
@@ -146,14 +212,17 @@ export function useTopologyData() {
|
||||
nodeMetrics,
|
||||
});
|
||||
|
||||
setGraph(topologyGraph);
|
||||
setIsInitialLoad(false);
|
||||
if (sequence === requestSequence.current) {
|
||||
setGraph(topologyGraph);
|
||||
setError(null);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : "Failed to fetch topology data";
|
||||
setError(errorMessage);
|
||||
if (sequence === requestSequence.current) setError(getErrorMessage(err));
|
||||
} finally {
|
||||
if (!isRefresh) {
|
||||
setLoading(false);
|
||||
if (sequence === requestSequence.current) {
|
||||
setIsInitialLoad(false);
|
||||
if (!isRefresh) setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -161,10 +230,10 @@ export function useTopologyData() {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!serversLoading) {
|
||||
if (!serversLoading && !serversError) {
|
||||
fetchTopologyData();
|
||||
}
|
||||
}, [serversLoading, fetchTopologyData]);
|
||||
}, [serversLoading, serversError, fetchTopologyData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serversLoading || isInitialLoad) return;
|
||||
@@ -178,8 +247,9 @@ export function useTopologyData() {
|
||||
|
||||
return {
|
||||
graph,
|
||||
loading: loading || serversLoading,
|
||||
error,
|
||||
loading: serversLoading || (loading && !serversError),
|
||||
error: serversError || error,
|
||||
refreshing,
|
||||
refresh: fetchTopologyData,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user