forked from baron/baron-sso
devfront consents 및 audit 테이블 공통화
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
import type { ReactNode, ThHTMLAttributes } from "react";
|
import type { ReactNode, ThHTMLAttributes } from "react";
|
||||||
import type { SortConfig } from "../../utils";
|
import type { SortConfig } from "../../utils";
|
||||||
|
import { commonTableHeadClass } from "../../../ui/table";
|
||||||
|
|
||||||
export const sortableTableHeadBaseClassName =
|
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 =
|
export const sortableTableHeaderClassName =
|
||||||
"sticky top-0 z-10 bg-secondary shadow-sm";
|
"sticky top-0 z-10 bg-secondary shadow-sm";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const commonTableFooterClass = "bg-muted/50 font-medium text-foreground";
|
|||||||
export const commonTableRowClass =
|
export const commonTableRowClass =
|
||||||
"border-b transition-colors hover:bg-muted/30 data-[state=selected]:bg-muted";
|
"border-b transition-colors hover:bg-muted/30 data-[state=selected]:bg-muted";
|
||||||
export const commonTableHeadClass =
|
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 commonTableCellClass = "p-6 align-middle text-sm";
|
||||||
export const commonTableCaptionClass = "mt-4 text-sm text-muted-foreground";
|
export const commonTableCaptionClass = "mt-4 text-sm text-muted-foreground";
|
||||||
export const commonTableShellClass =
|
export const commonTableShellClass =
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "../../components/ui/table";
|
} from "../../components/ui/table";
|
||||||
|
import {
|
||||||
|
commonTableShellClass,
|
||||||
|
commonTableViewportClass,
|
||||||
|
} from "../../../../common/ui/table";
|
||||||
import { fetchClient, fetchConsents, revokeConsent } from "../../lib/devApi";
|
import { fetchClient, fetchConsents, revokeConsent } from "../../lib/devApi";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
import { cn } from "../../lib/utils";
|
import { cn } from "../../lib/utils";
|
||||||
@@ -430,146 +434,153 @@ function ClientConsentsPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Table>
|
<div className={commonTableShellClass}>
|
||||||
<TableHeader>
|
<div className={commonTableViewportClass}>
|
||||||
<TableRow>
|
<Table>
|
||||||
<TableHead>
|
<TableHeader className="sticky top-0 z-10 bg-secondary shadow-sm">
|
||||||
{t("ui.dev.clients.consents.table.user", "User")}
|
<TableRow>
|
||||||
</TableHead>
|
<TableHead>
|
||||||
<TableHead>
|
{t("ui.dev.clients.consents.table.user", "User")}
|
||||||
{t("ui.dev.clients.consents.table.tenant", "Tenant")}
|
</TableHead>
|
||||||
</TableHead>
|
<TableHead>
|
||||||
<TableHead>
|
{t("ui.dev.clients.consents.table.tenant", "Tenant")}
|
||||||
{t("ui.dev.clients.consents.table.status", "Status")}
|
</TableHead>
|
||||||
</TableHead>
|
<TableHead>
|
||||||
<TableHead>
|
{t("ui.dev.clients.consents.table.status", "Status")}
|
||||||
{t("ui.dev.clients.consents.table.scopes", "Granted Scopes")}
|
</TableHead>
|
||||||
</TableHead>
|
<TableHead>
|
||||||
<TableHead>
|
{t("ui.dev.clients.consents.table.scopes", "Granted Scopes")}
|
||||||
{t(
|
</TableHead>
|
||||||
"ui.dev.clients.consents.table.first_granted",
|
<TableHead>
|
||||||
"First Granted",
|
{t(
|
||||||
)}
|
"ui.dev.clients.consents.table.first_granted",
|
||||||
</TableHead>
|
"First Granted",
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableHead>
|
||||||
<TableCell>
|
<TableHead>
|
||||||
<div className="flex flex-wrap gap-1">
|
{t(
|
||||||
{row.grantedScopes.map((scope) => (
|
"ui.dev.clients.consents.table.last_auth",
|
||||||
<Badge
|
"Last Authenticated / Revoked",
|
||||||
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>
|
</TableHead>
|
||||||
<TableCell className="text-right">
|
<TableHead className="text-right">
|
||||||
{row.status === "active" && (
|
{t("ui.dev.clients.consents.table.action", "Action")}
|
||||||
<Button
|
</TableHead>
|
||||||
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>
|
</TableRow>
|
||||||
))
|
</TableHeader>
|
||||||
)}
|
<TableBody>
|
||||||
</TableBody>
|
{filteredRows.length === 0 && !isLoading && !error ? (
|
||||||
</Table>
|
<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">
|
<CardContent className="flex items-center justify-between border-t border-border bg-muted/10 px-6 py-4 text-sm text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
{t(
|
{t(
|
||||||
|
|||||||
Reference in New Issue
Block a user