dashboard servers
This commit is contained in:
@@ -2,5 +2,26 @@
|
|||||||
{
|
{
|
||||||
"name": "Main Server",
|
"name": "Main Server",
|
||||||
"ip": "10.7.1.2"
|
"ip": "10.7.1.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Second Server",
|
||||||
|
"ip": "10.7.1.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Second Server",
|
||||||
|
"ip": "10.7.1.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Second Server",
|
||||||
|
"ip": "10.7.1.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Second Server",
|
||||||
|
"ip": "10.7.1.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Second Server",
|
||||||
|
"ip": "10.7.1.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"SESH-344d851e-01ea-4010-8c6a-6205b6772055":"admin","SESH-cc54eaab-a8b9-45d6-8be2-406e346581e8":"admin","SESH-909ab0b4-89e9-4a48-aa16-b8d257421651":"admin","SESH-a930805a-319b-4079-9f81-64a7026e271c":"admin","SESH-8902f651-201d-4493-b1f7-763512cff5b6":"admin"}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
.server-container .server {
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-heading-inner {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-icon {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: var(--server-icon-bg);
|
||||||
|
color: rgb(37 99 235);
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-heading-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-heading-info div div {
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
width: max-content;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
padding-top: 3px;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-heading-info div div svg {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-heading-info div div p {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-heading-info div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-heading-info div p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14.5px;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
color: rgb(100 116 139);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-heading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-heading button {
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
height: max-content;
|
||||||
|
width: max-content;
|
||||||
|
padding: 8px;
|
||||||
|
align-items: center;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 7px;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-heading button:hover {
|
||||||
|
background-color: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-monitor h2 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-monitor h2 svg {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-monitor div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-monitor div p {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.All {
|
||||||
|
color: var(--fg);
|
||||||
|
background-color: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Online {
|
||||||
|
color: var(--online);
|
||||||
|
background-color: var(--online-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Offline {
|
||||||
|
color: var(--offline);
|
||||||
|
background-color: var(--offline-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Warning {
|
||||||
|
color: var(--warning);
|
||||||
|
background-color: var(--warning-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Maintenance {
|
||||||
|
color: var(--maintenance);
|
||||||
|
background-color: var(--maintenance-bg);
|
||||||
|
}
|
||||||
@@ -1,9 +1,41 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import Data from "../data";
|
import Data from "../data";
|
||||||
import { ServerAPI } from "@/types/server";
|
import Server, { ServerAPI } from "@/types/server";
|
||||||
|
|
||||||
const servers = new Data<ServerAPI>("sentryx/servers.json", "array");
|
const servers = new Data<ServerAPI[]>("sentryx/servers.json", "array");
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
return NextResponse.json({ message: "ok", servers: servers.data });
|
const { searchParams } = new URL(request.url);
|
||||||
|
const query = searchParams.get("s");
|
||||||
|
const currentServers = servers.data.map(
|
||||||
|
(s) =>
|
||||||
|
({
|
||||||
|
...s,
|
||||||
|
status: "online",
|
||||||
|
cpu: 20,
|
||||||
|
memory: 70,
|
||||||
|
storage: 40,
|
||||||
|
network: 30,
|
||||||
|
} as Server)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
let tokens = (query || "").toLocaleLowerCase().split(" ");
|
||||||
|
|
||||||
|
console.log(tokens);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
message: "ok",
|
||||||
|
servers: currentServers.filter((s) =>
|
||||||
|
tokens.some(
|
||||||
|
(t) => s.name.toLowerCase().includes(t) || s.status.includes(t)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
message: "ok",
|
||||||
|
servers: currentServers,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+109
-12
@@ -6,6 +6,13 @@ import { cn } from "@/lib/utils";
|
|||||||
import { CircleCheckBig, Cpu, MemoryStick, TriangleAlert } from "lucide-react";
|
import { CircleCheckBig, Cpu, MemoryStick, TriangleAlert } from "lucide-react";
|
||||||
import { ChartAreaGradient } from "./chart";
|
import { ChartAreaGradient } from "./chart";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import getSession from "@/hooks/getSession";
|
||||||
|
import ServerComponent from "./server";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import useSession from "@/hooks/useSession";
|
||||||
|
import Server from "@/types/server";
|
||||||
|
|
||||||
export function generateHourlyData() {
|
export function generateHourlyData() {
|
||||||
const result = [];
|
const result = [];
|
||||||
@@ -60,43 +67,133 @@ export function Summary({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Dashboard() {
|
function average<T>(values: Array<T>, p: (v: T) => number) {
|
||||||
// const session = await getSession();
|
return values.length > 0
|
||||||
|
? values.reduce((sum, value) => sum + p(value), 0) / values.length
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
const d = generateHourlyData();
|
const d = generateHourlyData();
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const session = useSession();
|
||||||
|
const [servers, setServers] = useState<Server[]>([]);
|
||||||
|
const [searchedServers, setSearchedServers] = useState<Server[]>([]);
|
||||||
|
const [serversQuery, setServersQuery] = useState("");
|
||||||
|
const [serverData, setServerData] = useState({
|
||||||
|
online: 0,
|
||||||
|
issues: 0,
|
||||||
|
cpu: 0,
|
||||||
|
memory: 0,
|
||||||
|
storage: 0,
|
||||||
|
network: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!session) return;
|
||||||
|
|
||||||
|
session.getServers().then((servers) => {
|
||||||
|
setServers(servers);
|
||||||
|
setServerData({
|
||||||
|
online: servers.reduce(
|
||||||
|
(sum, server) => (server.status == "online" ? sum + 1 : sum),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
issues: servers.reduce(
|
||||||
|
(sum, server) => (server.status == "warning" ? sum + 1 : sum),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
cpu: average(servers, (s) => s.cpu),
|
||||||
|
memory: average(servers, (s) => s.memory),
|
||||||
|
storage: average(servers, (s) => s.storage),
|
||||||
|
network: average(servers, (s) => s.network),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
let tokens = serversQuery.toLowerCase().split(" ");
|
||||||
|
setSearchedServers(
|
||||||
|
servers.filter((s) =>
|
||||||
|
tokens.some(
|
||||||
|
(t) => s.name.toLowerCase().includes(t) || s.status.includes(t)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, 30);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [serversQuery, servers]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col gap-5">
|
<div className="w-full flex flex-col gap-5">
|
||||||
<div className="grid gap-4 w-full grid-cols-2 xl:grid-cols-4">
|
<div className="grid gap-4 w-full grid-cols-2 xl:grid-cols-4">
|
||||||
<SummaryCard className="text-chart-3" value="69" icon={CircleCheckBig}>
|
<SummaryCard
|
||||||
|
className="text-chart-3"
|
||||||
|
value={`${serverData.online}`}
|
||||||
|
icon={CircleCheckBig}
|
||||||
|
>
|
||||||
<span className="hidden sm:inline">Online Servers</span>
|
<span className="hidden sm:inline">Online Servers</span>
|
||||||
<span className="inline sm:hidden">Online</span>
|
<span className="inline sm:hidden">Online</span>
|
||||||
</SummaryCard>
|
</SummaryCard>
|
||||||
<SummaryCard className="text-chart-4" value="2" icon={TriangleAlert}>
|
<SummaryCard
|
||||||
|
className="text-chart-4"
|
||||||
|
value={`${serverData.issues}`}
|
||||||
|
icon={TriangleAlert}
|
||||||
|
>
|
||||||
Issues
|
Issues
|
||||||
</SummaryCard>
|
</SummaryCard>
|
||||||
<SummaryCard className="text-chart-2" value="52%" icon={Cpu}>
|
<SummaryCard
|
||||||
|
className="text-chart-2"
|
||||||
|
value={`${serverData.cpu}%`}
|
||||||
|
icon={Cpu}
|
||||||
|
>
|
||||||
<span className="hidden sm:inline">Avg CPU Usage</span>
|
<span className="hidden sm:inline">Avg CPU Usage</span>
|
||||||
<span className="inline sm:hidden">CPU</span>
|
<span className="inline sm:hidden">CPU</span>
|
||||||
</SummaryCard>
|
</SummaryCard>
|
||||||
<SummaryCard className="text-chart-1" value="100%" icon={MemoryStick}>
|
<SummaryCard
|
||||||
|
className="text-chart-1"
|
||||||
|
value={`${serverData.memory}%`}
|
||||||
|
icon={MemoryStick}
|
||||||
|
>
|
||||||
<span className="hidden sm:inline">Avg Memory Usage</span>
|
<span className="hidden sm:inline">Avg Memory Usage</span>
|
||||||
<span className="inline sm:hidden">Memory</span>
|
<span className="inline sm:hidden">Memory</span>
|
||||||
</SummaryCard>
|
</SummaryCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full h-96 flex gap-5 flex-col md:flex-row">
|
<div className="w-full flex gap-5 flex-col md:flex-row">
|
||||||
|
<div className="h-96 w-full">
|
||||||
<ChartAreaGradient chartData={d} />
|
<ChartAreaGradient chartData={d} />
|
||||||
<Card>
|
</div>
|
||||||
|
<Card className="h-96">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>System Overview</CardTitle>
|
<CardTitle>System Overview</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-6 w-full md:w-96">
|
<CardContent className="flex flex-col gap-6 w-full md:w-96">
|
||||||
<Summary value={20}>Average CPU</Summary>
|
<Summary value={serverData.cpu}>Average CPU</Summary>
|
||||||
<Summary value={69}>Average Memory</Summary>
|
<Summary value={serverData.memory}>Average Memory</Summary>
|
||||||
<Summary value={69}>Storage</Summary>
|
<Summary value={serverData.storage}>Storage</Summary>
|
||||||
<Summary value={69}>Average Network</Summary>
|
<Summary value={serverData.network}>Average Network</Summary>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex flex-row gap-3">
|
||||||
|
<Input
|
||||||
|
onInput={(e) => setServersQuery(e.currentTarget.value)}
|
||||||
|
placeholder="Search servers by name, location or IP..."
|
||||||
|
/>
|
||||||
|
<Button onClick={() => {}}>Search</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||||
|
{searchedServers.map((s, i) => (
|
||||||
|
<ServerComponent server={s} key={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import Server from "@/types/server";
|
||||||
|
import { Cpu, HardDrive, MemoryStick, Network, Wifi } from "lucide-react";
|
||||||
|
|
||||||
|
function UsageBar({
|
||||||
|
value,
|
||||||
|
Icon,
|
||||||
|
text,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
value: number;
|
||||||
|
Icon: React.ElementType;
|
||||||
|
text: string;
|
||||||
|
color: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<div className="justify-between flex">
|
||||||
|
<Label className="text-md flex justify-center">
|
||||||
|
<Icon size={16} color={color} /> {text}
|
||||||
|
</Label>
|
||||||
|
<Label className="text-md">{value}%</Label>
|
||||||
|
</div>
|
||||||
|
<Progress value={value} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ServerComponent({ server }: { server: Server }) {
|
||||||
|
return (
|
||||||
|
<Card className="w-full transition-transform duration-500 hover:scale-102 hover:cursor-pointer">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{server.name}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{server.status[0].toUpperCase() + server.status.slice(1)} {" · "}
|
||||||
|
{server.ip}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="w-full flex flex-col gap-3.5">
|
||||||
|
<UsageBar
|
||||||
|
value={server.cpu}
|
||||||
|
Icon={Cpu}
|
||||||
|
text="CPU"
|
||||||
|
color="var(--chart-2)"
|
||||||
|
/>
|
||||||
|
<UsageBar
|
||||||
|
value={server.memory}
|
||||||
|
Icon={MemoryStick}
|
||||||
|
text="Memory"
|
||||||
|
color="var(--chart-1)"
|
||||||
|
/>
|
||||||
|
<UsageBar
|
||||||
|
value={server.storage}
|
||||||
|
Icon={HardDrive}
|
||||||
|
text="Storage"
|
||||||
|
color="var(--chart-5)"
|
||||||
|
/>
|
||||||
|
<UsageBar
|
||||||
|
value={server.network}
|
||||||
|
Icon={Network}
|
||||||
|
text="Network"
|
||||||
|
color="var(--chart-3)"
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
|||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<main className="flex-1 flex flex-col min-h-0">
|
<main className="flex-1 flex flex-col min-h-0">
|
||||||
<SidebarTrigger />
|
<SidebarTrigger />
|
||||||
<div className="px-4 lg:px-10 flex-1 min-h-0">{children}</div>
|
<div className="px-4 lg:px-10 flex-1 min-h-0 mb-6">{children}</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function useSession() {
|
|||||||
console.log(e);
|
console.log(e);
|
||||||
router.replace("/login");
|
router.replace("/login");
|
||||||
});
|
});
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
|
import Server from "@/types/server";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
export default class Session {
|
export default class Session {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
constructor(id: string) {
|
constructor(id: string) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getServers(query?: string): Promise<Server[]> {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
((
|
||||||
|
await axios.get(`/api/servers?s=${query || ""}`, {
|
||||||
|
params: { id: this.id },
|
||||||
|
})
|
||||||
|
).data.servers as Server[]) || []
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,8 @@ export interface ServerAPI {
|
|||||||
|
|
||||||
export default interface Server extends ServerAPI {
|
export default interface Server extends ServerAPI {
|
||||||
status: "online" | "offline" | "warning" | "maintenance";
|
status: "online" | "offline" | "warning" | "maintenance";
|
||||||
|
cpu: number;
|
||||||
|
memory: number;
|
||||||
|
storage: number;
|
||||||
|
network: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user