diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index 172e9366..7885825f 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -5,7 +5,6 @@ import { Info, Plus, Save, - Search, Shield, ShieldHalf, Sparkles, @@ -36,7 +35,6 @@ import type { ClientStatus, ClientType, ClientUpsertRequest, - MyTenantSummary, TenantSummary, } from "../../lib/devApi"; import { @@ -45,7 +43,7 @@ import { deleteClient, fetchClient, fetchClientRelations, - fetchMyTenants, + fetchTenants, refreshHeadlessJwksCache, revokeHeadlessJwksCache, updateClient, @@ -58,6 +56,7 @@ 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, dateTimeInputToUnixSeconds, @@ -597,8 +596,8 @@ function ClientGeneralPage() { retry: false, }); const { data: tenantData } = useQuery({ - queryKey: ["my-tenants"], - queryFn: fetchMyTenants, + queryKey: ["tenants", "all"], + queryFn: () => fetchTenants(), }); const [name, setName] = useState(""); @@ -622,8 +621,6 @@ function ClientGeneralPage() { ] = useState(false); const [tenantAccessRestricted, setTenantAccessRestricted] = useState(false); const [allowedTenantIds, setAllowedTenantIds] = useState([]); - const [tenantSearch, setTenantSearch] = useState(""); - const [isTenantSearchOpen, setIsTenantSearchOpen] = useState(false); const [autoLoginSupported, setAutoLoginSupported] = useState(false); const [autoLoginUrl, setAutoLoginUrl] = useState(""); @@ -990,10 +987,6 @@ function ClientGeneralPage() { const handleTenantAccessToggle = (enabled: boolean) => { setTenantAccessRestricted(enabled); - setIsTenantSearchOpen(enabled); - if (!enabled) { - setTenantSearch(""); - } setScopes((current) => normalizeScopesForTenantAccess(current, enabled)); }; @@ -1009,8 +1002,6 @@ function ClientGeneralPage() { setAllowedTenantIds((current) => current.includes(tenantId) ? current : [...current, tenantId], ); - setTenantSearch(""); - setIsTenantSearchOpen(true); }; const addScope = () => { @@ -1270,25 +1261,10 @@ function ClientGeneralPage() { normalizedIdTokenClaimItems, ); const idTokenClaimPreviewJson = JSON.stringify(idTokenClaimPreview, null, 2); - const normalizedTenantSearch = tenantSearch.trim().toLowerCase(); - const tenantOptions: Array = - tenantData ?? []; - const filteredTenants = tenantOptions.filter((tenant) => { - if (!normalizedTenantSearch) { - return true; - } - 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 tenantOptions: TenantSummary[] = tenantData?.items ?? []; const selectedAllowedTenants = allowedTenantIds .map((tenantId) => tenantOptions.find((item) => item.id === tenantId)) - .filter( - (tenant): tenant is TenantSummary | MyTenantSummary => tenant != null, - ); + .filter((tenant): tenant is TenantSummary => tenant != null); const refreshHeadlessJwksCacheMutation = useMutation({ mutationFn: async () => { @@ -2368,92 +2344,19 @@ function ClientGeneralPage() {
-
diff --git a/devfront/src/features/clients/components/TenantAccessPicker.tsx b/devfront/src/features/clients/components/TenantAccessPicker.tsx new file mode 100644 index 00000000..f787eed5 --- /dev/null +++ b/devfront/src/features/clients/components/TenantAccessPicker.tsx @@ -0,0 +1,146 @@ +import { Building2 } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { createPortal } from "react-dom"; +import { Button } from "../../../components/ui/button"; +import { t } from "../../../lib/i18n"; +import { + buildAuthenticatedOrgChartTenantPickerUrl, + type OrgChartTenantSelection, + parseOrgChartTenantSelection, +} from "../orgChartPicker"; + +type TenantAccessPickerProps = { + disabled?: boolean; + selectedCount: number; + onSelectTenant: (selection: OrgChartTenantSelection) => void; +}; + +function resolveOrgFrontBaseUrl() { + return ( + import.meta.env.VITE_ORGFRONT_PUBLIC_URL || + import.meta.env.ORGFRONT_URL || + "http://localhost:5175" + ); +} + +export function TenantAccessPicker({ + disabled = false, + selectedCount, + onSelectTenant, +}: TenantAccessPickerProps) { + const [pickerOpen, setPickerOpen] = useState(false); + const pickerUrl = useMemo( + () => buildAuthenticatedOrgChartTenantPickerUrl(resolveOrgFrontBaseUrl()), + [], + ); + + useEffect(() => { + if (!pickerOpen) return; + + const onMessage = (event: MessageEvent) => { + const selection = parseOrgChartTenantSelection(event.data); + if (!selection) return; + onSelectTenant(selection); + setPickerOpen(false); + }; + + window.addEventListener("message", onMessage); + return () => window.removeEventListener("message", onMessage); + }, [onSelectTenant, pickerOpen]); + + const pickerDialog = + pickerOpen && typeof document !== "undefined" + ? createPortal( +
+
+
+
+

+ {t( + "ui.dev.clients.general.tenant_access.picker_title", + "테넌트 선택", + )} +

+

+ {t( + "msg.dev.clients.general.tenant_access.picker_description", + "orgfront 조직도에서 허용할 테넌트를 선택하면 목록에 추가됩니다.", + )} +

+
+ +
+
+