1
0
forked from baron/baron-sso

feat(devfront): show client creators and headless filter

This commit is contained in:
2026-06-17 22:03:15 +09:00
parent 69e1e32fd4
commit 5f3167a503
7 changed files with 311 additions and 9 deletions

View File

@@ -46,6 +46,7 @@ import {
type ClientSummary,
fetchClients,
fetchDeveloperRequestStatus,
fetchDevUser,
fetchMyTenants,
requestDeveloperAccess,
} from "../../lib/devApi";
@@ -59,6 +60,11 @@ import { ClientLogo } from "./components/ClientLogo";
type ClientSortKey = "application" | "id" | "type" | "status" | "createdAt";
const clientListPreviewCount = 5;
type ClientCreatorInfo = {
name?: string;
email?: string;
};
function isClientTenantLimited(client: ClientSummary) {
const metadata = client.metadata ?? {};
if (metadata.tenant_access_restricted === true) {
@@ -72,6 +78,19 @@ function isClientTenantLimited(client: ClientSummary) {
);
}
function isHeadlessLoginClient(client: ClientSummary) {
return client.metadata?.headless_login_enabled === true;
}
function clientCreatorID(client: ClientSummary) {
return (
client.creatorId?.trim() ||
(typeof client.metadata?.user_id === "string"
? client.metadata.user_id.trim()
: "")
);
}
function ClientsPage() {
const navigate = useNavigate();
const auth = useAuth();
@@ -136,6 +155,50 @@ function ClientsPage() {
});
const clients = data?.items || [];
const creatorIds = useMemo(
() =>
Array.from(
new Set(
clients
.map((client) => clientCreatorID(client))
.filter((creatorId) => creatorId !== ""),
),
).sort(),
[clients],
);
const { data: clientCreators = {} } = useQuery({
queryKey: ["client-creators", creatorIds],
queryFn: async () => {
const entries = await Promise.all(
creatorIds.map(async (creatorId) => {
try {
const user = await fetchDevUser(creatorId);
return [
creatorId,
{
name: user.name,
email: user.email,
},
] as const;
} catch {
return [creatorId, null] as const;
}
}),
);
return entries.reduce<Record<string, ClientCreatorInfo>>(
(acc, [creatorId, user]) => {
if (user) {
acc[creatorId] = user;
}
return acc;
},
{},
);
},
enabled: hasAccessToken && creatorIds.length > 0,
});
const clientSortResolvers = useMemo<
SortResolverMap<ClientSummary, ClientSortKey>
@@ -144,9 +207,7 @@ function ClientsPage() {
application: (client) => client.name || client.id,
id: (client) => client.id,
type: (client) =>
client.metadata?.headless_login_enabled
? "private-headless"
: client.type,
isHeadlessLoginClient(client) ? "private-headless" : client.type,
status: (client) => client.status,
createdAt: (client) =>
client.createdAt ? new Date(client.createdAt) : null,
@@ -160,7 +221,11 @@ function ClientsPage() {
!searchQuery ||
client.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
client.id.toLowerCase().includes(searchQuery.toLowerCase());
const matchesType = typeFilter === "all" || client.type === typeFilter;
const matchesType =
typeFilter === "all" ||
(typeFilter === "headless"
? isHeadlessLoginClient(client)
: client.type === typeFilter && !isHeadlessLoginClient(client));
const matchesStatus =
statusFilter === "all" || client.status === statusFilter;
return matchesSearch && matchesType && matchesStatus;
@@ -369,6 +434,9 @@ function ClientsPage() {
<option value="pkce">
{t("ui.dev.clients.type.pkce", "PKCE")}
</option>
<option value="headless">
{t("ui.dev.clients.type.headless", "Headless Login")}
</option>
</select>
</div>
<div className="flex items-center gap-2">
@@ -409,7 +477,7 @@ function ClientsPage() {
<CardContent className="flex-1 flex flex-col min-h-0 pt-0">
<div className={commonTableShellClass}>
<div className={commonTableViewportClass}>
<Table className="min-w-[1180px]">
<Table className="min-w-[1280px]">
<TableHeader className={sortableTableHeaderClassName}>
<TableRow>
<SortableTableHead
@@ -439,6 +507,9 @@ function ClientsPage() {
sortConfig={sortConfig}
sortKey="status"
/>
<TableHead className={sortableTableHeadBaseClassName}>
{t("ui.dev.clients.table.creator", "생성자")}
</TableHead>
<SortableTableHead
label={t("ui.dev.clients.table.created_at", "생성일")}
onSort={requestSort}
@@ -456,7 +527,7 @@ function ClientsPage() {
{!hasFilterResult && (
<TableRow>
<TableCell
colSpan={6}
colSpan={7}
className="h-32 text-center text-muted-foreground"
>
<div className="space-y-1">
@@ -567,12 +638,12 @@ function ClientsPage() {
<Badge
variant={
client.type === "private" ||
client.metadata?.headless_login_enabled
isHeadlessLoginClient(client)
? "success"
: "muted"
}
>
{client.metadata?.headless_login_enabled
{isHeadlessLoginClient(client)
? t(
"ui.dev.clients.type.private_headless",
"Server side App (Headless Login)",
@@ -598,6 +669,33 @@ function ClientsPage() {
: t("ui.common.status.inactive", "Inactive")}
</Badge>
</TableCell>
<TableCell className="text-sm">
{(() => {
const creatorId = clientCreatorID(client);
const creator = creatorId
? clientCreators[creatorId]
: undefined;
const name = creator?.name?.trim();
const email = creator?.email?.trim();
if (!creatorId) {
return (
<span className="text-muted-foreground">-</span>
);
}
return (
<div className="space-y-1">
<p className="font-medium text-foreground">
{name || creatorId}
</p>
<p className="break-all text-xs text-muted-foreground">
{email || creatorId}
</p>
</div>
);
})()}
</TableCell>
<TableCell className="text-muted-foreground">
{client.createdAt
? new Date(client.createdAt).toLocaleDateString()