1
0
forked from baron/baron-sso

devfront consents 및 audit 테이블 공통화

This commit is contained in:
2026-05-13 17:20:43 +09:00
parent 481ec5fc15
commit 76a63264fe
3 changed files with 151 additions and 139 deletions

View File

@@ -1,8 +1,9 @@
import type { ReactNode, ThHTMLAttributes } from "react";
import type { SortConfig } from "../../utils";
import { commonTableHeadClass } from "../../../ui/table";
export const sortableTableHeadBaseClassName =
"h-12 px-6 text-left text-xs font-sans font-bold uppercase tracking-[0.08em] text-foreground align-middle";
commonTableHeadClass;
export const sortableTableHeaderClassName =
"sticky top-0 z-10 bg-secondary shadow-sm";

View File

@@ -6,7 +6,7 @@ export const commonTableFooterClass = "bg-muted/50 font-medium text-foreground";
export const commonTableRowClass =
"border-b transition-colors hover:bg-muted/30 data-[state=selected]:bg-muted";
export const commonTableHeadClass =
"h-12 px-6 text-left text-xs font-bold uppercase tracking-[0.08em] text-muted-foreground align-middle";
"h-12 px-6 text-left text-xs font-sans font-bold uppercase tracking-[0.08em] text-foreground align-middle";
export const commonTableCellClass = "p-6 align-middle text-sm";
export const commonTableCaptionClass = "mt-4 text-sm text-muted-foreground";
export const commonTableShellClass =

View File

