diff --git a/package.json b/package.json index f097ac4d864f9284e1fed0df74b8b29c84042335..0d9d741f38a6b8ea8e735a2c616be17f112f0259 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-hover-card": "^1.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb572731c8c633724351ec44a7756804e08e587c..d9732eb25ebd56329766a2bfa721ab08fcbf762b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@radix-ui/react-dropdown-menu': specifier: ^2.1.6 version: 2.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-hover-card': + specifier: ^1.1.6 + version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-label': specifier: ^2.1.2 version: 2.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -485,6 +488,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-hover-card@1.1.6': + resolution: {integrity: sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==} + 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 + '@radix-ui/react-id@1.1.0': resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} peerDependencies: @@ -1987,6 +2003,23 @@ snapshots: '@types/react': 19.0.10 '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-hover-card@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-id@1.1.0(@types/react@19.0.10)(react@19.0.0)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.10)(react@19.0.0) diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index 765d57c588a8b973c43ee75713f79c4fc6b2ad42..2af12b1a00d7454c599aaf6c7eeced9943570e60 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -25,13 +25,13 @@ const data = { { icon: ArrowLeftRight, title: '라우팅 설정', - url: '#', + url: 'routing', isActive: true, }, { icon: ShieldCheck, title: 'SSL 인증서', - url: '#', + url: 'certificate', isActive: false, }, ], @@ -42,7 +42,7 @@ const data = { { icon: Server, title: 'SSH 설정', - url: '#', + url: 'forwarding', isActive: false, }, ], @@ -53,7 +53,7 @@ const data = { { icon: TextSearch, title: '설정 변경 내역', - url: '#', + url: 'log', isActive: false, }, ], diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000000000000000000000000000000000000..37e088abbf9e52776ff0ae807ddebd66d2200022 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -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", + 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 } diff --git a/src/components/ui/hover-card.tsx b/src/components/ui/hover-card.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fa8935d838351ce16d128ef9e63801e2e427dcc3 --- /dev/null +++ b/src/components/ui/hover-card.tsx @@ -0,0 +1,40 @@ +import * as React from "react" +import * as HoverCardPrimitive from "@radix-ui/react-hover-card" + +import { cn } from "@/lib/utils" + +function HoverCard({ + ...props +}: React.ComponentProps<typeof HoverCardPrimitive.Root>) { + return <HoverCardPrimitive.Root data-slot="hover-card" {...props} /> +} + +function HoverCardTrigger({ + ...props +}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) { + return ( + <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} /> + ) +} + +function HoverCardContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps<typeof HoverCardPrimitive.Content>) { + return ( + <HoverCardPrimitive.Content + data-slot="hover-card-content" + align={align} + sideOffset={sideOffset} + 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 w-64 rounded-md border p-4 shadow-md outline-hidden", + className + )} + {...props} + /> + ) +} + +export { HoverCard, HoverCardTrigger, HoverCardContent } diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index f186395cd28e0e54b7156a1b58693b1cc5b6ce68..927f3658296df35b49a32453f7ec68d7a5bf7ebf 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -33,7 +33,7 @@ const logs = [ export default function Home() { return ( <div className="flex flex-1 flex-col gap-4 p-6"> - <h1 className="scroll-m-20 text-3xl font-semibold first:mt-0">Aolda Proxy Manager</h1> + <h1 className="scroll-m-20 text-3xl font-semibold first:mt-0 mb-2">Aolda Proxy Manager</h1> <div className="grid auto-rows-min gap-4 md:grid-cols-3"> <Card> <CardHeader> diff --git a/src/pages/routing/List.tsx b/src/pages/routing/List.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4b19ab265fa5f5feb1f48547d20e43d004646329 --- /dev/null +++ b/src/pages/routing/List.tsx @@ -0,0 +1,152 @@ +import { Filter, Plus, Check, X, Pencil, Trash } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { Link } from 'react-router'; + +const routings = [ + { + id: 1, + name: '아올다 프록시 매니저 콘솔', + created_at: '2021-09-01 11:43:00', + updated_at: '2021-09-01 12:00:00', + domain: 'console.ajou.app', + instance_ip: '10.16.0.10', + ssl_id: 10921, + is_cached: false, + }, + { + id: 2, + name: '아올다 블로그', + created_at: '2021-09-01 12:00:00', + updated_at: '2021-09-01 12:01:00', + domain: 'blog.ajou.app', + instance_ip: '10.16.0.11', + ssl_id: 10921, + is_cached: true, + }, + { + id: 3, + name: '개인 블로그', + created_at: '2021-09-01 12:01:00', + updated_at: '2021-09-01 13:00:00', + domain: 'blog.username.blog', + instance_ip: '10.16.3.23', + ssl_id: 10923, + is_cached: true, + }, + { + id: 4, + name: '아올다 테스트 서버', + created_at: '2021-09-01 13:00:00', + updated_at: '2021-09-02 12:00:00', + domain: 'test.aolda.app', + instance_ip: '10.16.32.1', + ssl_id: null, + is_cached: false, + }, +]; + +export default function RoutingList() { + return ( + <div className="flex flex-1 flex-col gap-4 p-6"> + <div className="flex justify-between mb-2"> + <div> + <h1 className="scroll-m-20 text-3xl font-semibold first:mt-0">웹 라우팅 설정</h1> + <p className="mt-1 text-base text-gray-500">현재 3개의 웹 프록시가 설정되어 있습니다.</p> + </div> + <Button className="ml-2"> + <Plus className="h-4 w-4" /> 새 프록시 추가 + </Button> + </div> + <Card> + <CardContent> + <div className="flex w-full items-center space-x-2 mb-4"> + <Filter className="mr-3" /> + <Input placeholder="이름, 도메인, 인스턴스 IP로 검색..." /> + <Button variant="secondary">검색</Button> + </div> + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-48">이름</TableHead> + <TableHead>도메인</TableHead> + <TableHead>인스턴스 IP</TableHead> + <TableHead className="w-32 min-w-32 text-center">기타 설정</TableHead> + <TableHead className="w-32 min-w-32 text-center">작업</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {routings.map((routing) => ( + <TableRow key={routing.id}> + <TableCell className="truncate max-w-48"> + <HoverCard> + <HoverCardTrigger>{routing.name}</HoverCardTrigger> + <HoverCardContent className="w-80 whitespace-normal"> + <div className="flex justify-between space-x-4"> + <div className="space-y-1"> + <p className="text-sm font-semibold">{routing.name}</p> + <p className="text-sm"> + {routing.domain} ({routing.instance_ip}) + </p> + <p className="text-xs text-muted-foreground mt-2">{routing.created_at} 생성</p> + <p className="text-xs text-muted-foreground">{routing.updated_at} 수정</p> + </div> + </div> + </HoverCardContent> + </HoverCard> + </TableCell> + <TableCell>{routing.domain}</TableCell> + <TableCell>{routing.instance_ip}</TableCell> + <TableCell> + <div className="flex justify-center items-center gap-1"> + {routing.ssl_id !== null ? ( + <Badge variant="secondary"> + <Check className="h-3 w-3" /> + SSL + </Badge> + ) : ( + <Badge variant="outline" className="text-gray-500"> + <X className="h-3 w-3" /> + SSL + </Badge> + )} + {routing.is_cached ? ( + <Badge variant="secondary"> + <Check className="h-3 w-3" /> + 캐시 + </Badge> + ) : ( + <Badge variant="outline" className="text-gray-500"> + <X className="h-3 w-3" /> + 캐시 + </Badge> + )} + </div> + </TableCell> + <TableCell> + <div className="flex justify-center items-center gap-2"> + <Button variant="secondary" className="size-8"> + <Link to={`./edit/${routing.id}`}> + <Pencil /> + </Link> + </Button> + <Button variant="secondary" className="size-8"> + <Link to={`./delete/${routing.id}`}> + <Trash /> + </Link> + </Button> + </div> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </CardContent> + </Card> + </div> + ); +} diff --git a/src/routes.tsx b/src/routes.tsx index 91344997ec71ec9db0c8d244d11f7335d3af5c97..a8014cf7bb87f56bad0809b85ed686ccd2e806fd 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -2,6 +2,7 @@ import { Routes, Route } from 'react-router'; import Root from '@/pages/Root'; import Home from '@/pages/Home'; import Login from '@/pages/Login'; +import RoutingList from '@/pages/routing/List'; export default function AppRoutes() { return ( @@ -9,6 +10,9 @@ export default function AppRoutes() { <Route path="/" element={<Root />}> <Route index element={<Home />} /> <Route path="login" element={<Login />} /> + <Route path="routing"> + <Route index element={<RoutingList />} /> + </Route> </Route> </Routes> );