1
0
forked from baron/baron-sso
This commit is contained in:
2026-03-24 14:22:05 +09:00
parent b0e18cc724
commit a4f283e4e6
18 changed files with 197 additions and 93 deletions

View File

@@ -2,7 +2,6 @@ import { useMutation } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Download, FileText, Loader2, Upload } from "lucide-react";
import * as React from "react";
import { toast } from "../../../components/ui/use-toast";
import { Button } from "../../../components/ui/button";
import {
Dialog,
@@ -13,6 +12,7 @@ import {
DialogTitle,
DialogTrigger,
} from "../../../components/ui/dialog";
import { toast } from "../../../components/ui/use-toast";
import { importOrgChart } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n";

View File

@@ -12,7 +12,6 @@ import {
import { useState } from "react";
import { useAuth } from "react-oidc-context";
import { useParams } from "react-router-dom";
import { toast } from "../../../components/ui/use-toast";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
@@ -39,6 +38,7 @@ import {
TableHeader,
TableRow,
} from "../../../components/ui/table";
import { toast } from "../../../components/ui/use-toast";
import {
addTenantAdmin,
addTenantOwner,
@@ -291,25 +291,29 @@ export function TenantAdminsAndOwnersTab() {
{owner.email}
</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="icon"
className={`opacity-0 group-hover:opacity-100 transition-all ${
owner.id === currentUserId ||
currentOwners.length <= 1
? "opacity-50 cursor-not-allowed"
: "text-destructive hover:text-destructive hover:bg-destructive/10"
}`}
onClick={() =>
handleRemoveOwner(owner.id, owner.name)
}
disabled={
removeOwnerMutation.isPending ||
owner.id === currentUserId ||
currentOwners.length <= 1
}
title={
owner.id === currentUserId
<span className="relative inline-block group/tt">
<Button
variant="ghost"
size="icon"
className={`opacity-0 group-hover:opacity-100 transition-all ${
owner.id === currentUserId ||
currentOwners.length <= 1
? "opacity-50 cursor-not-allowed pointer-events-none"
: "text-destructive hover:text-destructive hover:bg-destructive/10"
}`}
onClick={() =>
handleRemoveOwner(owner.id, owner.name)
}
disabled={
removeOwnerMutation.isPending ||
owner.id === currentUserId ||
currentOwners.length <= 1
}
>
<Trash2 className="h-4 w-4" />
</Button>
<span className="pointer-events-none absolute bottom-full right-0 z-[100] mb-2 w-max rounded bg-foreground px-2 py-1 text-xs text-background opacity-0 shadow-lg transition-opacity group-hover/tt:opacity-100">
{owner.id === currentUserId
? t(
"msg.admin.tenants.owners.remove_self",
"본인의 권한은 회수할 수 없습니다.",
@@ -322,11 +326,9 @@ export function TenantAdminsAndOwnersTab() {
: t(
"ui.admin.tenants.owners.remove_title",
"소유자 권한 회수",
)
}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</span>
</span>
</TableCell>
</TableRow>
))
@@ -420,25 +422,29 @@ export function TenantAdminsAndOwnersTab() {
{admin.email}
</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="icon"
className={`opacity-0 group-hover:opacity-100 transition-all ${
admin.id === currentUserId ||
currentAdmins.length <= 1
? "opacity-50 cursor-not-allowed"
: "text-destructive hover:text-destructive hover:bg-destructive/10"
}`}
onClick={() =>
handleRemoveAdmin(admin.id, admin.name)
}
disabled={
removeAdminMutation.isPending ||
admin.id === currentUserId ||
currentAdmins.length <= 1
}
title={
admin.id === currentUserId
<span className="relative inline-block group/tt">
<Button
variant="ghost"
size="icon"
className={`opacity-0 group-hover:opacity-100 transition-all ${
admin.id === currentUserId ||
currentAdmins.length <= 1
? "opacity-50 cursor-not-allowed pointer-events-none"
: "text-destructive hover:text-destructive hover:bg-destructive/10"
}`}
onClick={() =>
handleRemoveAdmin(admin.id, admin.name)
}
disabled={
removeAdminMutation.isPending ||
admin.id === currentUserId ||
currentAdmins.length <= 1
}
>
<Trash2 className="h-4 w-4" />
</Button>
<span className="pointer-events-none absolute bottom-full right-0 z-[100] mb-2 w-max rounded bg-foreground px-2 py-1 text-xs text-background opacity-0 shadow-lg transition-opacity group-hover/tt:opacity-100">
{admin.id === currentUserId
? t(
"msg.admin.tenants.admins.remove_self",
"본인의 권한은 회수할 수 없습니다.",
@@ -451,11 +457,9 @@ export function TenantAdminsAndOwnersTab() {
: t(
"ui.admin.tenants.admins.remove_title",
"관리자 권한 회수",
)
}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</span>
</span>
</TableCell>
</TableRow>
))

View File

@@ -18,7 +18,6 @@ import {
import type React from "react";
import { useState } from "react";
import { useParams } from "react-router-dom";
import { toast } from "../../../components/ui/use-toast";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
@@ -38,6 +37,7 @@ import {
TableHeader,
TableRow,
} from "../../../components/ui/table";
import { toast } from "../../../components/ui/use-toast";
import {
type GroupSummary,
addGroupMember,

View File

@@ -3,7 +3,6 @@ import type { AxiosError } from "axios";
import { Save, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { toast } from "../../../components/ui/use-toast";
import { Button } from "../../../components/ui/button";
import {
Card,
@@ -15,10 +14,12 @@ import {
import { Input } from "../../../components/ui/input";
import { Label } from "../../../components/ui/label";
import { Textarea } from "../../../components/ui/textarea";
import { toast } from "../../../components/ui/use-toast";
import {
approveTenant,
deleteTenant,
fetchTenant,
fetchTenants,
updateTenant,
} from "../../../lib/adminApi";
import { t } from "../../../lib/i18n";
@@ -39,12 +40,21 @@ export function TenantProfilePage() {
queryFn: () => fetchTenant(tenantId),
});
const parentQuery = useQuery({
queryKey: ["tenants", "list-all"],
queryFn: () => fetchTenants(1000, 0),
});
const availableParents =
parentQuery.data?.items?.filter((t) => t.id !== tenantId) || [];
const [name, setName] = useState("");
const [type, setType] = useState("COMPANY");
const [slug, setSlug] = useState("");
const [description, setDescription] = useState("");
const [status, setStatus] = useState("active");
const [domains, setDomains] = useState("");
const [parentId, setParentId] = useState("");
useEffect(() => {
if (tenantQuery.data) {
@@ -54,6 +64,7 @@ export function TenantProfilePage() {
setDescription(tenantQuery.data.description ?? "");
setStatus(tenantQuery.data.status);
setDomains(tenantQuery.data.domains?.join(", ") ?? "");
setParentId(tenantQuery.data.parentId ?? "");
}
}, [tenantQuery.data]);
@@ -65,6 +76,7 @@ export function TenantProfilePage() {
slug,
description: description || undefined,
status,
parentId: parentId || undefined,
domains: domains
.split(",")
.map((d) => d.trim())
@@ -197,6 +209,31 @@ export function TenantProfilePage() {
</option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor="parentId" className="text-sm font-semibold">
{t("ui.admin.tenants.profile.form.parent", "상위 테넌트 (선택)")}
</Label>
<select
id="parentId"
name="parentId"
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
value={parentId}
onChange={(e) => setParentId(e.target.value)}
>
<option value="">{t("ui.common.none", "없음 (최상위)")}</option>
{availableParents.map((t) => (
<option key={t.id} value={t.id}>
{t.name} ({t.slug})
</option>
))}
</select>
<p className="text-xs text-muted-foreground mt-1">
{t(
"ui.admin.tenants.profile.form.parent_help",
"가족사 테넌트나 하위 조직을 종속시킬 경우 상위 테넌트를 선택해주세요.",
)}
</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">
{t("ui.admin.tenants.profile.slug", "슬러그 (Slug)")}

View File

@@ -3,7 +3,6 @@ import type { AxiosError } from "axios";
import { Plus, Save, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { toast } from "../../../components/ui/use-toast";
import { Button } from "../../../components/ui/button";
import {
Card,
@@ -14,6 +13,7 @@ import {
} from "../../../components/ui/card";
import { Input } from "../../../components/ui/input";
import { Label } from "../../../components/ui/label";
import { toast } from "../../../components/ui/use-toast";
import { fetchMe, fetchTenant, updateTenant } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n";

View File

@@ -35,7 +35,7 @@ function TenantUsersPage() {
// 해당 슬러그로 사용자 검색
const usersQuery = useQuery({
queryKey: ["users", { tenantSlug }],
queryFn: () => fetchUsers(100, 0, tenantSlug),
queryFn: () => fetchUsers(100, 0, undefined, tenantSlug),
enabled: !!tenantSlug,
});

View File

@@ -20,7 +20,6 @@ import {
import * as React from "react";
import { useMemo, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { toast } from "../../../components/ui/use-toast";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
@@ -55,6 +54,7 @@ import {
TabsList,
TabsTrigger,
} from "../../../components/ui/tabs";
import { toast } from "../../../components/ui/use-toast";
import {
type GroupSummary,
type TenantSummary,
@@ -775,7 +775,8 @@ const TenantTreeRow: React.FC<{
onClick={() => {
if (node.type === "USER_GROUP") {
// User groups have a different detail path
const baseTenantId = (node as any).tenantId || (node as any).parentId || "";
const baseTenantId =
(node as any).tenantId || (node as any).parentId || "";
navigate(`/tenants/${baseTenantId}/organization/${node.id}`);
} else {
navigate(`/tenants/${node.id}`);
@@ -866,6 +867,11 @@ function TenantUserGroupsTab() {
toast.success(t("msg.info.saved_success", "저장되었습니다."));
setIsAddDialogOpen(false);
},
onError: (error: AxiosError<{ error?: string }>) => {
toast.error(t("msg.common.error", "오류 발생"), {
description: error.response?.data?.error || error.message,
});
},
});
const allTenants = data?.items ?? [];

View File

@@ -3,7 +3,6 @@ import type { AxiosError } from "axios";
import { ArrowLeft, Shield, Trash2, UserPlus, Users } from "lucide-react";
import { useState } from "react";
import { Link, useParams } from "react-router-dom";
import { toast } from "../../../components/ui/use-toast";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
@@ -39,6 +38,7 @@ import {
TableHeader,
TableRow,
} from "../../../components/ui/table";
import { toast } from "../../../components/ui/use-toast";
import {
addGroupMember,
assignGroupRole,

View File

@@ -18,9 +18,9 @@ import {
type UserCreateRequest,
type UserCreateResponse,
createUser,
fetchMe,
fetchTenant,
fetchTenants,
fetchMe,
} from "../../lib/adminApi";
import { t } from "../../lib/i18n";

View File

@@ -19,7 +19,6 @@ import {
useForm,
} from "react-hook-form";
import { Link, useNavigate, useParams } from "react-router-dom";
import { toast } from "../../components/ui/use-toast";
import { Button } from "../../components/ui/button";
import {
Card,
@@ -30,6 +29,7 @@ import {
} from "../../components/ui/card";
import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import { toast } from "../../components/ui/use-toast";
import {
type TenantSummary,
type UserUpdateRequest,

View File

@@ -1,4 +1,3 @@
import { toast } from "../../components/ui/use-toast";
import { useMutation, useQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import {
@@ -42,6 +41,7 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import { toast } from "../../components/ui/use-toast";
import {
bulkDeleteUsers,
bulkUpdateUsers,
@@ -132,10 +132,7 @@ function UserListPage() {
};
const query = useQuery({
queryKey: [
"users",
{ limit, offset, search, tenantSlug: selectedCompany },
],
queryKey: ["users", { limit, offset, search, tenantSlug: selectedCompany }],
queryFn: () => fetchUsers(limit, offset, search, selectedCompany),
placeholderData: (previousData) => previousData,
});

View File

@@ -2,7 +2,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { AlertTriangle, FolderTree, Loader2, Search } from "lucide-react";
import * as React from "react";
import { toast } from "../../../components/ui/use-toast";
import { Button } from "../../../components/ui/button";
import {
Dialog,
@@ -15,6 +14,7 @@ import {
} from "../../../components/ui/dialog";
import { Input } from "../../../components/ui/input";
import { ScrollArea } from "../../../components/ui/scroll-area";
import { toast } from "../../../components/ui/use-toast";
import {
type GroupSummary,
type TenantSummary,