1
0
forked from baron/baron-sso

feat: apply sticky header and inner scroll pattern to all table pages

This commit is contained in:
2026-03-19 13:13:27 +09:00
parent 83991b13ca
commit f072d37362
9 changed files with 1007 additions and 944 deletions

View File

@@ -63,8 +63,8 @@ function ApiKeyListPage() {
};
return (
<div className="space-y-8">
<header className="flex flex-wrap items-start justify-between gap-4">
<div className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]">
<header className="flex flex-wrap items-start justify-between gap-4 flex-shrink-0">
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
<span>
@@ -103,8 +103,8 @@ function ApiKeyListPage() {
</div>
</header>
<Card className="bg-[var(--color-panel)]">
<CardHeader className="flex flex-row items-center justify-between">
<Card className="bg-[var(--color-panel)] flex-1 flex flex-col min-h-0 overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
<div>
<CardTitle>
{t("ui.admin.api_keys.list.registry.title", "API Key Registry")}
@@ -119,15 +119,17 @@ function ApiKeyListPage() {
</div>
<Badge variant="muted">{t("ui.common.badge.system", "System")}</Badge>
</CardHeader>
<CardContent>
<CardContent className="flex-1 flex flex-col min-h-0 pt-0">
{(errorMsg || fallbackError) && (
<div className="mb-4 rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{errorMsg ?? fallbackError}
</div>
)}
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
<div className="flex-1 overflow-auto relative custom-scrollbar">
<Table>
<TableHeader>
<TableHeader className="sticky top-0 bg-muted/90 backdrop-blur z-10 shadow-sm">
<TableRow>
<TableHead>
{t("ui.admin.api_keys.list.table.name", "NAME")}
@@ -168,7 +170,10 @@ function ApiKeyListPage() {
<TableRow key={key.id}>
<TableCell className="font-semibold">
<div className="flex items-center gap-2">
<Key size={14} className="text-[var(--color-muted)]" />
<Key
size={14}
className="text-[var(--color-muted)]"
/>
{key.name}
</div>
</TableCell>
@@ -208,6 +213,8 @@ function ApiKeyListPage() {
))}
</TableBody>
</Table>
</div>
</div>
</CardContent>
</Card>
</div>

View File

@@ -207,10 +207,11 @@ export function TenantAdminsAndOwnersTab() {
);
return (
<div className="space-y-8 mt-6">
<div className="space-y-8 mt-6 flex flex-col h-[calc(100vh-theme(spacing.32))]">
<div className="flex-1 flex flex-col lg:flex-row gap-8 min-h-0">
{/* Owners Card */}
<Card className="border-none shadow-sm bg-[var(--color-panel)]">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-7">
<Card className="flex-1 flex flex-col min-h-0 border-none shadow-sm bg-[var(--color-panel)]">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-7 flex-shrink-0">
<div className="space-y-1">
<CardTitle className="text-2xl font-bold flex items-center gap-2">
<Crown className="h-6 w-6 text-yellow-500" />
@@ -231,10 +232,11 @@ export function TenantAdminsAndOwnersTab() {
{t("ui.admin.tenants.owners.add_button", "소유자 추가")}
</Button>
</CardHeader>
<CardContent>
<div className="rounded-xl border border-border overflow-hidden">
<CardContent className="flex-1 flex flex-col min-h-0 pt-0">
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
<div className="flex-1 overflow-auto relative custom-scrollbar">
<Table>
<TableHeader className="bg-muted/30">
<TableHeader className="sticky top-0 bg-muted/90 backdrop-blur z-10 shadow-sm">
<TableRow>
<TableHead className="w-[250px] font-bold">
{t("ui.admin.tenants.owners.table_name", "이름")}
@@ -332,12 +334,13 @@ export function TenantAdminsAndOwnersTab() {
</TableBody>
</Table>
</div>
</div>
</CardContent>
</Card>
{/* Admins Card */}
<Card className="border-none shadow-sm bg-[var(--color-panel)]">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-7">
<Card className="flex-1 flex flex-col min-h-0 border-none shadow-sm bg-[var(--color-panel)]">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-7 flex-shrink-0">
<div className="space-y-1">
<CardTitle className="text-2xl font-bold flex items-center gap-2">
<ShieldCheck className="h-6 w-6 text-primary" />
@@ -358,10 +361,11 @@ export function TenantAdminsAndOwnersTab() {
{t("ui.admin.tenants.admins.add_button", "관리자 추가")}
</Button>
</CardHeader>
<CardContent>
<div className="rounded-xl border border-border overflow-hidden">
<CardContent className="flex-1 flex flex-col min-h-0 pt-0">
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
<div className="flex-1 overflow-auto relative custom-scrollbar">
<Table>
<TableHeader className="bg-muted/30">
<TableHeader className="sticky top-0 bg-muted/90 backdrop-blur z-10 shadow-sm">
<TableRow>
<TableHead className="w-[250px] font-bold">
{t("ui.admin.tenants.admins.table_name", "이름")}
@@ -459,8 +463,10 @@ export function TenantAdminsAndOwnersTab() {
</TableBody>
</Table>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Common Dialog for adding users */}
<Dialog

View File

@@ -343,11 +343,11 @@ function TenantGroupsPage() {
const currentGroup = groupsQuery.data?.find((g) => g.id === selectedGroupId);
return (
<div className="space-y-6 mt-6">
<div className="grid gap-6 md:grid-cols-3">
<div className="space-y-6 mt-6 flex flex-col h-[calc(100vh-theme(spacing.32))]">
<div className="grid gap-6 md:grid-cols-3 flex-1 min-h-0">
{/* 그룹 생성 폼 */}
<Card className="bg-[var(--color-panel)] md:col-span-1 border-primary/20">
<CardHeader>
<Card className="flex flex-col min-h-0 bg-[var(--color-panel)] md:col-span-1 border-primary/20">
<CardHeader className="flex-shrink-0">
<CardTitle className="text-sm flex items-center gap-2">
<Plus size={16} />{" "}
{t("ui.admin.groups.create.title", "새 그룹 생성")}
@@ -359,7 +359,7 @@ function TenantGroupsPage() {
)}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<CardContent className="space-y-4 flex-1 overflow-auto">
<div className="space-y-1">
<Label htmlFor="name">
{t("ui.admin.groups.form.name_label", "그룹 이름")}
@@ -431,8 +431,8 @@ function TenantGroupsPage() {
</Card>
{/* 그룹 목록 (트리 뷰) */}
<Card className="bg-[var(--color-panel)] md:col-span-2">
<CardHeader className="flex flex-row items-center justify-between">
<Card className="flex flex-col min-h-0 bg-[var(--color-panel)] md:col-span-2">
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
<div>
<CardTitle>
{t("ui.admin.groups.list.title", "User Groups")}
@@ -458,9 +458,11 @@ function TenantGroupsPage() {
</Button>
</div>
</CardHeader>
<CardContent>
<CardContent className="flex-1 flex flex-col min-h-0 pt-0">
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
<div className="flex-1 overflow-auto relative custom-scrollbar">
<Table>
<TableHeader>
<TableHeader className="sticky top-0 bg-muted/90 backdrop-blur z-10 shadow-sm">
<TableRow>
<TableHead>
{t("ui.admin.groups.table.name", "NAME")}
@@ -520,14 +522,16 @@ function TenantGroupsPage() {
))}
</TableBody>
</Table>
</div>
</div>
</CardContent>
</Card>
</div>
{/* 멤버 관리 섹션 (선택된 그룹이 있을 때) */}
{currentGroup && (
<Card className="bg-[var(--color-panel)] border-t-4 border-t-primary">
<CardHeader>
<Card className="flex flex-col min-h-0 flex-1 bg-[var(--color-panel)] border-t-4 border-t-primary">
<CardHeader className="flex-shrink-0">
<CardTitle className="flex items-center gap-2">
<Shield size={18} className="text-primary" />
{t("msg.admin.groups.members.title", "[{{name}}] 멤버 관리", {
@@ -541,8 +545,8 @@ function TenantGroupsPage() {
)}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex justify-end mb-4">
<CardContent className="flex-1 flex flex-col min-h-0 pt-0">
<div className="flex justify-end mb-4 flex-shrink-0">
<Button
size="sm"
onClick={() => handleAddMember(currentGroup.id)}
@@ -552,8 +556,10 @@ function TenantGroupsPage() {
{t("ui.common.add", "멤버 추가")}
</Button>
</div>
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
<div className="flex-1 overflow-auto relative custom-scrollbar">
<Table>
<TableHeader>
<TableHeader className="sticky top-0 bg-muted/90 backdrop-blur z-10 shadow-sm">
<TableRow>
<TableHead>
{t("ui.admin.groups.members.table.name", "이름")}
@@ -573,13 +579,18 @@ function TenantGroupsPage() {
colSpan={3}
className="text-center py-4 text-muted-foreground"
>
{t("msg.admin.groups.members.empty", "멤버가 없습니다.")}
{t(
"msg.admin.groups.members.empty",
"멤버가 없습니다.",
)}
</TableCell>
</TableRow>
)}
{currentGroup.members?.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell className="font-medium">
{user.name}
</TableCell>
<TableCell className="text-muted-foreground">
{user.email}
</TableCell>
@@ -602,6 +613,8 @@ function TenantGroupsPage() {
))}
</TableBody>
</Table>
</div>
</div>
</CardContent>
</Card>
)}

View File

@@ -116,8 +116,8 @@ function TenantListPage() {
};
return (
<div className="space-y-8">
<header className="flex flex-wrap items-start justify-between gap-4">
<div className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]">
<header className="flex flex-wrap items-start justify-between gap-4 flex-shrink-0">
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
<span>{t("ui.admin.tenants.breadcrumb.section", "Tenants")}</span>
@@ -156,8 +156,8 @@ function TenantListPage() {
</div>
</header>
<Card className="bg-[var(--color-panel)]">
<CardHeader className="flex flex-row items-center justify-between">
<Card className="bg-[var(--color-panel)] flex-1 flex flex-col min-h-0 overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
<div>
<CardTitle>
{t("ui.admin.tenants.registry.title", "Tenant Registry")}
@@ -172,15 +172,17 @@ function TenantListPage() {
{t("ui.common.badge.admin_only", "Admin only")}
</Badge>
</CardHeader>
<CardContent>
<CardContent className="flex-1 flex flex-col min-h-0 pt-0">
{(errorMsg || fallbackError) && (
<div className="mb-4 rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{errorMsg ?? fallbackError}
</div>
)}
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
<div className="flex-1 overflow-auto relative custom-scrollbar">
<Table>
<TableHeader>
<TableHeader className="sticky top-0 bg-muted/90 backdrop-blur z-10 shadow-sm">
<TableRow>
<TableHead>
{t("ui.admin.tenants.table.name", "NAME")}
@@ -228,9 +230,14 @@ function TenantListPage() {
)}
{tenants.map((tenant) => (
<TableRow key={tenant.id}>
<TableCell className="font-semibold">{tenant.name}</TableCell>
<TableCell className="font-semibold">
{tenant.name}
</TableCell>
<TableCell>
<Badge variant="outline" className="text-[10px] font-mono">
<Badge
variant="outline"
className="text-[10px] font-mono"
>
{t(
`domain.tenant_type.${tenant.type?.toLowerCase()}`,
tenant.type,
@@ -250,7 +257,10 @@ function TenantListPage() {
: "muted"
}
>
{t(`ui.common.status.${tenant.status}`, tenant.status)}
{t(
`ui.common.status.${tenant.status}`,
tenant.status,
)}
</Badge>
</TableCell>
<TableCell className="font-medium">
@@ -286,6 +296,8 @@ function TenantListPage() {
))}
</TableBody>
</Table>
</div>
</div>
</CardContent>
</Card>
</div>

View File

@@ -34,8 +34,8 @@ function TenantSubTenantsPage() {
const subTenants = data?.items ?? [];
return (
<Card className="mt-6 bg-[var(--color-panel)]">
<CardHeader className="flex flex-row items-center justify-between">
<Card className="mt-6 bg-[var(--color-panel)] flex-1 flex flex-col min-h-0 overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
<div>
<CardTitle className="flex items-center gap-2">
<Building2 size={18} className="text-primary" />
@@ -57,9 +57,11 @@ function TenantSubTenantsPage() {
</Link>
</Button>
</CardHeader>
<CardContent>
<CardContent className="flex-1 flex flex-col min-h-0 pt-0">
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
<div className="flex-1 overflow-auto relative custom-scrollbar">
<Table>
<TableHeader>
<TableHeader className="sticky top-0 bg-muted/90 backdrop-blur z-10 shadow-sm">
<TableRow>
<TableHead>
{t("ui.admin.tenants.sub.table.name", "NAME")}
@@ -82,13 +84,18 @@ function TenantSubTenantsPage() {
colSpan={4}
className="text-center py-8 text-muted-foreground"
>
{t("msg.admin.tenants.sub.empty", "하위 테넌트가 없습니다.")}
{t(
"msg.admin.tenants.sub.empty",
"하위 테넌트가 없습니다.",
)}
</TableCell>
</TableRow>
)}
{subTenants.map((tenant) => (
<TableRow key={tenant.id}>
<TableCell className="font-semibold">{tenant.name}</TableCell>
<TableCell className="font-semibold">
{tenant.name}
</TableCell>
<TableCell className="text-xs font-mono">
{tenant.slug}
</TableCell>
@@ -115,6 +122,8 @@ function TenantSubTenantsPage() {
))}
</TableBody>
</Table>
</div>
</div>
</CardContent>
</Card>
);

View File

@@ -42,8 +42,8 @@ function TenantUsersPage() {
const users = usersQuery.data?.items ?? [];
return (
<Card className="mt-6 bg-[var(--color-panel)]">
<CardHeader>
<Card className="mt-6 bg-[var(--color-panel)] flex-1 flex flex-col min-h-0 overflow-hidden">
<CardHeader className="flex-shrink-0">
<CardTitle className="flex items-center gap-2">
<User size={18} className="text-primary" />
{t("ui.admin.tenants.members.title", "Tenant Members ({{count}})", {
@@ -51,9 +51,11 @@ function TenantUsersPage() {
})}
</CardTitle>
</CardHeader>
<CardContent>
<CardContent className="flex-1 flex flex-col min-h-0 pt-0">
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
<div className="flex-1 overflow-auto relative custom-scrollbar">
<Table>
<TableHeader>
<TableHeader className="sticky top-0 bg-muted/90 backdrop-blur z-10 shadow-sm">
<TableRow>
<TableHead>
{t("ui.admin.tenants.members.table.name", "NAME")}
@@ -111,6 +113,8 @@ function TenantUsersPage() {
))}
</TableBody>
</Table>
</div>
</div>
</CardContent>
</Card>
);

View File

@@ -35,8 +35,8 @@ export default function GlobalUserGroupListPage() {
return <div className="p-8">Loading tenants and groups...</div>;
return (
<div className="space-y-8">
<header className="flex items-start justify-between">
<div className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]">
<header className="flex items-start justify-between flex-shrink-0">
<div className="space-y-1">
<h2 className="text-3xl font-bold tracking-tight">User Groups</h2>
<p className="text-muted-foreground">
@@ -46,7 +46,7 @@ export default function GlobalUserGroupListPage() {
</div>
</header>
<div className="grid gap-6">
<div className="grid gap-6 flex-1 overflow-auto p-1">
{tenantList?.items.map((tenant) => (
<TenantGroupCard key={tenant.id} tenant={tenant} />
))}
@@ -62,8 +62,8 @@ function TenantGroupCard({ tenant }: { tenant: TenantSummary }) {
});
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<Card className="flex flex-col min-h-0 bg-[var(--color-panel)] overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 flex-shrink-0">
<div className="space-y-1">
<CardTitle className="text-xl flex items-center gap-2">
<Building2 size={20} className="text-muted-foreground" />
@@ -83,9 +83,11 @@ function TenantGroupCard({ tenant }: { tenant: TenantSummary }) {
</Link>
</Button>
</CardHeader>
<CardContent>
<CardContent className="flex-1 flex flex-col min-h-0 pt-0">
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
<div className="flex-1 overflow-auto relative custom-scrollbar max-h-[400px]">
<Table>
<TableHeader>
<TableHeader className="sticky top-0 bg-muted/90 backdrop-blur z-10 shadow-sm">
<TableRow>
<TableHead className="w-[250px]"></TableHead>
<TableHead></TableHead>
@@ -139,6 +141,8 @@ function TenantGroupCard({ tenant }: { tenant: TenantSummary }) {
)}
</TableBody>
</Table>
</div>
</div>
</CardContent>
</Card>
);

View File

@@ -929,9 +929,9 @@ function TenantUserGroupsTab() {
const BaseIcon = getTenantIcon(currentBase.type);
return (
<div className="space-y-6 mt-6">
<Card className="bg-[var(--color-panel)] border-none shadow-sm overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between border-b bg-muted/5 py-4">
<div className="space-y-6 mt-6 flex flex-col h-[calc(100vh-theme(spacing.32))]">
<Card className="flex-1 flex flex-col min-h-0 bg-[var(--color-panel)] border-none shadow-sm overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between border-b bg-muted/5 py-4 flex-shrink-0">
<div className="space-y-1">
<CardTitle className="text-xl font-bold flex items-center gap-2">
<BaseIcon size={20} className="text-primary" />
@@ -1078,7 +1078,7 @@ function TenantUserGroupsTab() {
</Dialog>
</div>
</CardHeader>
<div className="px-6 py-3 bg-muted/5 border-b flex items-center gap-4">
<div className="px-6 py-3 bg-muted/5 border-b flex items-center gap-4 flex-shrink-0">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
@@ -1102,9 +1102,11 @@ function TenantUserGroupsTab() {
</Button>
)}
</div>
<CardContent className="p-0">
<CardContent className="flex-1 flex flex-col min-h-0 p-0">
<div className="flex-1 rounded-md border-0 overflow-hidden flex flex-col">
<div className="flex-1 overflow-auto relative custom-scrollbar">
<Table>
<TableHeader className="bg-muted/5">
<TableHeader className="sticky top-0 bg-muted/90 backdrop-blur z-10 shadow-sm">
<TableRow>
<TableHead className="pl-6 w-[40%]">
{t("ui.admin.tenants.table.name", "NAME")}
@@ -1135,6 +1137,8 @@ function TenantUserGroupsTab() {
/>
</TableBody>
</Table>
</div>
</div>
</CardContent>
</Card>
</div>

View File

@@ -211,8 +211,8 @@ export function UserGroupDetailPage() {
);
return (
<div className="space-y-8">
<header className="flex flex-wrap items-start justify-between gap-4">
<div className="space-y-8 flex flex-col h-[calc(100vh-theme(spacing.32))]">
<header className="flex flex-wrap items-start justify-between gap-4 flex-shrink-0">
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Link
@@ -260,10 +260,10 @@ export function UserGroupDetailPage() {
</div>
</header>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 flex-1 min-h-0">
{/* Members Management */}
<Card className="border-none shadow-sm bg-[var(--color-panel)]">
<CardHeader className="flex flex-row items-center justify-between">
<Card className="flex flex-col min-h-0 border-none shadow-sm bg-[var(--color-panel)] overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
<div>
<CardTitle>
{t("ui.admin.groups.detail.members_title", "구성원 관리")}
@@ -347,10 +347,11 @@ export function UserGroupDetailPage() {
</DialogContent>
</Dialog>
</CardHeader>
<CardContent>
<div className="rounded-md border border-border overflow-hidden">
<CardContent className="flex-1 flex flex-col min-h-0 pt-0">
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
<div className="flex-1 overflow-auto relative custom-scrollbar">
<Table>
<TableHeader className="bg-muted/30">
<TableHeader className="sticky top-0 bg-muted/90 backdrop-blur z-10 shadow-sm">
<TableRow>
<TableHead className="font-bold">
{t("ui.admin.users.list.table.name_email", "사용자")}
@@ -423,12 +424,13 @@ export function UserGroupDetailPage() {
</TableBody>
</Table>
</div>
</div>
</CardContent>
</Card>
{/* Roles/Permissions Management (Keto Based) */}
<Card className="border-none shadow-sm bg-[var(--color-panel)]">
<CardHeader className="flex flex-row items-center justify-between">
<Card className="flex flex-col min-h-0 border-none shadow-sm bg-[var(--color-panel)] overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
<div>
<CardTitle>
{t("ui.admin.groups.detail.permissions_title", "권한 관리")}
@@ -530,10 +532,11 @@ export function UserGroupDetailPage() {
</DialogContent>
</Dialog>
</CardHeader>
<CardContent>
<div className="rounded-md border border-border overflow-hidden">
<CardContent className="flex-1 flex flex-col min-h-0 pt-0">
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
<div className="flex-1 overflow-auto relative custom-scrollbar">
<Table>
<TableHeader className="bg-muted/30">
<TableHeader className="sticky top-0 bg-muted/90 backdrop-blur z-10 shadow-sm">
<TableRow>
<TableHead className="font-bold">
{t("ui.admin.users.detail.form.tenant", "대상 테넌트")}
@@ -611,6 +614,7 @@ export function UserGroupDetailPage() {
</TableBody>
</Table>
</div>
</div>
</CardContent>
</Card>
</div>