1
0
forked from baron/baron-sso

IdP 연동 관리 UI 및 라우팅 추가

This commit is contained in:
2026-01-28 17:48:24 +09:00
parent a52ec3b9f8
commit ec90853fe3
6 changed files with 215 additions and 66 deletions

View File

@@ -6,9 +6,9 @@ import AuditLogsPage from "../features/audit/AuditLogsPage";
import AuthPage from "../features/auth/AuthPage"; import AuthPage from "../features/auth/AuthPage";
import DashboardPage from "../features/dashboard/DashboardPage"; import DashboardPage from "../features/dashboard/DashboardPage";
import GlobalOverviewPage from "../features/overview/GlobalOverviewPage"; import GlobalOverviewPage from "../features/overview/GlobalOverviewPage";
import TenantCreatePage from "../features/tenants/TenantCreatePage"; import TenantCreatePage from "../features/tenants/routes/TenantCreatePage";
import TenantDetailPage from "../features/tenants/TenantDetailPage"; import TenantDetailPage from "../features/tenants/routes/TenantDetailPage";
import TenantListPage from "../features/tenants/TenantListPage"; import TenantListPage from "../features/tenants/routes/TenantListPage";
import UserCreatePage from "../features/users/UserCreatePage"; import UserCreatePage from "../features/users/UserCreatePage";
import UserDetailPage from "../features/users/UserDetailPage"; import UserDetailPage from "../features/users/UserDetailPage";
import UserListPage from "../features/users/UserListPage"; import UserListPage from "../features/users/UserListPage";

View File

