1
0
forked from baron/baron-sso

클라이언트 관리 페이지 ui/ux 수정

This commit is contained in:
2026-01-29 17:05:16 +09:00
parent 3e2ceff692
commit 765bf67cab
5 changed files with 341 additions and 359 deletions

View File

@@ -1,10 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { AlertCircle, Copy, Eye, Link2, Shield, Workflow } from "lucide-react";
import { AlertCircle, Copy, Eye, Link2, Shield, Workflow, Save } from "lucide-react";
import { Link, useParams } from "react-router-dom";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import { Card, CardContent } from "../../components/ui/card";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../../components/ui/card";
import { Separator } from "../../components/ui/separator";
import {
Table,
@@ -12,17 +12,47 @@ import {
TableCell,
TableRow,
} from "../../components/ui/table";
import { fetchClient } from "../../lib/devApi";
import { Textarea } from "../../components/ui/textarea";
import { Label } from "../../components/ui/label";
import { fetchClient, updateClient } from "../../lib/devApi";
import { useState, useEffect } from "react";
function ClientDetailsPage() {
const params = useParams();
const queryClient = useQueryClient();
const clientId = params.id ?? "";
const { data, isLoading, error } = useQuery({
queryKey: ["client", clientId],
queryFn: () => fetchClient(clientId),
enabled: clientId.length > 0,
});
const [redirectUris, setRedirectUris] = useState("");
useEffect(() => {
if (data?.client?.redirectUris) {
setRedirectUris(data.client.redirectUris.join(", "));
}
}, [data]);
const mutation = useMutation({
mutationFn: () => {
const uriList = redirectUris
.split(",")
.map((u) => u.trim())
.filter(Boolean);
return updateClient(clientId, { redirectUris: uriList });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["client", clientId] });
alert("Redirect URIs가 저장되었습니다.");
},
onError: (err) => {
alert(`저장 실패: ${(err as Error).message}`);
},
});
if (!clientId) {
return <div className="p-8 text-center">Client ID가 .</div>;
}
@@ -81,7 +111,7 @@ function ClientDetailsPage() {
to={`/clients/${clientId}`}
className="border-b-2 border-primary pb-3 text-sm font-bold text-primary"
>
Overview
Connection
</Link>
<Link
to={`/clients/${clientId}/consents`}
@@ -104,118 +134,141 @@ function ClientDetailsPage() {
</div>
</div>
<div className="space-y-4">
<h2 className="text-xl font-bold"> </h2>
<Card className="glass-panel">
<CardContent className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-xs font-bold uppercase tracking-widest text-muted-foreground">
Client ID
</p>
<p className="font-mono text-lg">{data.client.id}</p>
</div>
<Button variant="secondary" className="gap-2">
<Copy className="h-4 w-4" />
ID
</Button>
</CardContent>
</Card>
<Card className="glass-panel">
<CardContent className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-xs font-bold uppercase tracking-widest text-muted-foreground">
Client Secret
</p>
<p className="font-mono text-lg tracking-widest">
</p>
</div>
<div className="flex flex-wrap gap-2">
<Button variant="secondary" className="gap-2">
<Eye className="h-4 w-4" />
</Button>
<Button variant="secondary" className="gap-2">
<Copy className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="gap-2 border-amber-500/50 text-amber-500"
>
<AlertCircle className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
</div>
<div className="space-y-4">
<div className="flex items-center gap-2">
<h2 className="text-xl font-bold">OIDC </h2>
<Badge variant="muted" className="gap-1">
<Link2 className="h-3 w-3" />
</Badge>
</div>
<Card className="glass-panel">
<Table>
<TableBody>
{endpoints.map((endpoint) => (
<TableRow key={endpoint.label} className="border-border/70">
<TableCell className="w-1/3">
<p className="text-xs font-bold uppercase tracking-[0.12em] text-muted-foreground">
{endpoint.label}
</p>
</TableCell>
<TableCell className="flex items-center justify-between gap-3">
<span className="break-all font-mono text-sm">
{endpoint.value}
</span>
<Button
variant="secondary"
size="icon"
className="h-8 w-8"
aria-label={`${endpoint.label} 복사`}
>
<div className="grid gap-8 lg:grid-cols-2">
<div className="space-y-6">
<div className="space-y-4">
<h2 className="text-xl font-bold"> </h2>
<Card className="glass-panel">
<CardContent className="flex flex-col gap-4 p-6">
<div>
<p className="text-xs font-bold uppercase tracking-widest text-muted-foreground">
Client ID
</p>
<div className="flex items-center justify-between gap-2">
<p className="font-mono text-lg truncate">{data.client.id}</p>
<Button variant="secondary" size="icon" className="shrink-0" onClick={() => navigator.clipboard.writeText(data.client.id)}>
<Copy className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
</div>
</div>
</div>
<div className="glass-panel p-6 opacity-80">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/15 text-primary">
<Shield className="h-6 w-6" />
</div>
<div>
<p className="text-lg font-semibold"> </p>
<p className="text-sm text-muted-foreground">
, /
.
</p>
</div>
<Separator />
<div>
<p className="text-xs font-bold uppercase tracking-widest text-muted-foreground">
Client Secret
</p>
<div className="flex items-center justify-between gap-2">
<p className="font-mono text-lg tracking-widest"></p>
<div className="flex gap-2 shrink-0">
<Button variant="secondary" size="icon">
<Eye className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" className="border-amber-500/50 text-amber-500">
<AlertCircle className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
<div className="hidden items-center gap-2 md:flex">
<Badge variant="outline" className="gap-1">
<Workflow className="h-4 w-4" />
</Badge>
<div className="space-y-4">
<div className="flex items-center gap-2">
<h2 className="text-xl font-bold">OIDC </h2>
<Badge variant="muted" className="gap-1">
<Link2 className="h-3 w-3" />
</Badge>
</div>
<Card className="glass-panel">
<Table>
<TableBody>
{endpoints.map((endpoint) => (
<TableRow key={endpoint.label} className="border-border/70">
<TableCell className="w-1/3">
<p className="text-xs font-bold uppercase tracking-[0.12em] text-muted-foreground">
{endpoint.label}
</p>
</TableCell>
<TableCell className="flex items-center justify-between gap-3">
<span className="break-all font-mono text-sm">
{endpoint.value}
</span>
<Button
variant="secondary"
size="icon"
className="h-8 w-8 shrink-0"
onClick={() => navigator.clipboard.writeText(endpoint.value)}
>
<Copy className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
</div>
</div>
<div className="space-y-6">
<div className="space-y-4">
<h2 className="text-xl font-bold"> URI </h2>
<Card className="glass-panel border-primary/20">
<CardHeader>
<CardTitle className="text-lg">Redirect URIs</CardTitle>
<CardDescription>
URL . (,) .
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="redirect-uris" className="text-sm font-semibold"> URL</Label>
<Textarea
id="redirect-uris"
placeholder="https://your-app.com/callback, http://localhost:3000/auth/callback"
rows={5}
value={redirectUris}
onChange={(e) => setRedirectUris(e.target.value)}
className="font-mono text-sm"
/>
</div>
<Button
className="w-full gap-2"
onClick={() => mutation.mutate()}
disabled={mutation.isPending}
>
<Save className="h-4 w-4" />
{mutation.isPending ? "저장 중..." : "Redirect URIs 저장"}
</Button>
</CardContent>
</Card>
</div>
<div className="glass-panel p-6 opacity-80">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/15 text-primary">
<Shield className="h-6 w-6" />
</div>
<div>
<p className="text-lg font-semibold"> </p>
<p className="text-sm text-muted-foreground">
, /
.
</p>
</div>
</div>
</div>
<Separator className="my-4" />
<p className="text-sm text-muted-foreground">
TTL ,
.
</p>
</div>
</div>
<Separator className="my-4" />
<p className="text-sm text-muted-foreground">
TTL ,
.
</p>
</div>
</div>
);