@@ -28,6 +28,10 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import {
commonTableShellClass,
commonTableViewportClass,
} from "../../../../common/ui/table";
import { fetchClient, fetchConsents, revokeConsent } from "../../lib/devApi";
import { t } from "../../lib/i18n";
import { cn } from "../../lib/utils";
@@ -430,146 +434,153 @@ function ClientConsentsPage() {
</CardContent>
)}
<Table>
<TableHeader>
<TableRow>
<TableHead>
{t("ui.dev.clients.consents.table.user", "User")}
</TableHead>
<TableHead>
{t("ui.dev.clients.consents.table.tenant", "Tenant")}
</TableHead>
<TableHead>
{t("ui.dev.clients.consents.table.status", "Status")}
</TableHead>
<TableHead>
{t("ui.dev.clients.consents.table.scopes", "Granted Scopes")}
</TableHead>
<TableHead>
{t(
"ui.dev.clients.consents.table.first_granted",
"First Granted",
)}
</TableHead>
<TableHead>
{t(
"ui.dev.clients.consents.table.last_auth",
"Last Authenticated / Revoked",
)}
</TableHead>
<TableHead className="text-right">
{t("ui.dev.clients.consents.table.action", "Action")}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredRows.length === 0 && !isLoading && !error ? (
<TableRow>
<TableCell
colSpan={7}
className="h-32 text-center text-muted-foreground"
>
<div className="flex flex-col items-center gap-2">
<Search className="h-8 w-8 opacity-20" />
<p>
{t(
"msg.dev.clients.consents.empty",
"No consents found.",
)}
</p>
</div>
</TableCell>
</TableRow>
) : (
filteredRows.map((row) => (
<TableRow
key={`${row.subject}-${row.clientId}`}
className={row.status === "revoked" ? "opacity-60" : ""}
>
<TableCell>
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-xs font-bold text-primary">
{(row.userName || row.subject)
.slice(0, 2)
.toUpperCase()}
</div>
<div className="flex flex-col">
<span className="text-sm font-semibold">
{row.userName ||
t("ui.dev.clients.consents.subject", "Subject")}
</span>
<span className="text-xs text-muted-foreground">
{row.subject}
</span>
</div>
</div>
</TableCell>
<TableCell>
<div className="flex flex-col">
<span className="text-sm font-semibold">
{row.tenantName || t("ui.common.na", "N/A")}
</span>
<span className="text-xs text-muted-foreground">
{row.tenantId}
</span>
</div>
</TableCell>
<TableCell>
{row.status === "active" ? (
<Badge variant="success">
{t("ui.common.status.active", "Active")}
</Badge>
) : (
<Badge variant="warning">
{t("ui.dev.clients.consents.status_revoked", "Revoked")}
</Badge>
<div className={commonTableShellClass}>
<div className={commonTableViewportClass}>
<Table>
<TableHeader className="sticky top-0 z-10 bg-secondary shadow-sm">
<TableRow>
<TableHead>
{t("ui.dev.clients.consents.table.user", "User")}
</TableHead>
<TableHead>
{t("ui.dev.clients.consents.table.tenant", "Tenant")}
</TableHead>
<TableHead>
{t("ui.dev.clients.consents.table.status", "Status")}
</TableHead>
<TableHead>
{t("ui.dev.clients.consents.table.scopes", "Granted Scopes")}
</TableHead>
<TableHead>
{t(
"ui.dev.clients.consents.table.first_granted",
"First Granted",
)}
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{row.grantedScopes.map((scope) => (
<Badge
key={scope}
variant="muted"
className="border bg-muted/40 text-foreground"
>
{scope}
</Badge>
))}
</div>
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{new Date(row.createdAt).toLocaleString()}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{row.status === "revoked" && row.deletedAt ? (
<span className="text-destructive font-medium">
{t("ui.dev.clients.consents.revoked_at", "Revoked: ")}
{new Date(row.deletedAt).toLocaleString()}
</span>
) : row.authenticatedAt ? (
new Date(row.authenticatedAt).toLocaleString()
) : (
"-"
</TableHead>
<TableHead>
{t(
"ui.dev.clients.consents.table.last_auth",
"Last Authenticated / Revoked",
)}
</TableCell>
<TableCell className="text-right">
{row.status === "active" && (
<Button
variant="ghost"
className="text-destructive hover:bg-destructive/10"
onClick={() => handleRevoke(row.subject)}
disabled={revokeMutation.isPending}
>
{t("ui.dev.clients.consents.revoke", "Revoke")}
</Button>
)}
</TableCell>
</TableHead>
<TableHead className="text-right">
{t("ui.dev.clients.consents.table.action", "Action")}
</TableHead>
</TableRow>
))
)}
</TableBody>
</Table>
</TableHeader>
<TableBody>
{filteredRows.length === 0 && !isLoading && !error ? (
<TableRow>
<TableCell
colSpan={7}
className="h-32 text-center text-muted-foreground"
>
<div className="flex flex-col items-center gap-2">
<Search className="h-8 w-8 opacity-20" />
<p>
{t(
"msg.dev.clients.consents.empty",
"No consents found.",
)}
</p>
</div>
</TableCell>
</TableRow>
) : (
filteredRows.map((row) => (
<TableRow
key={`${row.subject}-${row.clientId}`}
className={row.status === "revoked" ? "opacity-60" : ""}
>
<TableCell>
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-xs font-bold text-primary">
{(row.userName || row.subject)
.slice(0, 2)
.toUpperCase()}
</div>
<div className="flex flex-col">
<span className="text-sm font-semibold">
{row.userName ||
t("ui.dev.clients.consents.subject", "Subject")}
</span>
<span className="text-xs text-muted-foreground">
{row.subject}
</span>
</div>
</div>
</TableCell>
<TableCell>
<div className="flex flex-col">
<span className="text-sm font-semibold">
{row.tenantName || t("ui.common.na", "N/A")}
</span>
<span className="text-xs text-muted-foreground">
{row.tenantId}
</span>
</div>
</TableCell>
<TableCell>
{row.status === "active" ? (
<Badge variant="success">
{t("ui.common.status.active", "Active")}
</Badge>
) : (
<Badge variant="warning">
{t(
"ui.dev.clients.consents.status_revoked",
"Revoked",
)}
</Badge>
)}
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{row.grantedScopes.map((scope) => (
<Badge
key={scope}
variant="muted"
className="border bg-muted/40 text-foreground"
>
{scope}
</Badge>
))}
</div>
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{new Date(row.createdAt).toLocaleString()}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{row.status === "revoked" && row.deletedAt ? (
<span className="text-destructive font-medium">
{t("ui.dev.clients.consents.revoked_at", "Revoked: ")}
{new Date(row.deletedAt).toLocaleString()}
</span>
) : row.authenticatedAt ? (
new Date(row.authenticatedAt).toLocaleString()
) : (
"-"
)}
</TableCell>
<TableCell className="text-right">
{row.status === "active" && (
<Button
variant="ghost"
className="text-destructive hover:bg-destructive/10"
onClick={() => handleRevoke(row.subject)}
disabled={revokeMutation.isPending}
>
{t("ui.dev.clients.consents.revoke", "Revoke")}
</Button>
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
<CardContent className="flex items-center justify-between border-t border-border bg-muted/10 px-6 py-4 text-sm text-muted-foreground">
<p>
{t(