Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • main
1 result

Target

Select target project
  • aolda/proxy-manager-frontend
1 result
Select Git revision
  • main
1 result
Show changes
Commits on Source (3)
......@@ -32,6 +32,9 @@ importers:
'@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)
'@radix-ui/react-select':
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-separator':
specifier: ^1.1.2
version: 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)
......@@ -365,6 +368,9 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@radix-ui/number@1.1.0':
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
'@radix-ui/primitive@1.1.1':
resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
......@@ -634,6 +640,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-select@2.1.6':
resolution: {integrity: sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==}
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-separator@1.1.2':
resolution: {integrity: sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==}
peerDependencies:
......@@ -1939,6 +1958,8 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
'@radix-ui/number@1.1.0': {}
'@radix-ui/primitive@1.1.1': {}
'@radix-ui/react-alert-dialog@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)':
......@@ -2212,6 +2233,35 @@ snapshots:
'@types/react': 19.0.10
'@types/react-dom': 19.0.4(@types/react@19.0.10)
'@radix-ui/react-select@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)':
dependencies:
'@radix-ui/number': 1.1.0
'@radix-ui/primitive': 1.1.1
'@radix-ui/react-collection': 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-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-direction': 1.1.0(@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-focus-guards': 1.1.1(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-focus-scope': 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-id': 1.1.0(@types/react@19.0.10)(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-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-slot': 1.1.2(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-use-previous': 1.1.0(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-visually-hidden': 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)
aria-hidden: 1.2.4
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
react-remove-scroll: 2.6.3(@types/react@19.0.10)(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.10
'@types/react-dom': 19.0.4(@types/react@19.0.10)
'@radix-ui/react-separator@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)':
dependencies:
'@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)
......
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("px-2 py-1.5 text-sm font-medium", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 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 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}
import { useState, useEffect } from 'react';
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
export default useDebounce;
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect, useState } from 'react';
import { Link } from 'react-router';
import { useForm } from 'react-hook-form';
import { useForm, useWatch } from 'react-hook-form';
import { Check, X } from 'lucide-react';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { Separator } from '@/components/ui/separator';
import { toast } from 'sonner';
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Certificate } from '@/types/certificate';
import { useAuthStore } from '@/stores/authStore';
import useDebounce from '@/hooks/useDebounce';
const formSchema = z.object({
const formSchema = z
.object({
name: z.string({ required_error: '서버 이름을 입력해주세요' }).min(1, { message: '서버 이름을 입력해주세요' }),
domain: z
.string({ required_error: '도메인 주소를 입력해주세요' })
.regex(/^(?=.{1,253}$)(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/, {
message: '올바른 도메인 주소를 입력해주세요',
}),
instance_ip: z
ip: z
.string({ required_error: '인스턴스 IP를 입력해주세요' })
.regex(/^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/, {
message: '올바른 IP 주소를 입력해주세요',
})
.startsWith('10.16.', { message: '인스턴스 IP는 10.16.0.0/16 대역을 사용해야 합니다' }),
enable_ssl: z.boolean(),
enable_cache: z.boolean(),
port: z.coerce
.number({ invalid_type_error: '포트 번호를 입력해주세요' })
.min(1, { message: '포트 번호를 입력해주세요' })
.max(65535, { message: '올바른 포트 번호를 입력해주세요' }),
enableSSL: z.boolean(),
certificateId: z.coerce.number().optional(),
caching: z.boolean(),
})
.refine((data) => !data.enableSSL || (data.enableSSL && data.certificateId !== undefined), {
message: 'SSL 인증서를 선택해주세요',
path: ['certificateId'],
});
export default function RoutingCreate() {
const { authFetch, selectedProject } = useAuthStore();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
domain: '',
instance_ip: '',
enable_ssl: false,
enable_cache: false,
name: undefined,
domain: undefined,
ip: undefined,
port: undefined,
enableSSL: false,
certificateId: undefined,
caching: false,
},
});
const domain = useWatch({ control: form.control, name: 'domain' });
const enableSSL = useWatch({ control: form.control, name: 'enableSSL' });
const debouncedDomain = useDebounce(domain, 500);
const [certificates, setCertificates] = useState<Certificate[]>([]);
useEffect(() => {
if (enableSSL) {
authFetch(`/api/certificates?projectId=${selectedProject?.id}&domain=${debouncedDomain}`)
.then((response) => {
if (!response.ok) {
toast.error('SSL 인증서 목록을 불러올 수 없습니다');
return { contents: [] };
}
return response.json();
})
.then(({ contents }) => {
setCertificates(contents);
});
}
}, [authFetch, enableSSL, selectedProject, debouncedDomain]);
async function onSubmit(values: z.infer<typeof formSchema>) {
const { name, domain, ip, port, enableSSL, certificateId, caching } = values;
const response = await authFetch(`/api/routing?projectId=${selectedProject?.id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name,
domain,
ip,
port,
certificateId: enableSSL ? certificateId : -1,
caching,
}),
});
function onSubmit(values: z.infer<typeof formSchema>) {
console.log('제출된 데이터:', values);
toast.warning('라우팅 설정을 등록합니다');
if (!response.ok) {
console.error(response);
toast.error('라우팅 설정을 등록할 수 없습니다');
} else {
toast.success('라우팅 설정을 등록합니다');
}
}
return (
......@@ -91,7 +155,7 @@ export default function RoutingCreate() {
<FormField
control={form.control}
name="instance_ip"
name="ip"
render={({ field }) => (
<FormItem>
<FormLabel required>인스턴스 IP</FormLabel>
......@@ -103,6 +167,21 @@ export default function RoutingCreate() {
</FormItem>
)}
/>
<FormField
control={form.control}
name="port"
render={({ field }) => (
<FormItem>
<FormLabel required>인스턴스 포트</FormLabel>
<FormControl>
<Input type="number" placeholder="8080" {...field} />
</FormControl>
<FormDescription>연결할 웹 서비스의 포트 번호를 입력해주세요</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
......@@ -114,7 +193,7 @@ export default function RoutingCreate() {
<CardContent className="space-y-4">
<FormField
control={form.control}
name="enable_ssl"
name="enableSSL"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between">
<div className="space-y-0.5">
......@@ -134,11 +213,49 @@ export default function RoutingCreate() {
)}
/>
{form.watch('enableSSL') && (
<FormField
control={form.control}
name="certificateId"
render={({ field }) => (
<FormItem>
<Select onValueChange={field.onChange}>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="SSL 인증서 선택" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectGroup>
{certificates.length === 0 ? (
<SelectLabel className="text-muted-foreground">
사용 가능한 SSL 인증서가 없습니다.
</SelectLabel>
) : (
certificates.map((certificate) => (
<SelectItem value={certificate.id.toString()}>{certificate.domain}</SelectItem>
))
)}
</SelectGroup>
</SelectContent>
</Select>
{/* <FormDescription>
<Link to="/certificate/create" className="font-medium underline underline-offset-4">
SSL 인증서 등록
</Link>
을 먼저 진행해주세요
</FormDescription> */}
<FormMessage />
</FormItem>
)}
/>
)}
<Separator />
<FormField
control={form.control}
name="enable_cache"
name="caching"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between">
<div className="space-y-0.5">
......
import { useEffect, useState } from 'react';
import { Link } from 'react-router';
import { Filter, Plus, Check, X, Pencil, Trash } from 'lucide-react';
import { toast } from 'sonner';
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,
},
];
import { Skeleton } from '@/components/ui/skeleton';
import { useAuthStore } from '@/stores/authStore';
import { Routing } from '@/types/routing';
import {
AlertDialog,
AlertDialogContent,
AlertDialogAction,
AlertDialogCancel,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
} from '@/components/ui/alert-dialog';
export default function RoutingList() {
const { authFetch, selectedProject } = useAuthStore();
const [routings, setRoutings] = useState<Routing[] | null>(null);
const [selectedRouting, setSelectedRouting] = useState<Routing | null>(null);
useEffect(() => {
authFetch(`/api/routings?projectId=${selectedProject?.id}`)
.then((response) => {
if (!response.ok) {
toast.error('라우팅 정보를 조회할 수 없습니다.');
return { contents: [] };
}
return response.json();
})
.then(({ contents }) => {
setRoutings(contents);
});
}, [authFetch, selectedProject]);
const handleDelete = () => {
if (selectedRouting === null) throw Error('selectedRouting is null');
authFetch(`/api/routing?routingId=${selectedRouting.id}`, {
method: 'DELETE',
}).then((response) => {
if (!response.ok) {
console.error(response);
toast.error('라우팅 설정 삭제에 실패했습니다');
} else {
toast.warning('라우팅 설정이 삭제되었습니다');
setRoutings((prev) => prev!.filter((routing) => routing.id !== selectedRouting.id));
}
});
setSelectedRouting(null);
};
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>
{routings === null ? (
<Skeleton className="w-[24rem] h-[1rem] mt-2 rounded-full" />
) : (
<p className="mt-1 text-base text-gray-500">
<p className="mt-1 text-base text-gray-500">현재 {routings.length}개의 웹 프록시가 설정되어 있습니다.</p>
</p>
)}
</div>
<Button className="ml-2" asChild>
<Link to="./create">
......@@ -82,7 +97,32 @@ export default function RoutingList() {
</TableRow>
</TableHeader>
<TableBody>
{routings.map((routing) => (
{routings === null ? (
<>
<TableRow>
<TableCell colSpan={4}>
<Skeleton className="w-full h-[1rem] my-2 rounded-full" />
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={4}>
<Skeleton className="w-full h-[1rem] my-2 rounded-full" />
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={4}>
<Skeleton className="w-full h-[1rem] my-2 rounded-full" />
</TableCell>
</TableRow>
</>
) : routings.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="text-center text-muted-foreground">
현재 프로젝트에 등록된 웹 프록시 설정이 없습니다.
</TableCell>
</TableRow>
) : (
routings.map((routing) => (
<TableRow key={routing.id}>
<TableCell className="truncate max-w-48">
<HoverCard>
......@@ -92,20 +132,20 @@ export default function RoutingList() {
<div className="space-y-1">
<p className="text-sm font-semibold">{routing.name}</p>
<p className="text-sm">
{routing.domain} ({routing.instance_ip})
{routing.domain} ({routing.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>
<p className="text-xs text-muted-foreground mt-2">{routing.createdAt} 생성</p>
<p className="text-xs text-muted-foreground">{routing.updatedAt} 수정</p>
</div>
</div>
</HoverCardContent>
</HoverCard>
</TableCell>
<TableCell>{routing.domain}</TableCell>
<TableCell>{routing.instance_ip}</TableCell>
<TableCell>{routing.ip}</TableCell>
<TableCell>
<div className="flex justify-center items-center gap-1">
{routing.ssl_id !== null ? (
{routing.certificateId !== undefined ? (
<Badge variant="secondary">
<Check className="h-3 w-3" />
SSL
......@@ -116,7 +156,7 @@ export default function RoutingList() {
SSL
</Badge>
)}
{routing.is_cached ? (
{routing.caching ? (
<Badge variant="secondary">
<Check className="h-3 w-3" />
캐시
......@@ -136,19 +176,33 @@ export default function RoutingList() {
<Pencil />
</Link>
</Button>
<Button variant="secondary" className="size-8">
<Link to={`./delete/${routing.id}`}>
<Button variant="secondary" className="size-8" onClick={() => setSelectedRouting(routing)}>
<Trash />
</Link>
</Button>
</div>
</TableCell>
</TableRow>
))}
))
)}
</TableBody>
</Table>
</CardContent>
</Card>
<AlertDialog open={selectedRouting !== null} onOpenChange={(open) => open || setSelectedRouting(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>웹 라우팅 설정 삭제</AlertDialogTitle>
<AlertDialogDescription>
정말 '{selectedRouting?.name}' 라우팅 설정을 삭제하시겠습니까?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>취소</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete}>삭제</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
export interface Certificate {
id: number;
email: string;
domain: string;
createdAt: string;
updatedAt: string;
expiresAt: string;
}
export interface Routing {
id: number;
name: string;
domain: string;
ip: string;
port: number;
createdAt: string;
updatedAt: string;
certificateId: number | undefined;
caching: boolean;
}