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