forked from baron/baron-sso
feat(devfront): show client creators and headless filter
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user