forked from baron/baron-sso
IdP 연동 관리 UI 및 라우팅 추가
This commit is contained in:
@@ -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";
|
||||||
|
|||||||
@@ -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();
|
||||||
71
adminfront/src/features/tenants/routes/TenantDetailPage.tsx
Normal file
71
adminfront/src/features/tenants/routes/TenantDetailPage.tsx
Normal 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;
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -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;
|
|
||||||
Reference in New Issue
Block a user