forked from baron/baron-sso
i18n refresh and frontend fixes
This commit is contained in:
@@ -13,7 +13,6 @@ import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../../components/ui/card";
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
import { fetchClient, fetchConsents, revokeConsent } from "../../lib/devApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
|
||||
function ClientConsentsPage() {
|
||||
const params = useParams();
|
||||
@@ -65,17 +65,20 @@ function ClientConsentsPage() {
|
||||
<div className="space-y-2">
|
||||
<nav className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||
<Link to="/" className="hover:text-primary">
|
||||
Home
|
||||
{t("ui.dev.clients.consents.breadcrumb.home", "Home")}
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<Link to="/clients" className="hover:text-primary">
|
||||
Clients
|
||||
{t("ui.dev.clients.consents.breadcrumb.clients", "Clients")}
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span>{clientData?.client?.name || clientId}</span>
|
||||
<span>/</span>
|
||||
<span className="text-foreground font-semibold">
|
||||
User Consent Grants
|
||||
{t(
|
||||
"ui.dev.clients.consents.breadcrumb.current",
|
||||
"User Consent Grants",
|
||||
)}
|
||||
</span>
|
||||
</nav>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -86,10 +89,13 @@ function ClientConsentsPage() {
|
||||
</Button>
|
||||
<div>
|
||||
<p className="text-3xl font-black leading-tight">
|
||||
User Consent Grants
|
||||
{t("ui.dev.clients.consents.title", "User Consent Grants")}
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
OIDC Relying Party 사용자 권한을 검토·관리합니다.
|
||||
{t(
|
||||
"msg.dev.clients.consents.subtitle",
|
||||
"OIDC Relying Party 사용자 권한을 검토·관리합니다.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,7 +106,9 @@ function ClientConsentsPage() {
|
||||
clientData?.client?.status === "active" ? "success" : "muted"
|
||||
}
|
||||
>
|
||||
{clientData?.client?.status === "active" ? "Active" : "Inactive"}
|
||||
{clientData?.client?.status === "active"
|
||||
? t("ui.common.status.active", "Active")
|
||||
: t("ui.common.status.inactive", "Inactive")}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,16 +117,16 @@ function ClientConsentsPage() {
|
||||
to={`/clients/${clientId}`}
|
||||
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Connection
|
||||
{t("ui.dev.clients.details.tab.connection", "Connection")}
|
||||
</Link>
|
||||
<span className="whitespace-nowrap border-b-2 border-primary pb-1 text-primary">
|
||||
Consent & Users
|
||||
{t("ui.dev.clients.details.tab.consents", "Consent & Users")}
|
||||
</span>
|
||||
<Link
|
||||
to={`/clients/${clientId}/settings`}
|
||||
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Settings
|
||||
{t("ui.dev.clients.details.tab.settings", "Settings")}
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
@@ -130,34 +138,48 @@ function ClientConsentsPage() {
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
className="pl-10"
|
||||
placeholder="사용자 ID, 이름, 이메일로 검색"
|
||||
placeholder={t(
|
||||
"ui.dev.clients.consents.search_placeholder",
|
||||
"사용자 ID, 이름, 이메일로 검색",
|
||||
)}
|
||||
value={subjectInput}
|
||||
onChange={(e) => setSubjectInput(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||
Status:
|
||||
{t("ui.dev.clients.consents.status_label", "Status:")}
|
||||
</span>
|
||||
<select className="h-10 rounded-lg border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/30">
|
||||
<option>All Statuses</option>
|
||||
<option selected>Active</option>
|
||||
<option>Revoked</option>
|
||||
<option>
|
||||
{t("ui.dev.clients.consents.status_all", "All Statuses")}
|
||||
</option>
|
||||
<option selected>
|
||||
{t("ui.common.status.active", "Active")}
|
||||
</option>
|
||||
<option>
|
||||
{t("ui.dev.clients.consents.status_revoked", "Revoked")}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="ghost" className="gap-1 text-muted-foreground">
|
||||
<Filter className="h-4 w-4" />
|
||||
Advanced Filters
|
||||
{t(
|
||||
"ui.dev.clients.consents.filters.advanced",
|
||||
"Advanced Filters",
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
className="shadow-sm shadow-primary/30"
|
||||
onClick={() => setSubject(subjectInput.trim())}
|
||||
>
|
||||
검색
|
||||
{t("ui.common.search", "검색")}
|
||||
</Button>
|
||||
<Button className="shadow-sm shadow-primary/30">
|
||||
{t("ui.dev.clients.consents.export_csv", "Export CSV")}
|
||||
</Button>
|
||||
<Button className="shadow-sm shadow-primary/30">Export CSV</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -165,32 +187,58 @@ function ClientConsentsPage() {
|
||||
<Card className="glass-panel">
|
||||
{error && (
|
||||
<CardContent className="text-sm text-red-500">
|
||||
Error loading consents: {(error as Error).message}
|
||||
{t(
|
||||
"msg.dev.clients.consents.load_error",
|
||||
"Error loading consents: {{error}}",
|
||||
{
|
||||
error: (error as Error).message,
|
||||
},
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
{isLoading && (
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Loading consents...
|
||||
{t("msg.dev.clients.consents.loading", "Loading consents...")}
|
||||
</CardContent>
|
||||
)}
|
||||
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Tenant</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Granted Scopes</TableHead>
|
||||
<TableHead>First Granted</TableHead>
|
||||
<TableHead>Last Authenticated</TableHead>
|
||||
<TableHead className="text-right">Action</TableHead>
|
||||
<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",
|
||||
)}
|
||||
</TableHead>
|
||||
<TableHead className="text-right">
|
||||
{t("ui.dev.clients.consents.table.action", "Action")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.length === 0 && !isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="h-24 text-center">
|
||||
No consents found.
|
||||
{t("msg.dev.clients.consents.empty", "No consents found.")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
@@ -199,11 +247,14 @@ function ClientConsentsPage() {
|
||||
<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()}
|
||||
{(row.userName || row.subject)
|
||||
.slice(0, 2)
|
||||
.toUpperCase()}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-semibold">
|
||||
{row.userName || "Subject"}
|
||||
{row.userName ||
|
||||
t("ui.dev.clients.consents.subject", "Subject")}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{row.subject}
|
||||
@@ -214,7 +265,7 @@ function ClientConsentsPage() {
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-semibold">
|
||||
{row.tenantName || "N/A"}
|
||||
{row.tenantName || t("ui.common.na", "N/A")}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{row.tenantId}
|
||||
@@ -222,7 +273,9 @@ function ClientConsentsPage() {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="success">Active</Badge>
|
||||
<Badge variant="success">
|
||||
{t("ui.common.status.active", "Active")}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
@@ -253,7 +306,7 @@ function ClientConsentsPage() {
|
||||
revokeMutation.mutate({ subject: row.subject })
|
||||
}
|
||||
>
|
||||
Revoke
|
||||
{t("ui.dev.clients.consents.revoke", "Revoke")}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -263,15 +316,23 @@ function ClientConsentsPage() {
|
||||
</Table>
|
||||
<CardContent className="flex items-center justify-between border-t border-border bg-muted/10 px-6 py-4 text-sm text-muted-foreground">
|
||||
<p>
|
||||
Showing <span className="font-semibold text-foreground">{rows.length > 0 ? 1 : 0}</span> to{" "}
|
||||
<span className="font-semibold text-foreground">{rows.length}</span> of{" "}
|
||||
<span className="font-semibold text-foreground">{rows.length}</span> users
|
||||
{t(
|
||||
"msg.dev.clients.consents.showing",
|
||||
"Showing {{from}} to {{to}} of {{total}} users",
|
||||
{
|
||||
from: rows.length > 0 ? 1 : 0,
|
||||
to: rows.length,
|
||||
total: rows.length,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="icon" disabled>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" disabled={rows.length === 0}>1</Button>
|
||||
<Button size="sm" disabled={rows.length === 0}>
|
||||
1
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" disabled>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -283,7 +344,10 @@ function ClientConsentsPage() {
|
||||
<Card className="glass-panel">
|
||||
<CardHeader className="pb-2">
|
||||
<p className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||
Active Grants
|
||||
{t(
|
||||
"ui.dev.clients.consents.stats.active_grants",
|
||||
"Active Grants",
|
||||
)}
|
||||
</p>
|
||||
<CardTitle className="text-2xl font-black">{rows.length}</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -291,7 +355,10 @@ function ClientConsentsPage() {
|
||||
<Card className="glass-panel">
|
||||
<CardHeader className="pb-2">
|
||||
<p className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||
Total Scopes Issued
|
||||
{t(
|
||||
"ui.dev.clients.consents.stats.total_scopes",
|
||||
"Total Scopes Issued",
|
||||
)}
|
||||
</p>
|
||||
<CardTitle className="text-2xl font-black">
|
||||
{rows.reduce((acc, row) => acc + row.grantedScopes.length, 0)}
|
||||
@@ -301,13 +368,18 @@ function ClientConsentsPage() {
|
||||
<Card className="glass-panel">
|
||||
<CardHeader className="pb-2">
|
||||
<p className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||
Avg. Scopes per User
|
||||
{t(
|
||||
"ui.dev.clients.consents.stats.avg_scopes",
|
||||
"Avg. Scopes per User",
|
||||
)}
|
||||
</p>
|
||||
<CardTitle className="text-2xl font-black">
|
||||
{rows.length > 0
|
||||
? (
|
||||
rows.reduce((acc, row) => acc + row.grantedScopes.length, 0) /
|
||||
rows.length
|
||||
rows.reduce(
|
||||
(acc, row) => acc + row.grantedScopes.length,
|
||||
0,
|
||||
) / rows.length
|
||||
).toFixed(1)
|
||||
: "0.0"}
|
||||
</CardTitle>
|
||||
|
||||
Reference in New Issue
Block a user