@@ -3,19 +3,19 @@ import type { AxiosError } from "axios";
import { Building2, Sparkles } from "lucide-react"; import { Building2, Sparkles } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Badge } from "../../components/ui/badge"; import { Badge } from "../../../components/ui/badge";
import { Button } from "../../components/ui/button"; import { Button } from "../../../components/ui/button";
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "../../components/ui/card"; } from "../../../components/ui/card";
import { Input } from "../../components/ui/input"; import { Input } from "../../../components/ui/input";
import { Label } from "../../components/ui/label"; import { Label } from "../../../components/ui/label";
import { Textarea } from "../../components/ui/textarea"; import { Textarea } from "../../../components/ui/textarea";
import { createTenant } from "../../lib/adminApi"; import { createTenant } from "../../../lib/adminApi";
function TenantCreatePage() { function TenantCreatePage() {
const navigate = useNavigate(); const navigate = useNavigate();

View File

@@ -0,0 +1,71 @@
import { useQuery } from "@tanstack/react-query";
import { ArrowLeft } from "lucide-react";
import { Link, Outlet, useLocation, useParams } from "react-router-dom";
import { Badge } from "../../../components/ui/badge";
import { fetchTenant } from "../../../lib/adminApi";
function TenantDetailPage() {
const { tenantId } = useParams<{ tenantId: string }>();
const location = useLocation();
const tenantQuery = useQuery({
queryKey: ["tenant", tenantId],
queryFn: () => fetchTenant(tenantId!),
enabled: !!tenantId,
});
const isFederationTab = location.pathname.includes("/federation");
return (
<div className="space-y-8">
<header className="flex flex-wrap items-start justify-between gap-4">
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
<Link to="/tenants" className="inline-flex items-center gap-2">
<ArrowLeft size={14} />
Tenants
</Link>
<span>/</span>
<span className="text-foreground">Detail</span>
</div>
<h2 className="text-3xl font-semibold">
{tenantQuery.data?.name ?? "Loading Tenant..."}
</h2>
<p className="text-sm text-[var(--color-muted)]">
Edit tenant information or manage federation settings.
</p>
</div>
<Badge variant="muted">Admin only</Badge>
</header>
{/* Tabs */}
<div className="flex border-b">
<Link
to={`/tenants/${tenantId}`}
className={`px-4 py-2 text-sm font-medium ${
!isFederationTab
? "border-b-2 border-blue-500 text-blue-600"
: "text-gray-500 hover:text-gray-700"
}`}
>
Profile
</Link>
<Link
to={`/tenants/${tenantId}/federation`}
className={`px-4 py-2 text-sm font-medium ${
isFederationTab
? "border-b-2 border-blue-500 text-blue-600"
: "text-gray-500 hover:text-gray-700"
}`}
>
Federation
</Link>
</div>
{/* Outlet for nested routes */}
<Outlet />
</div>
);
}
export default TenantDetailPage;

View File

@@ -0,0 +1,94 @@
import { useParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { listIdpConfigsForTenant } from "../../../lib/adminApi";
export function TenantFederationPage() {
const { tenantId } = useParams<{ tenantId: string }>();
if (!tenantId) {
return <div>Tenant ID is missing</div>;
}
const { data, isLoading, error } = useQuery({
queryKey: ["idpConfigs", tenantId],
queryFn: () => listIdpConfigsForTenant(tenantId),
});
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">
Identity Federation Settings
</h1>
<p className="mb-4 text-gray-600">
Manage external identity providers for this tenant.
</p>
<div className="mb-4">
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
+ Add IdP Configuration
</button>
</div>
{isLoading && <div>Loading configurations...</div>}
{error && (
<div className="text-red-500">
Failed to load configurations: {error.message}
</div>
)}
{data && (
<div className="overflow-x-auto">
<table className="min-w-full bg-white">
<thead className="bg-gray-200">
<tr>
<th className="py-2 px-4 border-b">Display Name</th>
<th className="py-2 px-4 border-b">Provider Type</th>
<th className="py-2 px-4 border-b">Status</th>
<th className="py-2 px-4 border-b">Actions</th>
</tr>
</thead>
<tbody>
{data.length === 0 ? (
<tr>
<td colSpan={4} className="text-center py-4">
No IdP configurations found.
</td>
</tr>
) : (
data.map((config) => (
<tr key={config.id}>
<td className="py-2 px-4 border-b">
{config.display_name}
</td>
<td className="py-2 px-4 border-b">
{config.provider_type.toUpperCase()}
</td>
<td className="py-2 px-4 border-b">
<span
className={`px-2 py-1 text-xs font-semibold rounded-full ${
config.status === "active"
? "bg-green-200 text-green-800"
: "bg-gray-200 text-gray-800"
}`}
>
{config.status}
</span>
</td>
<td className="py-2 px-4 border-b">
<button className="text-blue-500 hover:underline mr-2">
Edit
</button>
<button className="text-red-500 hover:underline">
Delete
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
</div>
);
}

View File

@@ -2,15 +2,15 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios"; import type { AxiosError } from "axios";
import { Pencil, Plus, RefreshCw, Trash2 } from "lucide-react"; import { Pencil, Plus, RefreshCw, Trash2 } from "lucide-react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { Badge } from "../../components/ui/badge"; import { Badge } from "../../../components/ui/badge";
import { Button } from "../../components/ui/button"; import { Button } from "../../../components/ui/button";
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "../../components/ui/card"; } from "../../../components/ui/card";
import { import {
Table, Table,
TableBody, TableBody,
@@ -18,8 +18,8 @@ import {
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
} from "../../components/ui/table"; } from "../../../components/ui/table";
import { deleteTenant, fetchTenants } from "../../lib/adminApi"; import { deleteTenant, fetchTenants } from "../../../lib/adminApi";
function TenantListPage() { function TenantListPage() {
const navigate = useNavigate(); const navigate = useNavigate();

View File

@@ -1,31 +1,36 @@
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios"; import type { AxiosError } from "axios";
import { ArrowLeft, Save, Trash2 } from "lucide-react"; import { Save, Trash2 } from "lucide-react";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { Badge } from "../../components/ui/badge"; import { Button } from "../../../components/ui/button";
import { Button } from "../../components/ui/button";
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "../../components/ui/card"; } from "../../../components/ui/card";
import { Input } from "../../components/ui/input"; import { Input } from "../../../components/ui/input";
import { Label } from "../../components/ui/label"; import { Label } from "../../../components/ui/label";
import { Textarea } from "../../components/ui/textarea"; import { Textarea } from "../../../components/ui/textarea";
import { deleteTenant, fetchTenant, updateTenant } from "../../lib/adminApi"; import {
deleteTenant,
fetchTenant,
updateTenant,
} from "../../../lib/adminApi";
function TenantDetailPage() { export function TenantProfilePage() {
const { id } = useParams(); const { tenantId } = useParams<{ tenantId: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const tenantId = useMemo(() => id ?? "", [id]);
if (!tenantId) {
return <div>Tenant ID is missing</div>;
}
const tenantQuery = useQuery({ const tenantQuery = useQuery({
queryKey: ["tenant", tenantId], queryKey: ["tenant", tenantId],
queryFn: () => fetchTenant(tenantId), queryFn: () => fetchTenant(tenantId),
enabled: tenantId !== "",
}); });
const [name, setName] = useState(""); const [name, setName] = useState("");
@@ -34,13 +39,12 @@ function TenantDetailPage() {
const [status, setStatus] = useState("active"); const [status, setStatus] = useState("active");
useEffect(() => { useEffect(() => {
if (!tenantQuery.data) { if (tenantQuery.data) {
return; setName(tenantQuery.data.name);
setSlug(tenantQuery.data.slug);
setDescription(tenantQuery.data.description ?? "");
setStatus(tenantQuery.data.status);
} }
setName(tenantQuery.data.name);
setSlug(tenantQuery.data.slug);
setDescription(tenantQuery.data.description ?? "");
setStatus(tenantQuery.data.status);
}, [tenantQuery.data]); }, [tenantQuery.data]);
const updateMutation = useMutation({ const updateMutation = useMutation({
@@ -69,36 +73,19 @@ function TenantDetailPage() {
?.response?.data?.error; ?.response?.data?.error;
const handleDelete = () => { const handleDelete = () => {
if (!window.confirm("이 테넌트를 삭제할까요?")) { if (window.confirm("Are you sure you want to delete this tenant?")) {
return; deleteMutation.mutate();
} }
deleteMutation.mutate();
}; };
return ( return (
<div className="space-y-8"> <>
<header className="flex flex-wrap items-start justify-between gap-4"> <Card className="bg-[var(--color-panel)] mt-6">
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
<Link to="/tenants" className="inline-flex items-center gap-2">
<ArrowLeft size={14} />
Tenants
</Link>
<span>/</span>
<span className="text-foreground">Detail</span>
</div>
<h2 className="text-3xl font-semibold"> </h2>
<p className="text-sm text-[var(--color-muted)]">
.
</p>
</div>
<Badge variant="muted">Admin only</Badge>
</header>
<Card className="bg-[var(--color-panel)]">
<CardHeader> <CardHeader>
<CardTitle>Tenant profile</CardTitle> <CardTitle>Tenant profile</CardTitle>
<CardDescription>Slug와 .</CardDescription> <CardDescription>
Changes to slug and status are applied immediately.
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{loadError && ( {loadError && (
@@ -143,7 +130,6 @@ function TenantDetailPage() {
</Button> </Button>
</div> </div>
</div> </div>
{errorMsg && ( {errorMsg && (
<div className="rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive"> <div className="rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{errorMsg} {errorMsg}
@@ -152,18 +138,18 @@ function TenantDetailPage() {
</CardContent> </CardContent>
</Card> </Card>
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="mt-8 flex flex-wrap items-center justify-between gap-3">
<Button <Button
variant="outline" variant="outline"
onClick={handleDelete} onClick={handleDelete}
disabled={deleteMutation.isPending} disabled={deleteMutation.isPending}
> >
<Trash2 size={16} /> <Trash2 size={16} />
Delete
</Button> </Button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="outline" onClick={() => navigate("/tenants")}> <Button variant="outline" onClick={() => navigate("/tenants")}>
Cancel
</Button> </Button>
<Button <Button
onClick={() => updateMutation.mutate()} onClick={() => updateMutation.mutate()}
@@ -174,12 +160,10 @@ function TenantDetailPage() {
} }
> >
<Save size={16} /> <Save size={16} />
Save
</Button> </Button>
</div> </div>
</div> </div>
</div> </>
); );
} }
export default TenantDetailPage;