1
0
forked from baron/baron-sso

테넌트 입력 자동완성형 변경

This commit is contained in:
2026-04-24 17:03:00 +09:00
parent d86c4111ad
commit 373751996a
4 changed files with 130 additions and 65 deletions

View File

@@ -2,14 +2,17 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import {
ArrowLeft,
Check,
ExternalLink,
Info,
Plus,
Save,
Search,
Shield,
Sparkles,
Trash2,
Upload,
X,
} from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
@@ -24,7 +27,6 @@ import {
} from "../../components/ui/card";
import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import { ScrollArea } from "../../components/ui/scroll-area";
import { Switch } from "../../components/ui/switch";
import { Textarea } from "../../components/ui/textarea";
import { toast } from "../../components/ui/use-toast";
@@ -153,6 +155,7 @@ function ClientGeneralPage() {
const [tenantAccessRestricted, setTenantAccessRestricted] = useState(false);
const [allowedTenantIds, setAllowedTenantIds] = useState<string[]>([]);
const [tenantSearch, setTenantSearch] = useState("");
const [isTenantSearchOpen, setIsTenantSearchOpen] = useState(false);
// Public Key Registration States
const [tokenEndpointAuthMethod, setTokenEndpointAuthMethod] =
@@ -357,6 +360,10 @@ function ClientGeneralPage() {
const handleTenantAccessToggle = (enabled: boolean) => {
setTenantAccessRestricted(enabled);
setIsTenantSearchOpen(enabled);
if (!enabled) {
setTenantSearch("");
}
setScopes((current) => normalizeScopesForTenantAccess(current, enabled));
};
@@ -368,6 +375,14 @@ function ClientGeneralPage() {
);
};
const handleSelectAllowedTenant = (tenantId: string) => {
setAllowedTenantIds((current) =>
current.includes(tenantId) ? current : [...current, tenantId],
);
setTenantSearch("");
setIsTenantSearchOpen(true);
};
const addScope = () => {
const newId = String(Date.now());
setScopes([
@@ -507,6 +522,12 @@ function ClientGeneralPage() {
const searchable = `${tenant.name} ${tenant.slug} ${tenant.description} ${tenant.type}`.toLowerCase();
return searchable.includes(normalizedTenantSearch);
});
const tenantSuggestions = filteredTenants
.filter((tenant) => !allowedTenantIds.includes(tenant.id))
.slice(0, 8);
const selectedAllowedTenants = allowedTenantIds
.map((tenantId) => tenantOptions.find((item) => item.id === tenantId))
.filter((tenant): tenant is TenantSummary => tenant != null);
const refreshHeadlessJwksCacheMutation = useMutation({
mutationFn: async () => {
@@ -1237,49 +1258,61 @@ function ClientGeneralPage() {
"테넌트 이름 또는 슬러그로 검색",
)}
</Label>
<Input
id="tenant-search"
value={tenantSearch}
onChange={(e) => setTenantSearch(e.target.value)}
placeholder={t(
"ui.dev.clients.general.tenant_access.search_placeholder",
"테넌트 이름 또는 슬러그로 검색",
)}
disabled={!tenantAccessRestricted}
/>
<ScrollArea className="h-72 rounded-xl border border-border bg-card">
<div className="divide-y divide-border">
{tenantAccessRestricted ? (
filteredTenants.length > 0 ? (
filteredTenants.map((tenant) => {
const checked = allowedTenantIds.includes(tenant.id);
return (
<label
key={tenant.id}
className="flex cursor-pointer items-center justify-between gap-4 px-4 py-3 transition hover:bg-muted/30"
>
<div className="min-w-0 space-y-1">
<div className="flex items-center gap-2">
<span className="truncate font-medium">
{tenant.name}
</span>
<Badge variant="outline" className="text-[11px]">
{tenant.slug}
</Badge>
</div>
<p className="text-xs text-muted-foreground">
{tenant.description || tenant.type}
</p>
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="tenant-search"
value={tenantSearch}
onFocus={() => {
if (tenantAccessRestricted) {
setIsTenantSearchOpen(true);
}
}}
onBlur={() => {
window.setTimeout(() => setIsTenantSearchOpen(false), 120);
}}
onChange={(e) => {
setTenantSearch(e.target.value);
if (tenantAccessRestricted) {
setIsTenantSearchOpen(true);
}
}}
placeholder={t(
"ui.dev.clients.general.tenant_access.search_placeholder",
"테넌트 이름 또는 슬러그로 검색",
)}
className="pl-10"
disabled={!tenantAccessRestricted}
/>
{tenantAccessRestricted && isTenantSearchOpen && (
<div className="absolute z-20 mt-2 max-h-72 w-full overflow-y-auto rounded-xl border border-border bg-background shadow-lg">
{tenantSuggestions.length > 0 ? (
tenantSuggestions.map((tenant) => (
<button
key={tenant.id}
type="button"
className="flex w-full items-start justify-between gap-3 border-b border-border/40 px-4 py-3 text-left transition hover:bg-muted/40 last:border-b-0"
onMouseDown={(event) => {
event.preventDefault();
handleSelectAllowedTenant(tenant.id);
}}
>
<div className="min-w-0 space-y-1">
<div className="flex items-center gap-2">
<span className="truncate font-medium">
{tenant.name}
</span>
<Badge variant="outline" className="text-[11px]">
{tenant.slug}
</Badge>
</div>
<input
type="checkbox"
checked={checked}
onChange={() => toggleAllowedTenant(tenant.id)}
className="h-4 w-4 rounded border-border text-primary focus:ring-primary"
/>
</label>
);
})
<p className="truncate text-xs text-muted-foreground">
{tenant.description || tenant.type}
</p>
</div>
<Plus className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
</button>
))
) : (
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
{t(
@@ -1287,17 +1320,21 @@ function ClientGeneralPage() {
"검색 결과가 없습니다.",
)}
</div>
)}
</div>
)}
</div>
<div className="rounded-xl border border-dashed border-border bg-muted/20 px-4 py-3 text-sm text-muted-foreground">
{tenantAccessRestricted
? t(
"ui.dev.clients.general.tenant_access.autocomplete_hint",
"테넌트 이름을 입력하면 자동 완성 후보가 나타납니다. 클릭하면 허용 목록에 추가됩니다.",
)
) : (
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
{t(
"ui.dev.clients.general.tenant_access.disabled",
"제한 없음",
)}
</div>
)}
</div>
</ScrollArea>
: t(
"ui.dev.clients.general.tenant_access.disabled",
"제한 없음",
)}
</div>
</div>
<div className="space-y-3">
@@ -1310,19 +1347,45 @@ function ClientGeneralPage() {
<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">
{allowedTenantIds.map((tenantId) => {
const tenant = tenantData?.items.find(
(item) => item.id === tenantId,
);
return (
{selectedAllowedTenants.map((tenant) => (
<Badge
key={tenant.id}
variant="secondary"
className="gap-2 px-3 py-1.5"
>
<Check className="h-3.5 w-3.5" />
<span className="max-w-44 truncate">{tenant.name}</span>
<span className="text-[11px] text-muted-foreground">
{tenant.slug}
</span>
<button
type="button"
aria-label={t(
"ui.common.delete",
"삭제",
)}
onClick={() => toggleAllowedTenant(tenant.id)}
className="text-muted-foreground transition hover:text-destructive"
>
<X className="h-3.5 w-3.5" />
</button>
</Badge>
))}
{allowedTenantIds
.filter(
(tenantId) =>
!selectedAllowedTenants.some(
(tenant) => tenant.id === tenantId,
),
)
.map((tenantId) => (
<Badge
key={tenantId}
variant="secondary"
className="gap-2 px-3 py-1.5"
>
<span className="max-w-44 truncate">
{tenant?.name || tenantId}
</span>
<Check className="h-3.5 w-3.5" />
<span className="max-w-44 truncate">{tenantId}</span>
<button
type="button"
aria-label={t(
@@ -1330,13 +1393,12 @@ function ClientGeneralPage() {
"삭제",
)}
onClick={() => toggleAllowedTenant(tenantId)}
className="text-xs font-semibold text-muted-foreground hover:text-destructive"
className="text-muted-foreground transition hover:text-destructive"
>
×
<X className="h-3.5 w-3.5" />
</button>
</Badge>
);
})}
))}
</div>
) : (
<div className="flex h-full min-h-64 items-center justify-center text-sm text-muted-foreground">

View File

@@ -1449,6 +1449,7 @@ selected_title = "Allowed tenants"
selected_empty = "No tenants selected yet."
empty = "No tenants match your search."
hint = "Turning this on adds the tenant scope automatically and requires at least one allowed tenant."
autocomplete_hint = "Type a tenant name to see autocomplete suggestions. Click one to add it to the allowed list."
validation_required = "Select at least one allowed tenant when tenant access restriction is enabled."
[ui.dev.clients.general.security]

View File

@@ -1448,6 +1448,7 @@ selected_title = "허용 테넌트"
selected_empty = "아직 선택된 테넌트가 없습니다."
empty = "검색 결과가 없습니다."
hint = "제한을 켜면 tenant 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다."
autocomplete_hint = "테넌트 이름을 입력하면 자동 완성 후보가 나타납니다. 클릭하면 허용 목록에 추가됩니다."
validation_required = "테넌트 접근 제한을 사용할 경우 허용 테넌트를 하나 이상 선택해야 합니다."
[ui.dev.clients.general.security]

View File

@@ -1531,6 +1531,7 @@ selected_title = ""
selected_empty = ""
empty = ""
hint = ""
autocomplete_hint = ""
validation_required = ""
[ui.dev.clients.general.security]