server deletion

This commit is contained in:
2025-07-17 06:42:40 +02:00
parent 1f798939f2
commit 9d22293ae8
14 changed files with 698 additions and 55 deletions
+2 -2
View File
@@ -55,6 +55,6 @@ export async function GET(request: Request) {
return NextResponse.json({ message: "ok" });
}
export default function validate(sessionId: string) {
return sessions.data[sessionId] != undefined;
export function validate(sessionId: string | null) {
return sessionId && sessions.data[sessionId] != undefined;
}
+31 -1
View File
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
import Data from "../data";
import Server, { ServerAPI } from "@/types/server";
import { HttpStatusCode } from "axios";
import validate from "../auth/route";
import { validate } from "../auth/route";
const servers = new Data<ServerAPI[]>("sentryx/servers.json", []);
@@ -73,3 +73,33 @@ export async function POST(request: Request) {
message: "ok",
});
}
export async function DELETE(request: Request) {
const { searchParams } = new URL(request.url);
const sessionId = searchParams.get("sessionId");
const index = searchParams.get("index");
if (!validate(sessionId))
return NextResponse.json(
{ message: "Invalid session" },
{ status: HttpStatusCode.Unauthorized }
);
if (!index)
return NextResponse.json(
{
message: "The index is invalid",
},
{
status: HttpStatusCode.BadRequest,
}
);
servers.data.splice(parseInt(index), 1);
servers.write();
return NextResponse.json({
message: "ok",
});
}
+18 -1
View File
@@ -13,6 +13,7 @@ import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
import useSession from "@/hooks/useSession";
import Server from "@/types/server";
import { toast } from "sonner";
export function generateHourlyData() {
const result = [];
@@ -191,7 +192,23 @@ export default function Dashboard() {
<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} />
<ServerComponent
server={s}
key={i}
onDelete={() =>
session
?.removeServer(i)
.then(() => {
toast.info("Server successfully deleted");
})
.catch((e) => {
console.log(e.response.data.message);
toast.error(
`Error deleting server: ` + e.response.data.message
);
})
}
/>
))}
</div>
</div>
+112 -44
View File
@@ -1,3 +1,13 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
Card,
CardContent,
@@ -5,6 +15,12 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import { Label } from "@/components/ui/label";
import { Progress } from "@/components/ui/progress";
import Server from "@/types/server";
@@ -14,8 +30,11 @@ import {
MapPin,
MemoryStick,
Network,
Pencil,
Trash,
Wifi,
} from "lucide-react";
import { useState } from "react";
function UsageBar({
value,
@@ -41,50 +60,99 @@ function UsageBar({
);
}
export default function ServerComponent({ server }: { server: Server }) {
export default function ServerComponent({
server,
onDelete,
}: {
server: Server;
onDelete: () => void;
}) {
const [deleteAlert, setDeleteAlert] = useState(false);
return (
<Card className="w-full transition-transform duration-500 hover:scale-102 hover:cursor-pointer">
<CardHeader>
<CardTitle>{server.name}</CardTitle>
<CardDescription className="flex justify-between">
<div className="flex gap-2">
{server.status[0].toUpperCase() + server.status.slice(1)} {" · "}
{server.ip}
{server.location && (
<div className="flex items-center gap-0.5">
<MapPin size={14} />
{server.location}
</div>
)}
</div>
</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>
<>
<ContextMenu>
<ContextMenuTrigger>
<Card className="w-full transition-transform duration-500 hover:scale-102 hover:cursor-pointer">
<CardHeader>
<CardTitle>{server.name}</CardTitle>
<CardDescription className="flex justify-between">
<div className="flex gap-2">
{server.status[0].toUpperCase() + server.status.slice(1)}{" "}
{" · "}
{server.ip}
{server.location && (
<div className="flex items-center gap-0.5">
<MapPin size={14} />
{server.location}
</div>
)}
</div>
</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>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>
<Pencil className="text-current" />
Edit
</ContextMenuItem>
<ContextMenuItem
className="text-destructive"
onClick={() => setDeleteAlert(true)}
>
<Trash className="text-current" />
Delete
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<AlertDialog open={deleteAlert} onOpenChange={setDeleteAlert}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This action will permanently remove your server from the server
list
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className="text-destructive-foreground bg-destructive"
onClick={onDelete}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}
+18 -1
View File
@@ -9,6 +9,7 @@ import Server from "@/types/server";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import Map from "./Map";
import NewServerDialog from "./NewServerDialog";
import { toast } from "sonner";
export default function Servers() {
const session = useSession();
@@ -59,7 +60,23 @@ export default function Servers() {
<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} />
<ServerComponent
server={s}
key={i}
onDelete={() =>
session
?.removeServer(i)
.then(() => {
toast.info("Server successfully deleted");
})
.catch((e) => {
console.log(e.response.data.message);
toast.error(
`Error deleting server: ` + e.response.data.message
);
})
}
/>
))}
</div>
</TabsContent>
+157
View File
@@ -0,0 +1,157 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
+252
View File
@@ -0,0 +1,252 @@
"use client"
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function ContextMenu({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
}
function ContextMenuTrigger({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
)
}
function ContextMenuGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
)
}
function ContextMenuPortal({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
)
}
function ContextMenuSub({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
}
function ContextMenuRadioGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
)
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
)
}
function ContextMenuSubContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
function ContextMenuContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
)
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
)
}
function ContextMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
)
}
function ContextMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function ContextMenuSeparator({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}
+6
View File
@@ -25,4 +25,10 @@ export default class Session {
public async addServer(s: ServerAPI) {
return await axios.post("/api/servers", { ...s, sessionId: this.id });
}
public async removeServer(index: number) {
return await axios.delete("/api/servers", {
params: { index, sessionId: this.id },
});
}
}