forked from baron/baron-sso
adminfront 로그인 해결
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Building2, Plus, Users } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
@@ -18,8 +19,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../../components/ui/table";
|
||||
import { fetchTenants, fetchGroups } from "../../../lib/adminApi";
|
||||
import { useState } from "react";
|
||||
import { fetchGroups, fetchTenants } from "../../../lib/adminApi";
|
||||
|
||||
export default function GlobalUserGroupListPage() {
|
||||
const { data: tenantList, isLoading: isTenantsLoading } = useQuery({
|
||||
@@ -27,7 +27,8 @@ export default function GlobalUserGroupListPage() {
|
||||
queryFn: () => fetchTenants(100, 0),
|
||||
});
|
||||
|
||||
if (isTenantsLoading) return <div className="p-8">Loading tenants and groups...</div>;
|
||||
if (isTenantsLoading)
|
||||
return <div className="p-8">Loading tenants and groups...</div>;
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
@@ -35,7 +36,8 @@ export default function GlobalUserGroupListPage() {
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-3xl font-bold tracking-tight">User Groups</h2>
|
||||
<p className="text-muted-foreground">
|
||||
모든 테넌트의 유저 그룹을 관리합니다. 권한 상속의 주체가 되는 그룹을 설정하세요.
|
||||
모든 테넌트의 유저 그룹을 관리합니다. 권한 상속의 주체가 되는 그룹을
|
||||
설정하세요.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
@@ -62,7 +64,9 @@ function TenantGroupCard({ tenant }: { tenant: any }) {
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<Building2 size={20} className="text-muted-foreground" />
|
||||
{tenant.name}
|
||||
<Badge variant="outline" className="ml-2">{tenant.slug}</Badge>
|
||||
<Badge variant="outline" className="ml-2">
|
||||
{tenant.slug}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
이 테넌트에 정의된 유저 그룹 목록입니다.
|
||||
@@ -88,11 +92,16 @@ function TenantGroupCard({ tenant }: { tenant: any }) {
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center">Loading...</TableCell>
|
||||
<TableCell colSpan={4} className="text-center">
|
||||
Loading...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : groups?.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center text-muted-foreground py-4">
|
||||
<TableCell
|
||||
colSpan={4}
|
||||
className="text-center text-muted-foreground py-4"
|
||||
>
|
||||
등록된 유저 그룹이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -102,7 +111,10 @@ function TenantGroupCard({ tenant }: { tenant: any }) {
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users size={14} className="text-primary" />
|
||||
<Link to={`/tenants/${tenant.id}/user-groups/${group.id}`} className="hover:underline">
|
||||
<Link
|
||||
to={`/tenants/${tenant.id}/user-groups/${group.id}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{group.name}
|
||||
</Link>
|
||||
</div>
|
||||
@@ -111,7 +123,11 @@ function TenantGroupCard({ tenant }: { tenant: any }) {
|
||||
<TableCell>{group.members?.length || 0} 명</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link to={`/tenants/${tenant.id}/user-groups/${group.id}`}>상세보기</Link>
|
||||
<Link
|
||||
to={`/tenants/${tenant.id}/user-groups/${group.id}`}
|
||||
>
|
||||
상세보기
|
||||
</Link>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -61,7 +61,11 @@ export function UserGroupDetailPage() {
|
||||
const [selectedRelation, setSelectedRelation] = useState("view");
|
||||
|
||||
// Fetch specific group details
|
||||
const { data: currentGroup, isLoading: isGroupLoading, error } = useQuery({
|
||||
const {
|
||||
data: currentGroup,
|
||||
isLoading: isGroupLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ["user-group-detail", id],
|
||||
queryFn: () => fetchGroup(tenantId!, id!),
|
||||
enabled: !!id && !!tenantId,
|
||||
@@ -112,12 +116,7 @@ export function UserGroupDetailPage() {
|
||||
|
||||
const assignRoleMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
assignGroupRole(
|
||||
tenantId!,
|
||||
id!,
|
||||
selectedTargetTenantId,
|
||||
selectedRelation,
|
||||
),
|
||||
assignGroupRole(tenantId!, id!, selectedTargetTenantId, selectedRelation),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["user-group-roles", id] });
|
||||
setIsAddRoleOpen(false);
|
||||
@@ -137,29 +136,50 @@ export function UserGroupDetailPage() {
|
||||
},
|
||||
});
|
||||
|
||||
if (isGroupLoading) return (
|
||||
<div className="flex items-center justify-center p-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
<span className="ml-3 text-muted-foreground">Loading group details...</span>
|
||||
</div>
|
||||
);
|
||||
if (isGroupLoading)
|
||||
return (
|
||||
<div className="flex items-center justify-center p-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
<span className="ml-3 text-muted-foreground">
|
||||
Loading group details...
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (error || !currentGroup) return (
|
||||
<div className="p-8 text-center space-y-4">
|
||||
<h3 className="text-xl font-semibold text-destructive">Could not load group</h3>
|
||||
<div className="p-4 bg-red-50 text-red-700 rounded-md text-left text-sm font-mono overflow-auto max-w-xl mx-auto border border-red-100">
|
||||
<p>Error: {(error as any)?.response?.data?.error || (error as any)?.message || "Not found"}</p>
|
||||
<p className="mt-2 text-red-500 opacity-70">Path: /admin/tenants/{tenantId}/user-groups/{id}</p>
|
||||
if (error || !currentGroup)
|
||||
return (
|
||||
<div className="p-8 text-center space-y-4">
|
||||
<h3 className="text-xl font-semibold text-destructive">
|
||||
Could not load group
|
||||
</h3>
|
||||
<div className="p-4 bg-red-50 text-red-700 rounded-md text-left text-sm font-mono overflow-auto max-w-xl mx-auto border border-red-100">
|
||||
<p>
|
||||
Error:{" "}
|
||||
{(error as any)?.response?.data?.error ||
|
||||
(error as any)?.message ||
|
||||
"Not found"}
|
||||
</p>
|
||||
<p className="mt-2 text-red-500 opacity-70">
|
||||
Path: /admin/tenants/{tenantId}/user-groups/{id}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-muted-foreground pt-2">
|
||||
The group ID might be invalid or you don't have sufficient
|
||||
permissions.
|
||||
</p>
|
||||
<Button variant="outline" onClick={() => window.location.reload()}>
|
||||
Retry
|
||||
</Button>
|
||||
<div className="pt-4 border-t">
|
||||
<Link
|
||||
to={`/tenants/${tenantId}/user-groups`}
|
||||
className="text-primary hover:underline text-sm"
|
||||
>
|
||||
Return to Group List
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground pt-2">The group ID might be invalid or you don't have sufficient permissions.</p>
|
||||
<Button variant="outline" onClick={() => window.location.reload()}>Retry</Button>
|
||||
<div className="pt-4 border-t">
|
||||
<Link to={`/tenants/${tenantId}/user-groups`} className="text-primary hover:underline text-sm">
|
||||
Return to Group List
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
@@ -187,8 +207,8 @@ export function UserGroupDetailPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Badge variant="outline">User Group</Badge>
|
||||
<Badge variant="muted">Tenant: {tenantId?.split('-')[0]}...</Badge>
|
||||
<Badge variant="outline">User Group</Badge>
|
||||
<Badge variant="muted">Tenant: {tenantId?.split("-")[0]}...</Badge>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -217,15 +237,18 @@ export function UserGroupDetailPage() {
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Search User</Label>
|
||||
<Input
|
||||
placeholder="Search by email or name..."
|
||||
<Input
|
||||
placeholder="Search by email or name..."
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Select User</Label>
|
||||
<Select value={selectedUserId} onValueChange={setSelectedUserId}>
|
||||
<Select
|
||||
value={selectedUserId}
|
||||
onValueChange={setSelectedUserId}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Choose a user" />
|
||||
</SelectTrigger>
|
||||
@@ -240,10 +263,13 @@ export function UserGroupDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsAddMemberOpen(false)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsAddMemberOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
onClick={() => addMemberMutation.mutate(selectedUserId)}
|
||||
disabled={!selectedUserId || addMemberMutation.isPending}
|
||||
>
|
||||
@@ -264,7 +290,10 @@ export function UserGroupDetailPage() {
|
||||
<TableBody>
|
||||
{!currentGroup.members || currentGroup.members.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="text-center py-4 text-muted-foreground">
|
||||
<TableCell
|
||||
colSpan={2}
|
||||
className="text-center py-4 text-muted-foreground"
|
||||
>
|
||||
No members in this group.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -274,13 +303,15 @@ export function UserGroupDetailPage() {
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="font-medium">{member.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{member.email}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{member.email}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-destructive"
|
||||
onClick={() => removeMemberMutation.mutate(member.id)}
|
||||
>
|
||||
@@ -300,7 +331,9 @@ export function UserGroupDetailPage() {
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Permissions</CardTitle>
|
||||
<CardDescription>Tenant roles assigned to this group.</CardDescription>
|
||||
<CardDescription>
|
||||
Tenant roles assigned to this group.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Dialog open={isAddRoleOpen} onOpenChange={setIsAddRoleOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -313,13 +346,17 @@ export function UserGroupDetailPage() {
|
||||
<DialogHeader>
|
||||
<DialogTitle>Assign Tenant Role</DialogTitle>
|
||||
<DialogDescription>
|
||||
Members of this group will inherit this role on the target tenant.
|
||||
Members of this group will inherit this role on the target
|
||||
tenant.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Target Tenant</Label>
|
||||
<Select value={selectedTargetTenantId} onValueChange={setSelectedTargetTenantId}>
|
||||
<Select
|
||||
value={selectedTargetTenantId}
|
||||
onValueChange={setSelectedTargetTenantId}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select target tenant" />
|
||||
</SelectTrigger>
|
||||
@@ -334,25 +371,37 @@ export function UserGroupDetailPage() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Role (Relation)</Label>
|
||||
<Select value={selectedRelation} onValueChange={setSelectedRelation}>
|
||||
<Select
|
||||
value={selectedRelation}
|
||||
onValueChange={setSelectedRelation}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="view">View (Read-only)</SelectItem>
|
||||
<SelectItem value="manage">Manage (Read/Write)</SelectItem>
|
||||
<SelectItem value="admins">Admin (Full Control)</SelectItem>
|
||||
<SelectItem value="manage">
|
||||
Manage (Read/Write)
|
||||
</SelectItem>
|
||||
<SelectItem value="admins">
|
||||
Admin (Full Control)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsAddRoleOpen(false)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsAddRoleOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
onClick={() => assignRoleMutation.mutate()}
|
||||
disabled={!selectedTargetTenantId || assignRoleMutation.isPending}
|
||||
disabled={
|
||||
!selectedTargetTenantId || assignRoleMutation.isPending
|
||||
}
|
||||
>
|
||||
Assign
|
||||
</Button>
|
||||
@@ -371,10 +420,17 @@ export function UserGroupDetailPage() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isRolesLoading ? (
|
||||
<TableRow><TableCell colSpan={3} className="text-center">Loading...</TableCell></TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-center">
|
||||
Loading...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : !groupRoles || groupRoles.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-center py-4 text-muted-foreground">
|
||||
<TableCell
|
||||
colSpan={3}
|
||||
className="text-center py-4 text-muted-foreground"
|
||||
>
|
||||
No roles assigned.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -382,17 +438,26 @@ export function UserGroupDetailPage() {
|
||||
groupRoles.map((role, idx) => (
|
||||
<TableRow key={`${role.tenantId}-${role.relation}-${idx}`}>
|
||||
<TableCell>
|
||||
<div className="font-medium">{role.tenantName || role.tenantId}</div>
|
||||
<div className="font-medium">
|
||||
{role.tenantName || role.tenantId}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="capitalize">{role.relation}</Badge>
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{role.relation}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-destructive"
|
||||
onClick={() => removeRoleMutation.mutate({ targetTenantId: role.tenantId, relation: role.relation })}
|
||||
onClick={() =>
|
||||
removeRoleMutation.mutate({
|
||||
targetTenantId: role.tenantId,
|
||||
relation: role.relation,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user