forked from baron/baron-sso
adminfront 및 백엔드: 세부 권한 변경 시 Keto 동기식 실시간 쓰기 및 프론트 일괄 갱신 적용하여 지연/롤백 버그 해결 완료
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
ShieldCheck,
|
||||
UserPlus,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useTenantPermission } from "../hooks/useTenantPermission";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
@@ -58,13 +58,45 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
// 🌟 테넌트 탭별 드롭다운 즉각 변경을 위한 임시 로컬 맵 선언
|
||||
const [localTenantPermissions, setLocalTenantPermissions] = useState<Record<string, Record<string, "none" | "read" | "write">>>({});
|
||||
|
||||
const relationsQuery = useQuery({
|
||||
queryKey: ["tenant-relations", tenantId],
|
||||
queryFn: () => fetchTenantRelations(tenantId),
|
||||
enabled: !!tenantId,
|
||||
});
|
||||
const relationsData = relationsQuery.data ?? [];
|
||||
|
||||
// 🌟 서버 데이터를 수신하면 로컬 변경 상태 맵을 실시간 동기화
|
||||
useEffect(() => {
|
||||
if (relationsQuery.data) {
|
||||
const initialMap: Record<string, Record<string, "none" | "read" | "write">> = {};
|
||||
for (const user of relationsQuery.data) {
|
||||
initialMap[user.userId] = {};
|
||||
const tabs = ["profile", "permissions", "organization", "schema"];
|
||||
for (const tab of tabs) {
|
||||
const isWrite = user.relations.includes(`${tab}_managers`);
|
||||
const isRead = user.relations.includes(`${tab}_viewers`);
|
||||
initialMap[user.userId][tab] = isWrite ? "write" : isRead ? "read" : "none";
|
||||
}
|
||||
}
|
||||
setLocalTenantPermissions(initialMap);
|
||||
}
|
||||
}, [relationsQuery.data]);
|
||||
const relations = relationsQuery.data ?? [];
|
||||
|
||||
const invalidateAllQueries = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tenant-relations", tenantId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["me"] });
|
||||
setTimeout(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tenant-relations", tenantId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["me"] });
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const addRelationMutation = useMutation({
|
||||
mutationFn: (payload: { userId: string; relation: string }) =>
|
||||
addTenantRelation(tenantId, payload.userId, payload.relation),
|
||||
@@ -96,10 +128,7 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
||||
toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다."));
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success(t("msg.admin.tenants.relations.add_success", "세부 권한이 추가되었습니다."));
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tenant-relations", tenantId] });
|
||||
// Quiet mutate
|
||||
},
|
||||
});
|
||||
|
||||
@@ -132,10 +161,7 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
||||
toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다."));
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success(t("msg.admin.tenants.relations.remove_success", "세부 권한이 회수되었습니다."));
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tenant-relations", tenantId] });
|
||||
// Quiet mutate
|
||||
},
|
||||
});
|
||||
|
||||
@@ -150,16 +176,25 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
||||
|
||||
if (currentVal === newVal) return;
|
||||
|
||||
if (currentVal === "read") {
|
||||
await removeRelationMutation.mutateAsync({ userId, relation: readRel });
|
||||
} else if (currentVal === "write") {
|
||||
await removeRelationMutation.mutateAsync({ userId, relation: writeRel });
|
||||
}
|
||||
try {
|
||||
if (currentVal === "read") {
|
||||
await removeRelationMutation.mutateAsync({ userId, relation: readRel });
|
||||
} else if (currentVal === "write") {
|
||||
await removeRelationMutation.mutateAsync({ userId, relation: writeRel });
|
||||
}
|
||||
|
||||
if (newVal === "read") {
|
||||
await addRelationMutation.mutateAsync({ userId, relation: readRel });
|
||||
} else if (newVal === "write") {
|
||||
await addRelationMutation.mutateAsync({ userId, relation: writeRel });
|
||||
if (newVal === "read") {
|
||||
await addRelationMutation.mutateAsync({ userId, relation: readRel });
|
||||
} else if (newVal === "write") {
|
||||
await addRelationMutation.mutateAsync({ userId, relation: writeRel });
|
||||
}
|
||||
|
||||
invalidateAllQueries();
|
||||
|
||||
// 🌟 Trigger a single consolidated success toast at the very end
|
||||
toast.success(t("msg.admin.tenants.relations.update_success", "세부 권한이 성공적으로 변경되었습니다."));
|
||||
} catch {
|
||||
// Individual mutations handle error toast via onError
|
||||
}
|
||||
};
|
||||
|
||||
@@ -170,6 +205,7 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
||||
for (const rel of userRelations) {
|
||||
await removeRelationMutation.mutateAsync({ userId, relation: rel });
|
||||
}
|
||||
invalidateAllQueries();
|
||||
};
|
||||
|
||||
const usersQuery = useQuery({
|
||||
@@ -179,7 +215,11 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
||||
});
|
||||
|
||||
const handleAddUser = (userId: string) => {
|
||||
addRelationMutation.mutate({ userId, relation: "profile_viewers" });
|
||||
addRelationMutation.mutate({ userId, relation: "profile_viewers" }, {
|
||||
onSettled: () => {
|
||||
invalidateAllQueries();
|
||||
}
|
||||
});
|
||||
setIsDialogOpen(false);
|
||||
setSearchTerm("");
|
||||
};
|
||||
@@ -259,6 +299,11 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
||||
? "read"
|
||||
: "none";
|
||||
|
||||
const curProfileVal = localTenantPermissions[user.userId]?.profile ?? profileVal;
|
||||
const curPermissionsVal = localTenantPermissions[user.userId]?.permissions ?? permissionsVal;
|
||||
const curOrganizationVal = localTenantPermissions[user.userId]?.organization ?? organizationVal;
|
||||
const curSchemaVal = localTenantPermissions[user.userId]?.schema ?? schemaVal;
|
||||
|
||||
return (
|
||||
<TableRow key={user.userId} className="hover:bg-muted/10 transition-colors">
|
||||
<TableCell className="font-medium">
|
||||
@@ -270,16 +315,25 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
||||
<TableCell>
|
||||
<select
|
||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={profileVal}
|
||||
value={curProfileVal}
|
||||
disabled={!isWritable}
|
||||
onChange={(e) =>
|
||||
name={`tenant-fine-grained-profile-${user.userId}`}
|
||||
onChange={(e) => {
|
||||
const nextVal = e.target.value as "none" | "read" | "write";
|
||||
setLocalTenantPermissions(prev => ({
|
||||
...prev,
|
||||
[user.userId]: {
|
||||
...(prev[user.userId] ?? {}),
|
||||
profile: nextVal
|
||||
}
|
||||
}));
|
||||
handleRelationChange(
|
||||
user.userId,
|
||||
"profile",
|
||||
profileVal,
|
||||
e.target.value as "none" | "read" | "write",
|
||||
)
|
||||
}
|
||||
nextVal,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
||||
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
||||
@@ -289,16 +343,25 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
||||
<TableCell>
|
||||
<select
|
||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={permissionsVal}
|
||||
value={curPermissionsVal}
|
||||
disabled={!isWritable}
|
||||
onChange={(e) =>
|
||||
name={`tenant-fine-grained-permissions-${user.userId}`}
|
||||
onChange={(e) => {
|
||||
const nextVal = e.target.value as "none" | "read" | "write";
|
||||
setLocalTenantPermissions(prev => ({
|
||||
...prev,
|
||||
[user.userId]: {
|
||||
...(prev[user.userId] ?? {}),
|
||||
permissions: nextVal
|
||||
}
|
||||
}));
|
||||
handleRelationChange(
|
||||
user.userId,
|
||||
"permissions",
|
||||
permissionsVal,
|
||||
e.target.value as "none" | "read" | "write",
|
||||
)
|
||||
}
|
||||
nextVal,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
||||
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
||||
@@ -308,16 +371,25 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
||||
<TableCell>
|
||||
<select
|
||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={organizationVal}
|
||||
value={curOrganizationVal}
|
||||
disabled={!isWritable}
|
||||
onChange={(e) =>
|
||||
name={`tenant-fine-grained-organization-${user.userId}`}
|
||||
onChange={(e) => {
|
||||
const nextVal = e.target.value as "none" | "read" | "write";
|
||||
setLocalTenantPermissions(prev => ({
|
||||
...prev,
|
||||
[user.userId]: {
|
||||
...(prev[user.userId] ?? {}),
|
||||
organization: nextVal
|
||||
}
|
||||
}));
|
||||
handleRelationChange(
|
||||
user.userId,
|
||||
"organization",
|
||||
organizationVal,
|
||||
e.target.value as "none" | "read" | "write",
|
||||
)
|
||||
}
|
||||
nextVal,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
||||
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
||||
@@ -327,16 +399,25 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
||||
<TableCell>
|
||||
<select
|
||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={schemaVal}
|
||||
value={curSchemaVal}
|
||||
disabled={!isWritable}
|
||||
onChange={(e) =>
|
||||
name={`tenant-fine-grained-schema-${user.userId}`}
|
||||
onChange={(e) => {
|
||||
const nextVal = e.target.value as "none" | "read" | "write";
|
||||
setLocalTenantPermissions(prev => ({
|
||||
...prev,
|
||||
[user.userId]: {
|
||||
...(prev[user.userId] ?? {}),
|
||||
schema: nextVal
|
||||
}
|
||||
}));
|
||||
handleRelationChange(
|
||||
user.userId,
|
||||
"schema",
|
||||
schemaVal,
|
||||
e.target.value as "none" | "read" | "write",
|
||||
)
|
||||
}
|
||||
nextVal,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
||||
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
||||
|
||||
Reference in New Issue
Block a user