forked from baron/baron-sso
연동 앱 및 Consent 목록 상세 필터 기능 추가
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
||||
} from "../../components/ui/table";
|
||||
import { fetchClient, fetchConsents, revokeConsent } from "../../lib/devApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
function ClientConsentsPage() {
|
||||
const params = useParams();
|
||||
@@ -34,6 +35,8 @@ function ClientConsentsPage() {
|
||||
const [subjectInput, setSubjectInput] = useState("");
|
||||
const [subject, setSubject] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
const [scopeFilter, setScopeFilter] = useState("all");
|
||||
const [isAdvancedFilterOpen, setIsAdvancedFilterOpen] = useState(false);
|
||||
|
||||
const { data: clientData } = useQuery({
|
||||
queryKey: ["client", clientId],
|
||||
@@ -72,6 +75,10 @@ function ClientConsentsPage() {
|
||||
};
|
||||
|
||||
const rows = consentsData?.items ?? [];
|
||||
const allScopes = Array.from(new Set(rows.flatMap((r) => r.grantedScopes)));
|
||||
const filteredRows = rows.filter((row) => {
|
||||
return scopeFilter === "all" || row.grantedScopes.includes(scopeFilter);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
@@ -147,59 +154,105 @@ function ClientConsentsPage() {
|
||||
</header>
|
||||
|
||||
<Card className="glass-panel">
|
||||
<CardContent className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex flex-wrap items-center gap-4 flex-1">
|
||||
<div className="relative w-full max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
className="pl-10"
|
||||
placeholder={t(
|
||||
"ui.dev.clients.consents.search_placeholder",
|
||||
"사용자 ID, 이름, 이메일로 검색",
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex flex-wrap items-center gap-4 flex-1">
|
||||
<div className="relative w-full max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
className="pl-10"
|
||||
placeholder={t(
|
||||
"ui.dev.clients.consents.search_placeholder",
|
||||
"사용자 ID, 이름, 이메일로 검색",
|
||||
)}
|
||||
value={subjectInput}
|
||||
onChange={(e) => setSubjectInput(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"gap-1 text-muted-foreground",
|
||||
isAdvancedFilterOpen && "text-primary bg-primary/10",
|
||||
)}
|
||||
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">
|
||||
{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"
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
onClick={() => setIsAdvancedFilterOpen(!isAdvancedFilterOpen)}
|
||||
>
|
||||
<option value="all">
|
||||
{t("ui.dev.clients.consents.status_all", "All Statuses")}
|
||||
</option>
|
||||
<option value="active">
|
||||
{t("ui.common.status.active", "Active")}
|
||||
</option>
|
||||
<option value="revoked">
|
||||
{t("ui.dev.clients.consents.status_revoked", "Revoked")}
|
||||
</option>
|
||||
</select>
|
||||
<Filter className="h-4 w-4" />
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="ghost" className="gap-1 text-muted-foreground">
|
||||
<Filter className="h-4 w-4" />
|
||||
{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>
|
||||
</div>
|
||||
|
||||
{isAdvancedFilterOpen && (
|
||||
<div className="flex flex-wrap items-center gap-6 rounded-lg bg-secondary/30 p-4 border border-border/40 animate-in fade-in slide-in-from-top-2 duration-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap">
|
||||
{t("ui.dev.clients.consents.status_label", "Status:")}
|
||||
</span>
|
||||
<select
|
||||
className="h-9 rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30 min-w-[140px]"
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">
|
||||
{t("ui.dev.clients.consents.status_all", "All Statuses")}
|
||||
</option>
|
||||
<option value="active">
|
||||
{t("ui.common.status.active", "Active")}
|
||||
</option>
|
||||
<option value="revoked">
|
||||
{t("ui.dev.clients.consents.status_revoked", "Revoked")}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap">
|
||||
{t("ui.dev.clients.consents.scope_label", "Scope:")}
|
||||
</span>
|
||||
<select
|
||||
className="h-9 rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30 min-w-[140px]"
|
||||
value={scopeFilter}
|
||||
onChange={(e) => setScopeFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">
|
||||
{t("ui.dev.clients.consents.scope_all", "All Scopes")}
|
||||
</option>
|
||||
{allScopes.map((scope) => (
|
||||
<option key={scope} value={scope}>
|
||||
{scope}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="text-xs text-muted-foreground ml-auto"
|
||||
onClick={() => {
|
||||
setStatusFilter("all");
|
||||
setScopeFilter("all");
|
||||
}}
|
||||
>
|
||||
{t("ui.common.reset", "초기화")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -254,14 +307,14 @@ function ClientConsentsPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.length === 0 && !isLoading ? (
|
||||
{filteredRows.length === 0 && !isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="h-24 text-center">
|
||||
{t("msg.dev.clients.consents.empty", "No consents found.")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
rows.map((row) => (
|
||||
filteredRows.map((row) => (
|
||||
<TableRow
|
||||
key={`${row.subject}-${row.clientId}`}
|
||||
className={row.status === "revoked" ? "opacity-60" : ""}
|
||||
@@ -356,8 +409,8 @@ function ClientConsentsPage() {
|
||||
"msg.dev.clients.consents.showing",
|
||||
"Showing {{from}} to {{to}} of {{total}} users",
|
||||
{
|
||||
from: rows.length > 0 ? 1 : 0,
|
||||
to: rows.length,
|
||||
from: filteredRows.length > 0 ? 1 : 0,
|
||||
to: filteredRows.length,
|
||||
total: rows.length,
|
||||
},
|
||||
)}
|
||||
@@ -366,7 +419,7 @@ function ClientConsentsPage() {
|
||||
<Button variant="outline" size="icon" disabled>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" disabled={rows.length === 0}>
|
||||
<Button size="sm" disabled={filteredRows.length === 0}>
|
||||
1
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" disabled>
|
||||
@@ -402,8 +455,6 @@ function ClientConsentsPage() {
|
||||
{rows.reduce((acc, row) => acc + row.grantedScopes.length, 0)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card className="glass-panel">
|
||||
<CardHeader className="pb-2">
|
||||
<p className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||
{t(
|
||||
|
||||
Reference in New Issue
Block a user