feat: add server details
This commit is contained in:
79
frontend/src/models/cityflags.ts
Normal file
79
frontend/src/models/cityflags.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
export const cityToFlag: Record<string, string> = {
|
||||||
|
Vienna: "🇦🇹",
|
||||||
|
"Vienna (DATASIX)": "🇦🇹",
|
||||||
|
Klagenfurt: "🇦🇹",
|
||||||
|
"Vienna (InterXion)": "🇦🇹",
|
||||||
|
Sofia: "🇧🇬",
|
||||||
|
Prague: "🇨🇿",
|
||||||
|
Copenhagen: "🇩🇰",
|
||||||
|
Helsinki: "🇫🇮",
|
||||||
|
Paris: "🇫🇷",
|
||||||
|
Marseille: "🇫🇷",
|
||||||
|
Munich: "🇩🇪",
|
||||||
|
Frankfurt: "🇩🇪",
|
||||||
|
Nuremberg: "🇩🇪",
|
||||||
|
Athens: "🇬🇷",
|
||||||
|
Budapest: "🇭🇺",
|
||||||
|
Dublin: "🇮🇪",
|
||||||
|
Milan: "🇮🇹",
|
||||||
|
Palermo: "🇮🇹",
|
||||||
|
Luxembourg: "🇱🇺",
|
||||||
|
Amsterdam: "🇳🇱",
|
||||||
|
Oslo: "🇳🇴",
|
||||||
|
Warsaw: "🇵🇱",
|
||||||
|
Lisbon: "🇵🇹",
|
||||||
|
Bucharest: "🇷🇴",
|
||||||
|
"St. Petersburg": "🇷🇺",
|
||||||
|
Bratislava: "🇸🇰",
|
||||||
|
Ljubljana: "🇸🇮",
|
||||||
|
Madrid: "🇪🇸",
|
||||||
|
Stockholm: "🇸🇪",
|
||||||
|
Zurich: "🇨🇭",
|
||||||
|
Ankara: "🇹🇷",
|
||||||
|
Istanbul: "🇹🇷",
|
||||||
|
Kyiv: "🇺🇦",
|
||||||
|
London: "🇬🇧",
|
||||||
|
Manchester: "🇬🇧",
|
||||||
|
Belgrade: "🇷🇸",
|
||||||
|
"New York City": "🇺🇸",
|
||||||
|
Denver: "🇺🇸",
|
||||||
|
"Los Angeles": "🇺🇸",
|
||||||
|
Miami: "🇺🇸",
|
||||||
|
Seattle: "🇺🇸",
|
||||||
|
Chicago: "🇺🇸",
|
||||||
|
Atlanta: "🇺🇸",
|
||||||
|
Honolulu: "🇺🇸",
|
||||||
|
Dallas: "🇺🇸",
|
||||||
|
Manassas: "🇺🇸",
|
||||||
|
Vancouver: "🇨🇦",
|
||||||
|
Toronto: "🇨🇦",
|
||||||
|
"Mexico City": "🇲🇽",
|
||||||
|
"Buenos Aires": "🇦🇷",
|
||||||
|
"Rio de Janeiro": "🇧🇷",
|
||||||
|
"São Paulo": "🇧🇷",
|
||||||
|
"Santiago de Chile": "🇨🇱",
|
||||||
|
Medellín: "🇨🇴",
|
||||||
|
Lima: "🇵🇪",
|
||||||
|
|
||||||
|
Sydney: "🇦🇺",
|
||||||
|
Brisbane: "🇦🇺",
|
||||||
|
Perth: "🇦🇺",
|
||||||
|
Tianjin: "🇨🇳",
|
||||||
|
"Hong Kong": "🇭🇰",
|
||||||
|
Chennai: "🇮🇳",
|
||||||
|
Delhi: "🇮🇳",
|
||||||
|
Mumbai: "🇮🇳",
|
||||||
|
Jakarta: "🇮🇩",
|
||||||
|
"Tel Aviv": "🇮🇱",
|
||||||
|
Osaka: "🇯🇵",
|
||||||
|
Tokyo: "🇯🇵",
|
||||||
|
Seoul: "🇰🇷",
|
||||||
|
"Kuala Lumpur": "🇲🇾",
|
||||||
|
Singapore: "🇸🇬",
|
||||||
|
Taipei: "🇹🇼",
|
||||||
|
Bangkok: "🇹🇭",
|
||||||
|
Dubai: "🇦🇪",
|
||||||
|
Hanoi: "🇻🇳",
|
||||||
|
|
||||||
|
Johannesburg: "🇿🇦",
|
||||||
|
};
|
||||||
89
frontend/src/models/server.ts
Normal file
89
frontend/src/models/server.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
export interface Server {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
hostname: string;
|
||||||
|
nickname: string;
|
||||||
|
disabled: boolean;
|
||||||
|
template: Template;
|
||||||
|
serverLiveInfo: ServerLiveInfo;
|
||||||
|
ipv4Addresses: Ipv4Address[];
|
||||||
|
ipv6Addresses: Ipv6Address[];
|
||||||
|
site: Site;
|
||||||
|
snapshotCount: number;
|
||||||
|
maxCpuCount: number;
|
||||||
|
disksAvailableSpaceInMiB: number;
|
||||||
|
rescueSystemActive: boolean;
|
||||||
|
snapshotAllowed: boolean;
|
||||||
|
architecture: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Template {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerLiveInfo {
|
||||||
|
state: string;
|
||||||
|
autostart: boolean;
|
||||||
|
uefi: boolean;
|
||||||
|
interfaces: Interface[];
|
||||||
|
disks: Disk[];
|
||||||
|
bootorder: string[];
|
||||||
|
requiredStorageOptimization: string;
|
||||||
|
template: string;
|
||||||
|
uptimeInSeconds: number;
|
||||||
|
currentServerMemoryInMiB: number;
|
||||||
|
maxServerMemoryInMiB: number;
|
||||||
|
cpuCount: number;
|
||||||
|
cpuMaxCount: number;
|
||||||
|
sockets: number;
|
||||||
|
coresPerSocket: number;
|
||||||
|
latestQemu: boolean;
|
||||||
|
configChanged: boolean;
|
||||||
|
osOptimization: string;
|
||||||
|
nestedGuest: boolean;
|
||||||
|
machineType: string;
|
||||||
|
keyboardLayout: string;
|
||||||
|
cloudinitAttached: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Interface {
|
||||||
|
mac: string;
|
||||||
|
driver: string;
|
||||||
|
mtu: number;
|
||||||
|
speedInMBits: number;
|
||||||
|
rxMonthlyInMiB: number;
|
||||||
|
txMonthlyInMiB: number;
|
||||||
|
ipv4Addresses: string[];
|
||||||
|
ipv6LinkLocalAddresses: string[];
|
||||||
|
ipv6NetworkPrefixes: string[];
|
||||||
|
trafficThrottled: boolean;
|
||||||
|
vlanInterface: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Disk {
|
||||||
|
dev: string;
|
||||||
|
driver: string;
|
||||||
|
capacityInMiB: number;
|
||||||
|
allocationInMiB: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ipv4Address {
|
||||||
|
id: number;
|
||||||
|
ip: string;
|
||||||
|
netmask: string;
|
||||||
|
gateway: string;
|
||||||
|
broadcast: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ipv6Address {
|
||||||
|
id: number;
|
||||||
|
networkPrefix: string;
|
||||||
|
networkPrefixLength: number;
|
||||||
|
gateway: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Site {
|
||||||
|
id: number;
|
||||||
|
city: string;
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
import { Route as Server_listRouteImport } from './routes/server_list'
|
import { Route as Server_listRouteImport } from './routes/server_list'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
|
import { Route as ServerServerIdRouteImport } from './routes/server.$serverId'
|
||||||
|
|
||||||
const Server_listRoute = Server_listRouteImport.update({
|
const Server_listRoute = Server_listRouteImport.update({
|
||||||
id: '/server_list',
|
id: '/server_list',
|
||||||
@@ -22,31 +23,40 @@ const IndexRoute = IndexRouteImport.update({
|
|||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ServerServerIdRoute = ServerServerIdRouteImport.update({
|
||||||
|
id: '/server/$serverId',
|
||||||
|
path: '/server/$serverId',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/server_list': typeof Server_listRoute
|
'/server_list': typeof Server_listRoute
|
||||||
|
'/server/$serverId': typeof ServerServerIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/server_list': typeof Server_listRoute
|
'/server_list': typeof Server_listRoute
|
||||||
|
'/server/$serverId': typeof ServerServerIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/server_list': typeof Server_listRoute
|
'/server_list': typeof Server_listRoute
|
||||||
|
'/server/$serverId': typeof ServerServerIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/server_list'
|
fullPaths: '/' | '/server_list' | '/server/$serverId'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/server_list'
|
to: '/' | '/server_list' | '/server/$serverId'
|
||||||
id: '__root__' | '/' | '/server_list'
|
id: '__root__' | '/' | '/server_list' | '/server/$serverId'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
Server_listRoute: typeof Server_listRoute
|
Server_listRoute: typeof Server_listRoute
|
||||||
|
ServerServerIdRoute: typeof ServerServerIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -65,12 +75,20 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexRouteImport
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/server/$serverId': {
|
||||||
|
id: '/server/$serverId'
|
||||||
|
path: '/server/$serverId'
|
||||||
|
fullPath: '/server/$serverId'
|
||||||
|
preLoaderRoute: typeof ServerServerIdRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
Server_listRoute: Server_listRoute,
|
Server_listRoute: Server_listRoute,
|
||||||
|
ServerServerIdRoute: ServerServerIdRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|||||||
@@ -6,32 +6,5 @@ export const Route = createFileRoute("/")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return <div className="text-center">Dashboard coming soon!</div>;
|
||||||
<div className="text-center">
|
|
||||||
<header className="min-h-full flex flex-col items-center justify-center text-white text-[calc(10px+2vmin)]">
|
|
||||||
<img
|
|
||||||
src={logo}
|
|
||||||
className="h-[40vmin] pointer-events-none animate-[spin_20s_linear_infinite]"
|
|
||||||
alt="logo"
|
|
||||||
/>
|
|
||||||
<p>Hehe</p>
|
|
||||||
<a
|
|
||||||
className="text-[#61dafb] hover:underline"
|
|
||||||
href="https://reactjs.org"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Learn React
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="text-[#61dafb] hover:underline"
|
|
||||||
href="https://tanstack.com"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Learn TanStack
|
|
||||||
</a>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
721
frontend/src/routes/server.$serverId.tsx
Normal file
721
frontend/src/routes/server.$serverId.tsx
Normal file
@@ -0,0 +1,721 @@
|
|||||||
|
import type { Server } from "@/models/server";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { createFileRoute, Link, useParams } from "@tanstack/react-router";
|
||||||
|
import { AlertTriangle, CheckCircle2, CircleOff } from "lucide-react";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cityToFlag } from "@/models/cityflags";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/server/$serverId")({
|
||||||
|
component: RouteComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { serverId } = useParams({ strict: false });
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ["server", serverId],
|
||||||
|
enabled: Boolean(serverId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch(`/api/servers/${serverId}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load server (${response.status})`);
|
||||||
|
}
|
||||||
|
return (await response.json()) as Server;
|
||||||
|
},
|
||||||
|
refetchInterval: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (query.isLoading) {
|
||||||
|
return <ServerDetailsSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.isError) {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-5xl p-6">
|
||||||
|
<Link to="/server_list">
|
||||||
|
<Button variant="outline">Back to list</Button>
|
||||||
|
</Link>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-destructive" />
|
||||||
|
Failed to load server
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{(query.error as Error).message}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query.data) return null;
|
||||||
|
|
||||||
|
return <ServerDetails server={query.data} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ServerDetails({ server }: { server: Server }) {
|
||||||
|
const state = server.serverLiveInfo?.state ?? "unknown";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-5xl space-y-6 p-6">
|
||||||
|
<Header server={server} />
|
||||||
|
|
||||||
|
<Tabs defaultValue="overview" className="space-y-4">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||||
|
<TabsTrigger value="live">Live</TabsTrigger>
|
||||||
|
<TabsTrigger value="network">Network</TabsTrigger>
|
||||||
|
<TabsTrigger value="storage">Storage</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="overview" className="space-y-4">
|
||||||
|
<Card className="min-w-175">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Summary</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<DetailItem label="Server ID" value={server.id} />
|
||||||
|
<DetailItem label="Architecture" value={server.architecture} />
|
||||||
|
<DetailItem label="Template" value={server.template?.name} />
|
||||||
|
<DetailItem
|
||||||
|
label="Site"
|
||||||
|
value={
|
||||||
|
server.site?.city + " " + cityToFlag[server.site?.city]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<FeatureBadge
|
||||||
|
label="Disabled"
|
||||||
|
enabled={server.disabled}
|
||||||
|
enabledVariant="destructive"
|
||||||
|
disabledText="Active"
|
||||||
|
/>
|
||||||
|
<FeatureBadge
|
||||||
|
label="Rescue system"
|
||||||
|
enabled={server.rescueSystemActive}
|
||||||
|
/>
|
||||||
|
<FeatureBadge
|
||||||
|
label="Snapshots allowed"
|
||||||
|
enabled={server.snapshotAllowed}
|
||||||
|
/>
|
||||||
|
<Badge variant="secondary">
|
||||||
|
Snapshots: {server.snapshotCount}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="secondary">Max CPU: {server.maxCpuCount}</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="live" className="space-y-4">
|
||||||
|
<Card className="min-w-175">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
Live status
|
||||||
|
<StateBadge state={state} />
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<DetailItem
|
||||||
|
label="Uptime"
|
||||||
|
value={formatUptime(server.serverLiveInfo?.uptimeInSeconds)}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Autostart"
|
||||||
|
value={formatBool(server.serverLiveInfo?.autostart)}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="UEFI"
|
||||||
|
value={formatBool(server.serverLiveInfo?.uefi)}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Latest QEMU"
|
||||||
|
value={formatBool(server.serverLiveInfo?.latestQemu)}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Config changed"
|
||||||
|
value={formatBool(server.serverLiveInfo?.configChanged)}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Cloud-init"
|
||||||
|
value={formatBool(server.serverLiveInfo?.cloudinitAttached)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<Card className="shadow-none">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base">CPU</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-3">
|
||||||
|
<DetailItem
|
||||||
|
label="CPU count"
|
||||||
|
value={server.serverLiveInfo?.cpuCount}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="CPU max"
|
||||||
|
value={server.serverLiveInfo?.cpuMaxCount}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Sockets"
|
||||||
|
value={server.serverLiveInfo?.sockets}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Cores / socket"
|
||||||
|
value={server.serverLiveInfo?.coresPerSocket}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="shadow-none">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base">Memory</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-3">
|
||||||
|
<DetailItem
|
||||||
|
label="Current"
|
||||||
|
value={formatMiB(
|
||||||
|
server.serverLiveInfo?.currentServerMemoryInMiB,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Max"
|
||||||
|
value={formatMiB(
|
||||||
|
server.serverLiveInfo?.maxServerMemoryInMiB,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<DetailItem
|
||||||
|
label="Machine type"
|
||||||
|
value={server.serverLiveInfo?.machineType}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Keyboard layout"
|
||||||
|
value={server.serverLiveInfo?.keyboardLayout}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="OS optimization"
|
||||||
|
value={server.serverLiveInfo?.osOptimization}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Required storage optimization"
|
||||||
|
value={server.serverLiveInfo?.requiredStorageOptimization}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="outline">
|
||||||
|
Nested guest: {formatBool(server.serverLiveInfo?.nestedGuest)}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline">
|
||||||
|
Boot order:{" "}
|
||||||
|
{(server.serverLiveInfo?.bootorder ?? []).join(", ") || "—"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="network" className="space-y-4">
|
||||||
|
<Card className="min-w-175">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>IP addresses</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">IPv4</div>
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>IP</TableHead>
|
||||||
|
<TableHead className="hidden sm:table-cell">
|
||||||
|
Netmask
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="hidden md:table-cell">
|
||||||
|
Gateway
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{(server.ipv4Addresses ?? []).length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={3}
|
||||||
|
className="text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
No IPv4 addresses
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
server.ipv4Addresses.map((a) => (
|
||||||
|
<TableRow key={a.id}>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{a.ip}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="hidden sm:table-cell">
|
||||||
|
{a.netmask}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="hidden md:table-cell">
|
||||||
|
{a.gateway}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">IPv6</div>
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Prefix</TableHead>
|
||||||
|
<TableHead className="hidden sm:table-cell">
|
||||||
|
Length
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="hidden md:table-cell">
|
||||||
|
Gateway
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{(server.ipv6Addresses ?? []).length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={3}
|
||||||
|
className="text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
No IPv6 addresses
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
server.ipv6Addresses.map((a) => (
|
||||||
|
<TableRow key={a.id}>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{a.networkPrefix}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="hidden sm:table-cell">
|
||||||
|
/{a.networkPrefixLength}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="hidden md:table-cell">
|
||||||
|
{a.gateway}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">Interfaces</div>
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>MAC</TableHead>
|
||||||
|
<TableHead className="hidden sm:table-cell">
|
||||||
|
Driver
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="hidden md:table-cell">
|
||||||
|
MTU
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="hidden lg:table-cell">
|
||||||
|
Speed
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>Flags</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{(server.serverLiveInfo?.interfaces ?? []).length ===
|
||||||
|
0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={5}
|
||||||
|
className="text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
No interfaces
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
server.serverLiveInfo.interfaces.map((i) => (
|
||||||
|
<TableRow key={i.mac}>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{i.mac}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="hidden sm:table-cell">
|
||||||
|
{i.driver}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="hidden md:table-cell">
|
||||||
|
{i.mtu}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="hidden lg:table-cell">
|
||||||
|
{i.speedInMBits} Mbit/s
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{i.vlanInterface && (
|
||||||
|
<Badge variant="secondary">VLAN</Badge>
|
||||||
|
)}
|
||||||
|
{i.trafficThrottled && (
|
||||||
|
<Badge variant="destructive">Throttled</Badge>
|
||||||
|
)}
|
||||||
|
<Badge variant="outline">
|
||||||
|
RX {formatMiB(i.rxMonthlyInMiB)}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline">
|
||||||
|
TX {formatMiB(i.txMonthlyInMiB)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(server.serverLiveInfo?.interfaces ?? []).map((i) => (
|
||||||
|
<Card key={`${i.mac}-ips`} className="mt-4 shadow-none">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base">
|
||||||
|
{i.mac} addresses
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
IPv4
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{(i.ipv4Addresses ?? []).length === 0 ? (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
—
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
i.ipv4Addresses.map((ip) => (
|
||||||
|
<Badge key={ip} variant="secondary">
|
||||||
|
{ip}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
IPv6 link-local
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{(i.ipv6LinkLocalAddresses ?? []).length === 0 ? (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
—
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
i.ipv6LinkLocalAddresses.map((ip) => (
|
||||||
|
<Badge key={ip} variant="secondary">
|
||||||
|
{ip}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
IPv6 prefixes
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{(i.ipv6NetworkPrefixes ?? []).length === 0 ? (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
—
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
i.ipv6NetworkPrefixes.map((p) => (
|
||||||
|
<Badge key={p} variant="outline">
|
||||||
|
{p}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="storage" className="space-y-4">
|
||||||
|
<Card className="min-w-175">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Storage</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<DetailItem
|
||||||
|
label="Disks available"
|
||||||
|
value={formatMiB(server.disksAvailableSpaceInMiB)}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Required optimization"
|
||||||
|
value={server.serverLiveInfo?.requiredStorageOptimization}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Template (live)"
|
||||||
|
value={server.serverLiveInfo?.template}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">Disks</div>
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Device</TableHead>
|
||||||
|
<TableHead className="hidden sm:table-cell">
|
||||||
|
Driver
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>Capacity</TableHead>
|
||||||
|
<TableHead className="hidden md:table-cell">
|
||||||
|
Allocation
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{(server.serverLiveInfo?.disks ?? []).length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={4}
|
||||||
|
className="text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
No disks
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
server.serverLiveInfo.disks.map((d) => (
|
||||||
|
<TableRow key={d.dev}>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{d.dev}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="hidden sm:table-cell">
|
||||||
|
{d.driver}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{formatMiB(d.capacityInMiB)}</TableCell>
|
||||||
|
<TableCell className="hidden md:table-cell">
|
||||||
|
{formatMiB(d.allocationInMiB)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Header({ server }: { server: Server }) {
|
||||||
|
const state = server.serverLiveInfo?.state ?? "unknown";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="space-y-3">
|
||||||
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<h1 className="truncate text-xl font-semibold leading-tight">
|
||||||
|
{server.name}
|
||||||
|
</h1>
|
||||||
|
{server.nickname ? (
|
||||||
|
<Badge variant="secondary" className="max-w-full truncate">
|
||||||
|
{server.nickname}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-sm text-muted-foreground">
|
||||||
|
<span className="font-mono">{server.hostname}</span>
|
||||||
|
<span className="mx-2">•</span>
|
||||||
|
<span>ID {server.id}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<StateBadge state={state} />
|
||||||
|
{server.disabled ? (
|
||||||
|
<Badge variant="destructive" className="gap-1">
|
||||||
|
<CircleOff className="h-3.5 w-3.5" />
|
||||||
|
Disabled
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline" className="gap-1">
|
||||||
|
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||||
|
Enabled
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StateBadge({ state }: { state: string }) {
|
||||||
|
const normalized = (state || "unknown").toLowerCase();
|
||||||
|
|
||||||
|
if (normalized === "running" || normalized === "online") {
|
||||||
|
return (
|
||||||
|
<Badge className="bg-emerald-600 hover:bg-emerald-600">Running</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized === "stopped" || normalized === "offline") {
|
||||||
|
return <Badge variant="secondary">Stopped</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Badge variant="outline">{state || "unknown"}</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailItem({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const v = value === null || value === undefined || value === "" ? "—" : value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-xs text-muted-foreground">{label}</div>
|
||||||
|
<div className="text-sm font-medium">{v}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FeatureBadge({
|
||||||
|
label,
|
||||||
|
enabled,
|
||||||
|
enabledVariant = "secondary",
|
||||||
|
disabledText = "No",
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
enabled: boolean;
|
||||||
|
enabledVariant?: "default" | "secondary" | "destructive" | "outline";
|
||||||
|
disabledText?: string;
|
||||||
|
}) {
|
||||||
|
if (enabled) {
|
||||||
|
return <Badge variant={enabledVariant}>{label}: Yes</Badge>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Badge variant="outline">
|
||||||
|
{label}: {disabledText}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBool(v: boolean | undefined) {
|
||||||
|
if (v === undefined) return "—";
|
||||||
|
return v ? "Yes" : "No";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMiB(mib: number | undefined) {
|
||||||
|
if (mib === undefined) return "—";
|
||||||
|
const gib = mib / 1024;
|
||||||
|
if (gib >= 10) return `${gib.toFixed(0)} GiB`;
|
||||||
|
if (gib >= 1) return `${gib.toFixed(1)} GiB`;
|
||||||
|
return `${mib.toLocaleString()} MiB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUptime(seconds: number | undefined) {
|
||||||
|
if (seconds === undefined) return "—";
|
||||||
|
const s = Math.max(0, Math.floor(seconds));
|
||||||
|
const d = Math.floor(s / 86400);
|
||||||
|
const h = Math.floor((s % 86400) / 3600);
|
||||||
|
const m = Math.floor((s % 3600) / 60);
|
||||||
|
if (d > 0) return `${d}d ${h}h ${m}m`;
|
||||||
|
if (h > 0) return `${h}h ${m}m`;
|
||||||
|
return `${m}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ServerDetailsSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-5xl space-y-6 p-6">
|
||||||
|
<Card className="min-w-175">
|
||||||
|
<CardHeader className="space-y-3">
|
||||||
|
<Skeleton className="h-6 w-72" />
|
||||||
|
<Skeleton className="h-4 w-96" />
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Skeleton className="h-6 w-24" />
|
||||||
|
<Skeleton className="h-6 w-24" />
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<Skeleton className="h-5 w-32" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<Skeleton className="h-12 w-full" />
|
||||||
|
<Skeleton className="h-12 w-full" />
|
||||||
|
<Skeleton className="h-12 w-full" />
|
||||||
|
<Skeleton className="h-12 w-full" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="h-36 w-full" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import type { MinimalServers } from "@/models/minimal_servers";
|
import type { MinimalServers } from "@/models/minimal_servers";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createFileRoute("/server_list")({
|
export const Route = createFileRoute("/server_list")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
@@ -24,6 +24,8 @@ function RouteComponent() {
|
|||||||
return (await res.json()) as MinimalServers;
|
return (await res.json()) as MinimalServers;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const navigate = useNavigate({ from: "/server_list" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-2 flex justify-center items-center w-full">
|
<div className="p-2 flex justify-center items-center w-full">
|
||||||
<Card className="w-full max-w-225 p-2">
|
<Card className="w-full max-w-225 p-2">
|
||||||
@@ -38,7 +40,13 @@ function RouteComponent() {
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{servers?.map((server) => (
|
{servers?.map((server) => (
|
||||||
<TableRow key={server.id}>
|
<TableRow
|
||||||
|
key={server.id}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
navigate({ to: `/server/${server.id}` });
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TableCell className="font-medium">{server.name}</TableCell>
|
<TableCell className="font-medium">{server.name}</TableCell>
|
||||||
<TableCell>{server.hostname}</TableCell>
|
<TableCell>{server.hostname}</TableCell>
|
||||||
<TableCell>{server.nickname}</TableCell>
|
<TableCell>{server.nickname}</TableCell>
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.service(auth::start_flow)
|
.service(auth::start_flow)
|
||||||
.service(auth::get_user)
|
.service(auth::get_user)
|
||||||
.service(servers::list_servers)
|
.service(servers::list_servers)
|
||||||
|
.service(servers::get_server)
|
||||||
.service(
|
.service(
|
||||||
Files::new("/", "./frontend/dist")
|
Files::new("/", "./frontend/dist")
|
||||||
.index_file("index.html")
|
.index_file("index.html")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use actix_web::{Responder, get};
|
use actix_web::{Responder, get, web};
|
||||||
|
|
||||||
use crate::helper;
|
use crate::helper;
|
||||||
|
|
||||||
@@ -12,3 +12,17 @@ pub async fn list_servers() -> impl Responder {
|
|||||||
|
|
||||||
serde_json::to_string(&response).unwrap()
|
serde_json::to_string(&response).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/api/servers/{id}")]
|
||||||
|
pub async fn get_server(id: web::Path<i32>) -> impl Responder {
|
||||||
|
let config = helper::get_authed_api_config().await.unwrap();
|
||||||
|
let response = scp_core::apis::default_api::api_v1_servers_server_id_get(
|
||||||
|
&config,
|
||||||
|
id.into_inner(),
|
||||||
|
Some(true),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
serde_json::to_string(&response).unwrap()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user