Basic VM functionality

This commit is contained in:
headlessdev 2025-04-18 14:35:36 +02:00
parent c266296c4f
commit 406091fdcb
8 changed files with 1015 additions and 256 deletions

View File

@ -28,6 +28,8 @@ export async function PUT(request: NextRequest) {
const updatedServer = await prisma.server.update({
where: { id },
data: {
host,
hostServer,
name,
os,
ip,

View File

@ -6,24 +6,39 @@ interface GetRequest {
ITEMS_PER_PAGE?: number;
}
export async function POST(request: NextRequest) {
try {
const body: GetRequest = await request.json();
const page = Math.max(1, body.page || 1);
const ITEMS_PER_PAGE = body.ITEMS_PER_PAGE || 4;
const servers = await prisma.server.findMany({
// Host-Server mit Paginierung holen
const hosts = await prisma.server.findMany({
where: { hostServer: null },
skip: (page - 1) * ITEMS_PER_PAGE,
take: ITEMS_PER_PAGE,
orderBy: { name: 'asc' }
});
const totalCount = await prisma.server.count();
const maxPage = Math.ceil(totalCount / ITEMS_PER_PAGE);
// VMs für alle Hosts sammeln
const hostsWithVms = await Promise.all(
hosts.map(async (host) => ({
...host,
hostedVMs: await prisma.server.findMany({
where: { hostServer: host.id },
orderBy: { name: 'asc' }
})
}))
);
const totalHosts = await prisma.server.count({
where: { hostServer: null }
});
const maxPage = Math.ceil(totalHosts / ITEMS_PER_PAGE);
return NextResponse.json({
servers,
servers: hostsWithVms,
maxPage
});
} catch (error: any) {

View File

@ -0,0 +1,13 @@
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
export async function GET(request: NextRequest) {
try {
const servers = await prisma.server.findMany({
where: { host: true },
});
return NextResponse.json({ servers });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -29,6 +29,7 @@ import {
Microchip,
MemoryStick,
HardDrive,
Server,
} from "lucide-react";
import {
Card,
@ -77,10 +78,15 @@ import Cookies from "js-cookie";
import { useState, useEffect } from "react";
import axios from "axios";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Checkbox } from "@/components/ui/checkbox";
import { Alert } from "@/components/ui/alert";
import { ScrollArea } from "@/components/ui/scroll-area";
interface Server {
id: number;
name: string;
host: boolean;
hostServer: number | null;
os?: string;
ip?: string;
url?: string;
@ -88,6 +94,7 @@ interface Server {
gpu?: string;
ram?: string;
disk?: string;
hostedVMs: Server[];
}
interface GetServersResponse {
@ -96,6 +103,8 @@ interface GetServersResponse {
}
export default function Dashboard() {
const [host, setHost] = useState<boolean>(false);
const [hostServer, setHostServer] = useState<number>(0);
const [name, setName] = useState<string>("");
const [os, setOs] = useState<string>("");
const [ip, setIp] = useState<string>("");
@ -113,6 +122,8 @@ export default function Dashboard() {
const [loading, setLoading] = useState<boolean>(true);
const [editId, setEditId] = useState<number | null>(null);
const [editHost, setEditHost] = useState<boolean>(false);
const [editHostServer, setEditHostServer] = useState<number | null>(0);
const [editName, setEditName] = useState<string>("");
const [editOs, setEditOs] = useState<string>("");
const [editIp, setEditIp] = useState<string>("");
@ -125,6 +136,9 @@ export default function Dashboard() {
const [searchTerm, setSearchTerm] = useState<string>("");
const [isSearching, setIsSearching] = useState<boolean>(false);
const [hostServers, setHostServers] = useState<Server[]>([]);
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
useEffect(() => {
const savedLayout = Cookies.get("layoutPreference-servers");
const layout_bool = savedLayout === "grid";
@ -146,6 +160,8 @@ export default function Dashboard() {
const add = async () => {
try {
await axios.post("/api/servers/add", {
host,
hostServer,
name,
os,
ip,
@ -155,6 +171,18 @@ export default function Dashboard() {
ram,
disk,
});
setIsAddDialogOpen(false);
setHost(false);
setHostServer(0);
setName("");
setOs("");
setIp("");
setUrl("");
setCpu("");
setGpu("");
setRam("");
setDisk("");
getServers();
} catch (error: any) {
console.log(error.response.data);
@ -171,6 +199,10 @@ export default function Dashboard() {
ITEMS_PER_PAGE: itemsPerPage,
}
);
for (const server of response.data.servers) {
console.log("Host Server:" + server.hostServer);
console.log("ID:" + server.id);
}
setServers(response.data.servers);
setMaxPage(response.data.maxPage);
setLoading(false);
@ -202,6 +234,8 @@ export default function Dashboard() {
const openEditDialog = (server: Server) => {
setEditId(server.id);
setEditHost(server.host);
setEditHostServer(server.hostServer || null);
setEditName(server.name);
setEditOs(server.os || "");
setEditIp(server.ip || "");
@ -218,6 +252,8 @@ export default function Dashboard() {
try {
await axios.put("/api/servers/edit", {
id: editId,
host: editHost,
hostServer: editHostServer,
name: editName,
os: editOs,
ip: editIp,
@ -261,6 +297,23 @@ export default function Dashboard() {
return () => clearTimeout(delayDebounce);
}, [searchTerm]);
useEffect(() => {
const fetchHostServers = async () => {
try {
const response = await axios.get<{ servers: Server[] }>(
"/api/servers/hosts"
);
setHostServers(response.data.servers);
} catch (error) {
console.error("Error fetching host servers:", error);
}
};
if (isAddDialogOpen || editId !== null) {
fetchHostServers();
}
}, [isAddDialogOpen, editId]);
return (
<SidebarProvider>
<AppSidebar />
@ -312,7 +365,7 @@ export default function Dashboard() {
</TooltipContent>
</Tooltip>
</TooltipProvider>
<AlertDialog>
<AlertDialog onOpenChange={setIsAddDialogOpen}>
<AlertDialogTrigger asChild>
<Button variant="outline" size="icon">
<Plus />
@ -326,6 +379,9 @@ export default function Dashboard() {
<TabsList className="w-full">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="hardware">Hardware</TabsTrigger>
<TabsTrigger value="virtualization">
Virtualization
</TabsTrigger>
</TabsList>
<TabsContent value="general">
<div className="space-y-4 pt-4">
@ -459,6 +515,47 @@ export default function Dashboard() {
</div>
</div>
</TabsContent>
<TabsContent value="virtualization">
<div className="space-y-4 pt-4">
<div className="flex items-center space-x-2">
<Checkbox
id="hostCheckbox"
checked={host}
onCheckedChange={(checked) =>
setHost(checked === true)
}
/>
<Label htmlFor="hostCheckbox">
Mark as host server
</Label>
</div>
{!host && (
<div className="grid w-full items-center gap-1.5">
<Label>Host Server</Label>
<Select
value={hostServer?.toString()}
onValueChange={(value) =>
setHostServer(Number(value))
}
>
<SelectTrigger>
<SelectValue placeholder="Select a host server" />
</SelectTrigger>
<SelectContent>
{hostServers.map((server) => (
<SelectItem
key={server.id}
value={server.id.toString()}
>
{server.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
</TabsContent>
</Tabs>
</AlertDialogDescription>
</AlertDialogHeader>
@ -487,7 +584,9 @@ export default function Dashboard() {
: "space-y-4"
}
>
{servers.map((server) => (
{servers
.filter((server) => server.hostServer === null)
.map((server) => (
<Card
key={server.id}
className={
@ -606,9 +705,26 @@ export default function Dashboard() {
<TabsTrigger value="hardware">
Hardware
</TabsTrigger>
<TabsTrigger value="virtualization">
Virtualization
</TabsTrigger>
</TabsList>
<TabsContent value="general">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editName">
Name
</Label>
<Input
id="editName"
type="text"
placeholder="e.g. Server1"
value={editName}
onChange={(e) =>
setEditName(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editOs">
Operating System
@ -667,7 +783,9 @@ export default function Dashboard() {
<TabsContent value="hardware">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editCpu">CPU</Label>
<Label htmlFor="editCpu">
CPU
</Label>
<Input
id="editCpu"
value={editCpu}
@ -677,7 +795,9 @@ export default function Dashboard() {
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editGpu">GPU</Label>
<Label htmlFor="editGpu">
GPU
</Label>
<Input
id="editGpu"
value={editGpu}
@ -687,7 +807,9 @@ export default function Dashboard() {
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editRam">RAM</Label>
<Label htmlFor="editRam">
RAM
</Label>
<Input
id="editRam"
value={editRam}
@ -710,15 +832,495 @@ export default function Dashboard() {
</div>
</div>
</TabsContent>
<TabsContent value="virtualization">
<div className="space-y-4 pt-4">
<div className="flex items-center space-x-2">
<Checkbox
id="editHostCheckbox"
checked={editHost}
onCheckedChange={(checked) =>
setEditHost(checked === true)
}
/>
<Label htmlFor="editHostCheckbox">
Mark as host server
</Label>
</div>
{!editHost && (
<div className="grid w-full items-center gap-1.5">
<Label>Host Server</Label>
<Select
value={editHostServer?.toString()}
onValueChange={(value) =>
setEditHostServer(
Number(value)
)
}
>
<SelectTrigger>
<SelectValue placeholder="Select a host server" />
</SelectTrigger>
<SelectContent>
{hostServers.map(
(server) => (
<SelectItem
key={server.id}
value={server.id.toString()}
>
{server.name}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
)}
</div>
</TabsContent>
</Tabs>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>
Cancel
</AlertDialogCancel>
<Button onClick={edit}>Save</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{server.hostedVMs.length > 0 && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
size="icon"
className="h-9 w-9"
>
<Server className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Hosted VMs
</AlertDialogTitle>
<AlertDialogDescription>
{server.host && (
<div className="mt-4">
<ScrollArea className="h-[500px] w-fzull rounded-md border p-4">
<div className="space-y-2 mt-2">
{server.hostedVMs?.map(
(hostedVM) => (
<div
key={hostedVM.id}
className="flex flex-col gap-2 border border-muted py-2 px-4 rounded-md"
>
<div className="flex items-center justify-between">
<div className="text-base font-md text-muted-foreground">
{hostedVM.name}
</div>
<div className="flex items-center gap-2 text-foreground/80">
<Button
variant="outline"
className="gap-2"
onClick={() =>
window.open(
hostedVM.url,
"_blank"
)
}
>
<Link className="h-4 w-4" />
</Button>
<Button
variant="destructive"
size="icon"
className="h-9 w-9"
onClick={() =>
deleteApplication(
hostedVM.id
)
}
>
<Trash2 className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger
asChild
>
<Button
size="icon"
className="h-9 w-9"
onClick={() =>
openEditDialog(
hostedVM
)
}
>
<Pencil className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Edit VM
</AlertDialogTitle>
<AlertDialogDescription>
<Tabs
defaultValue="general"
className="w-full"
>
<TabsList className="w-full">
<TabsTrigger value="general">
General
</TabsTrigger>
<TabsTrigger value="hardware">
Hardware
</TabsTrigger>
<TabsTrigger value="virtualization">
Virtualization
</TabsTrigger>
</TabsList>
<TabsContent value="general">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editName">
Name
</Label>
<Input
id="editName"
type="text"
placeholder="e.g. Server1"
value={
editName
}
onChange={(
e
) =>
setEditName(
e
.target
.value
)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editOs">
Operating
System
</Label>
<Select
value={
editOs
}
onValueChange={
setEditOs
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Windows">
Windows
</SelectItem>
<SelectItem value="Linux">
Linux
</SelectItem>
<SelectItem value="MacOS">
MacOS
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editIp">
IP
Adress
</Label>
<Input
id="editIp"
type="text"
placeholder="e.g. 192.168.100.2"
value={
editIp
}
onChange={(
e
) =>
setEditIp(
e
.target
.value
)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editUrl">
Management
URL
</Label>
<Input
id="editUrl"
type="text"
placeholder="e.g. https://proxmox.server1.com"
value={
editUrl
}
onChange={(
e
) =>
setEditUrl(
e
.target
.value
)
}
/>
</div>
</div>
</TabsContent>
<TabsContent value="hardware">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editCpu">
CPU
</Label>
<Input
id="editCpu"
value={
editCpu
}
onChange={(
e
) =>
setEditCpu(
e
.target
.value
)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editGpu">
GPU
</Label>
<Input
id="editGpu"
value={
editGpu
}
onChange={(
e
) =>
setEditGpu(
e
.target
.value
)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editRam">
RAM
</Label>
<Input
id="editRam"
value={
editRam
}
onChange={(
e
) =>
setEditRam(
e
.target
.value
)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editDisk">
Disk
</Label>
<Input
id="editDisk"
value={
editDisk
}
onChange={(
e
) =>
setEditDisk(
e
.target
.value
)
}
/>
</div>
</div>
</TabsContent>
<TabsContent value="virtualization">
<div className="space-y-4 pt-4">
<div className="flex items-center space-x-2">
<Checkbox
id="editHostCheckbox"
checked={
editHost
}
onCheckedChange={(
checked
) =>
setEditHost(
checked ===
true
)
}
/>
<Label htmlFor="editHostCheckbox">
Mark as
host
server
</Label>
</div>
{!editHost && (
<div className="grid w-full items-center gap-1.5">
<Label>
Host
Server
</Label>
<Select
value={editHostServer?.toString()}
onValueChange={(
value
) =>
setEditHostServer(
Number(
value
)
)
}
>
<SelectTrigger>
<SelectValue placeholder="Select a host server" />
</SelectTrigger>
<SelectContent>
{hostServers.map(
(
server
) => (
<SelectItem
key={
server.id
}
value={server.id.toString()}
>
{
server.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
)}
</div>
</TabsContent>
</Tabs>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
Cancel
</AlertDialogCancel>
<Button
onClick={edit}
>
Save
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
<div className="col-span-fullpb-2">
<Separator />
</div>
<div className="flex gap-5 pb-2">
<div className="flex items-center gap-2 text-foreground/80">
<MonitorCog className="h-4 w-4 text-muted-foreground" />
<span>
<b>OS:</b>{" "}
{hostedVM.os || "-"}
</span>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<FileDigit className="h-4 w-4 text-muted-foreground" />
<span>
<b>IP:</b>{" "}
{hostedVM.ip ||
"Not set"}
</span>
</div>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<Cpu className="h-4 w-4 text-muted-foreground" />
<span>
<b>CPU:</b>{" "}
{hostedVM.cpu || "-"}
</span>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<Microchip className="h-4 w-4 text-muted-foreground" />
<span>
<b>GPU:</b>{" "}
{hostedVM.gpu || "-"}
</span>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<MemoryStick className="h-4 w-4 text-muted-foreground" />
<span>
<b>RAM:</b>{" "}
{hostedVM.ram || "-"}
</span>
</div>
<div className="flex items-center gap-2 text-foreground/80">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<span>
<b>Disk:</b>{" "}
{hostedVM.disk || "-"}
</span>
</div>
</div>
)
)}
</div>
</ScrollArea>
</div>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
Close
</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
</div>
</div>
@ -762,7 +1364,9 @@ export default function Dashboard() {
<PaginationPrevious
onClick={handlePrevious}
isActive={currentPage > 1}
style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
style={{
cursor: currentPage === 1 ? "not-allowed" : "pointer",
}}
/>
</PaginationItem>
@ -774,7 +1378,10 @@ export default function Dashboard() {
<PaginationNext
onClick={handleNext}
isActive={currentPage < maxPage}
style={{ cursor: currentPage === maxPage ? 'not-allowed' : 'pointer' }}
style={{
cursor:
currentPage === maxPage ? "not-allowed" : "pointer",
}}
/>
</PaginationItem>
</PaginationContent>

46
components/ui/badge.tsx Normal file
View File

@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

47
package-lock.json generated
View File

@ -17,6 +17,7 @@
"@radix-ui/react-dialog": "^1.1.7",
"@radix-ui/react-dropdown-menu": "^2.1.7",
"@radix-ui/react-label": "^2.1.3",
"@radix-ui/react-scroll-area": "^1.2.4",
"@radix-ui/react-select": "^2.1.7",
"@radix-ui/react-separator": "^1.1.3",
"@radix-ui/react-slot": "^1.2.0",
@ -1720,6 +1721,37 @@
}
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.4.tgz",
"integrity": "sha512-G9rdWTQjOR4sk76HwSdROhPU0jZWpfozn9skU1v4N0/g9k7TmswrJn8W8WMU+aYktnLLpk5LX6fofj2bGe5NFQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-presence": "1.1.3",
"@radix-ui/react-primitive": "2.0.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.7.tgz",
@ -4577,21 +4609,6 @@
"optional": true
}
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz",
"integrity": "sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
}
}
}

View File

@ -18,6 +18,7 @@
"@radix-ui/react-dialog": "^1.1.7",
"@radix-ui/react-dropdown-menu": "^2.1.7",
"@radix-ui/react-label": "^2.1.3",
"@radix-ui/react-scroll-area": "^1.2.4",
"@radix-ui/react-select": "^2.1.7",
"@radix-ui/react-separator": "^1.1.3",
"@radix-ui/react-slot": "^1.2.0",