forked from baron/baron-sso
RP 테넌트 제한을 orgfront picker로 전환
This commit is contained in:
@@ -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<string[]>([]);
|
||||
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<TenantSummary | MyTenantSummary> =
|
||||
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() {
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-[1.2fr_0.8fr]">
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="tenant-search" className="text-sm font-semibold">
|
||||
<Label className="text-sm font-semibold">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.search_placeholder",
|
||||
"테넌트 이름 또는 슬러그로 검색",
|
||||
"ui.dev.clients.general.tenant_access.picker_label",
|
||||
"허용 테넌트 추가",
|
||||
)}
|
||||
</Label>
|
||||
<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={
|
||||
isGeneralSettingsReadOnly || !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);
|
||||
}}
|
||||
disabled={isGeneralSettingsReadOnly}
|
||||
>
|
||||
<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="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(
|
||||
"ui.dev.clients.general.tenant_access.empty",
|
||||
"검색 결과가 없습니다.",
|
||||
)}
|
||||
</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",
|
||||
"테넌트 이름을 입력하면 자동 완성 후보가 나타납니다. 클릭하면 허용 목록에 추가됩니다.",
|
||||
)
|
||||
: t(
|
||||
"ui.dev.clients.general.tenant_access.disabled",
|
||||
"제한 없음",
|
||||
)}
|
||||
</div>
|
||||
<TenantAccessPicker
|
||||
disabled={isGeneralSettingsReadOnly || !tenantAccessRestricted}
|
||||
selectedCount={allowedTenantIds.length}
|
||||
onSelectTenant={(selection) =>
|
||||
handleSelectAllowedTenant(selection.id)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
|
||||
Reference in New Issue
Block a user