forked from baron/baron-sso
허용 테넌트 테이블로 전환
This commit is contained in:
@@ -26,9 +26,18 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../../components/ui/card";
|
||||
import { CopyButton } from "../../components/ui/copy-button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { Label } from "../../components/ui/label";
|
||||
import { Switch } from "../../components/ui/switch";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { toast } from "../../components/ui/use-toast";
|
||||
import type {
|
||||
@@ -55,7 +64,6 @@ import { cn } from "../../lib/utils";
|
||||
import { fetchMe, type UserProfile } from "../auth/authApi";
|
||||
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
|
||||
import { ClientDetailTabs } from "./ClientDetailTabs";
|
||||
import { AllowedTenantBadge } from "./components/AllowedTenantBadge";
|
||||
import { TenantAccessPicker } from "./components/TenantAccessPicker";
|
||||
import {
|
||||
claimDateTimeValueToInputString,
|
||||
@@ -2298,12 +2306,20 @@ function ClientGeneralPage() {
|
||||
"테넌트 접근 제한",
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.subtitle",
|
||||
"허용된 테넌트만 이 RP에 접근할 수 있도록 제한합니다.",
|
||||
)}
|
||||
</CardDescription>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p className="leading-relaxed">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.subtitle",
|
||||
"허용된 테넌트만 이 RP에 접근할 수 있도록 제한합니다.",
|
||||
)}
|
||||
</p>
|
||||
<p className="leading-relaxed">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.hint",
|
||||
"제한을 켜면 tenant 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 rounded-xl border border-border bg-muted/30 px-4 py-3">
|
||||
<div className="space-y-0.5 text-right">
|
||||
@@ -2334,81 +2350,168 @@ function ClientGeneralPage() {
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-5">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.hint",
|
||||
"제한을 켜면 tenant 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다.",
|
||||
)}
|
||||
</p>
|
||||
<CardContent className="space-y-3">
|
||||
{tenantAccessRestricted ? (
|
||||
<div className="grid gap-4 lg:grid-cols-[0.8fr_1.2fr]">
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.picker_label",
|
||||
"허용 테넌트 추가",
|
||||
)}{" "}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<TenantAccessPicker
|
||||
disabled={isGeneralSettingsReadOnly}
|
||||
selectedCount={allowedTenantIds.length}
|
||||
onSelectTenant={(selection) =>
|
||||
handleSelectAllowedTenant(selection.id)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-[1.2fr_0.8fr]">
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.picker_label",
|
||||
"허용 테넌트 추가",
|
||||
)}
|
||||
</Label>
|
||||
<TenantAccessPicker
|
||||
disabled={isGeneralSettingsReadOnly || !tenantAccessRestricted}
|
||||
selectedCount={allowedTenantIds.length}
|
||||
onSelectTenant={(selection) =>
|
||||
handleSelectAllowedTenant(selection.id)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.selected_title",
|
||||
"허용 테넌트",
|
||||
)}
|
||||
</Label>
|
||||
<div className="min-h-72 rounded-xl border border-border bg-muted/20 p-3">
|
||||
{tenantAccessRestricted && allowedTenantIds.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedAllowedTenants.map((tenant) => (
|
||||
<AllowedTenantBadge
|
||||
key={tenant.id}
|
||||
tenant={tenant}
|
||||
onRemove={() => toggleAllowedTenant(tenant.id)}
|
||||
disabled={isGeneralSettingsReadOnly}
|
||||
/>
|
||||
))}
|
||||
{allowedTenantIds
|
||||
.filter(
|
||||
(tenantId) =>
|
||||
!selectedAllowedTenants.some(
|
||||
(tenant) => tenant.id === tenantId,
|
||||
),
|
||||
)
|
||||
.map((tenantId) => (
|
||||
<AllowedTenantBadge
|
||||
key={tenantId}
|
||||
tenant={{ id: tenantId, name: tenantId }}
|
||||
onRemove={() => toggleAllowedTenant(tenantId)}
|
||||
disabled={isGeneralSettingsReadOnly}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full min-h-64 items-center justify-center text-sm text-muted-foreground">
|
||||
{tenantAccessRestricted
|
||||
? t(
|
||||
"ui.dev.clients.general.tenant_access.selected_empty",
|
||||
"아직 선택된 테넌트가 없습니다.",
|
||||
)
|
||||
: t(
|
||||
"ui.dev.clients.general.tenant_access.disabled",
|
||||
"제한 없음",
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.selected_title",
|
||||
"허용 테넌트",
|
||||
)}
|
||||
</Label>
|
||||
<div className="overflow-hidden rounded-md border border-border bg-background">
|
||||
{allowedTenantIds.length > 0 ? (
|
||||
<div className="max-h-80 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10 bg-muted/50">
|
||||
<TableRow>
|
||||
<TableHead className="w-[34%] px-4 py-3 text-left font-bold">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.table.name",
|
||||
"테넌트명",
|
||||
)}
|
||||
</TableHead>
|
||||
<TableHead className="w-[18%] px-4 py-3 text-left font-bold">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.table.slug",
|
||||
"슬러그",
|
||||
)}
|
||||
</TableHead>
|
||||
<TableHead className="px-4 py-3 text-left font-bold">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.table.id",
|
||||
"테넌트 ID",
|
||||
)}
|
||||
</TableHead>
|
||||
<TableHead className="w-[112px] px-4 py-3 text-right font-bold">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.table.actions",
|
||||
"작업",
|
||||
)}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{selectedAllowedTenants.map((tenant) => (
|
||||
<TableRow
|
||||
key={tenant.id}
|
||||
data-testid={`allowed-tenant-${tenant.id}`}
|
||||
>
|
||||
<TableCell className="px-4 py-3 font-medium">
|
||||
{tenant.name}
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-3 text-muted-foreground">
|
||||
{tenant.slug || "-"}
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-3 font-mono text-xs text-muted-foreground">
|
||||
<span className="break-all">{tenant.id}</span>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-3 text-right">
|
||||
<div className="inline-flex items-center gap-1.5">
|
||||
<CopyButton
|
||||
aria-label="테넌트 UUID 복사"
|
||||
className="h-8 w-8"
|
||||
data-testid={`allowed-tenant-copy-${tenant.id}`}
|
||||
size="icon"
|
||||
value={tenant.id}
|
||||
variant="ghost"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("ui.common.delete", "삭제")}
|
||||
onClick={() =>
|
||||
toggleAllowedTenant(tenant.id)
|
||||
}
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:text-destructive"
|
||||
data-testid={`allowed-tenant-remove-${tenant.id}`}
|
||||
disabled={isGeneralSettingsReadOnly}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{allowedTenantIds
|
||||
.filter(
|
||||
(tenantId) =>
|
||||
!selectedAllowedTenants.some(
|
||||
(tenant) => tenant.id === tenantId,
|
||||
),
|
||||
)
|
||||
.map((tenantId) => (
|
||||
<TableRow
|
||||
key={tenantId}
|
||||
data-testid={`allowed-tenant-${tenantId}`}
|
||||
>
|
||||
<TableCell className="px-4 py-3 font-medium">
|
||||
{tenantId}
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-3 text-muted-foreground">
|
||||
-
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-3 font-mono text-xs text-muted-foreground">
|
||||
<span className="break-all">{tenantId}</span>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-3 text-right">
|
||||
<div className="inline-flex items-center gap-1.5">
|
||||
<CopyButton
|
||||
aria-label="테넌트 UUID 복사"
|
||||
className="h-8 w-8"
|
||||
data-testid={`allowed-tenant-copy-${tenantId}`}
|
||||
size="icon"
|
||||
value={tenantId}
|
||||
variant="ghost"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("ui.common.delete", "삭제")}
|
||||
onClick={() =>
|
||||
toggleAllowedTenant(tenantId)
|
||||
}
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:text-destructive"
|
||||
data-testid={`allowed-tenant-remove-${tenantId}`}
|
||||
disabled={isGeneralSettingsReadOnly}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-[99px] items-center px-4 py-3 text-sm text-muted-foreground">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.selected_empty",
|
||||
"아직 선택된 테넌트가 없습니다.",
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ export function TenantAccessPicker({
|
||||
{selectedCount > 0
|
||||
? t(
|
||||
"msg.dev.clients.general.tenant_access.picker_hint_with_count",
|
||||
"선택기를 열어 허용 테넌트를 추가하세요. 현재 {{count}}개가 선택되어 있습니다.",
|
||||
"현재 {{count}}개가 선택되어 있습니다.",
|
||||
{ count: selectedCount },
|
||||
)
|
||||
: t(
|
||||
|
||||
Reference in New Issue
Block a user