From 373751996a2c943dbd4fb3225d0018e3cc750309 Mon Sep 17 00:00:00 2001 From: kyy Date: Fri, 24 Apr 2026 17:03:00 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=8C=EB=84=8C=ED=8A=B8=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EC=9E=90=EB=8F=99=EC=99=84=EC=84=B1=ED=98=95=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/clients/ClientGeneralPage.tsx | 192 ++++++++++++------ devfront/src/locales/en.toml | 1 + devfront/src/locales/ko.toml | 1 + devfront/src/locales/template.toml | 1 + 4 files changed, 130 insertions(+), 65 deletions(-) diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index 72f36f28..b20cfb80 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -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([]); 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() { "테넌트 이름 또는 슬러그로 검색", )} - setTenantSearch(e.target.value)} - placeholder={t( - "ui.dev.clients.general.tenant_access.search_placeholder", - "테넌트 이름 또는 슬러그로 검색", - )} - disabled={!tenantAccessRestricted} - /> - -
- {tenantAccessRestricted ? ( - filteredTenants.length > 0 ? ( - filteredTenants.map((tenant) => { - const checked = allowedTenantIds.includes(tenant.id); - return ( -
@@ -1310,19 +1347,45 @@ function ClientGeneralPage() {
{tenantAccessRestricted && allowedTenantIds.length > 0 ? (
- {allowedTenantIds.map((tenantId) => { - const tenant = tenantData?.items.find( - (item) => item.id === tenantId, - ); - return ( + {selectedAllowedTenants.map((tenant) => ( + + + {tenant.name} + + {tenant.slug} + + + + ))} + {allowedTenantIds + .filter( + (tenantId) => + !selectedAllowedTenants.some( + (tenant) => tenant.id === tenantId, + ), + ) + .map((tenantId) => ( - - {tenant?.name || tenantId} - + + {tenantId} - ); - })} + ))}
) : (
diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index c0af058a..8212652f 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -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] diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index ede404b3..72729806 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -1448,6 +1448,7 @@ selected_title = "허용 테넌트" selected_empty = "아직 선택된 테넌트가 없습니다." empty = "검색 결과가 없습니다." hint = "제한을 켜면 tenant 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다." +autocomplete_hint = "테넌트 이름을 입력하면 자동 완성 후보가 나타납니다. 클릭하면 허용 목록에 추가됩니다." validation_required = "테넌트 접근 제한을 사용할 경우 허용 테넌트를 하나 이상 선택해야 합니다." [ui.dev.clients.general.security] diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml index 63c4b651..3ce81004 100644 --- a/devfront/src/locales/template.toml +++ b/devfront/src/locales/template.toml @@ -1531,6 +1531,7 @@ selected_title = "" selected_empty = "" empty = "" hint = "" +autocomplete_hint = "" validation_required = "" [ui.dev.clients.general.security]