forked from baron/baron-sso
연동 앱 및 Consent 목록 상세 필터 기능 추가
This commit is contained in:
@@ -2,11 +2,13 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import {
|
||||
BookOpenText,
|
||||
Filter,
|
||||
Plus,
|
||||
Search,
|
||||
ServerCog,
|
||||
ShieldHalf,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Avatar,
|
||||
@@ -43,7 +45,24 @@ function ClientsPage() {
|
||||
queryFn: fetchClients,
|
||||
});
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [typeFilter, setTypeFilter] = useState("all");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
const [isAdvancedFilterOpen, setIsAdvancedFilterOpen] = useState(false);
|
||||
|
||||
const clients = data?.items || [];
|
||||
|
||||
const filteredClients = clients.filter((client) => {
|
||||
const matchesSearch =
|
||||
!searchQuery ||
|
||||
client.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
client.id.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesType = typeFilter === "all" || client.type === typeFilter;
|
||||
const matchesStatus =
|
||||
statusFilter === "all" || client.status === statusFilter;
|
||||
return matchesSearch && matchesType && matchesStatus;
|
||||
});
|
||||
|
||||
const totalClients = clients.length;
|
||||
const activeClients = clients.filter(
|
||||
(client) => client.status === "active",
|
||||
@@ -137,25 +156,102 @@ function ClientsPage() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-3 md:grid-cols-[1.5fr,1fr]">
|
||||
<div className="relative">
|
||||
<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.search_placeholder",
|
||||
"클라이언트 이름/ID로 검색...",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 md:justify-start">
|
||||
<Badge variant="muted">
|
||||
{t("ui.dev.clients.badge.tenant_selected", "테넌트: 선택됨")}
|
||||
</Badge>
|
||||
<Badge variant="success">
|
||||
{t("ui.dev.clients.badge.admin_session", "관리자 세션")}
|
||||
</Badge>
|
||||
<div className="mt-4 flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center">
|
||||
<div className="relative flex-1">
|
||||
<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.search_placeholder",
|
||||
"클라이언트 이름/ID로 검색...",
|
||||
)}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"gap-1 text-muted-foreground",
|
||||
isAdvancedFilterOpen && "text-primary bg-primary/10",
|
||||
)}
|
||||
onClick={() => setIsAdvancedFilterOpen(!isAdvancedFilterOpen)}
|
||||
>
|
||||
<Filter className="h-4 w-4" />
|
||||
{t(
|
||||
"ui.dev.clients.consents.filters.advanced",
|
||||
"Advanced Filters",
|
||||
)}
|
||||
</Button>
|
||||
<div className="hidden items-center gap-2 md:flex">
|
||||
<Badge variant="muted">
|
||||
{t("ui.dev.clients.badge.tenant_selected", "테넌트: 선택됨")}
|
||||
</Badge>
|
||||
<Badge variant="success">
|
||||
{t("ui.dev.clients.badge.admin_session", "관리자 세션")}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</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.filter.type_label", "Type:")}
|
||||
</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={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">
|
||||
{t("ui.dev.clients.filter.type_all", "모든 유형")}
|
||||
</option>
|
||||
<option value="private">
|
||||
{t("ui.dev.clients.type.private", "Server side App")}
|
||||
</option>
|
||||
<option value="pkce">
|
||||
{t("ui.dev.clients.type.pkce", "PKCE")}
|
||||
</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.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.filter.status_all", "모든 상태")}
|
||||
</option>
|
||||
<option value="active">
|
||||
{t("ui.common.status.active", "Active")}
|
||||
</option>
|
||||
<option value="inactive">
|
||||
{t("ui.common.status.inactive", "Inactive")}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="text-xs text-muted-foreground ml-auto"
|
||||
onClick={() => {
|
||||
setTypeFilter("all");
|
||||
setStatusFilter("all");
|
||||
}}
|
||||
>
|
||||
{t("ui.common.reset", "초기화")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
@@ -222,7 +318,7 @@ function ClientsPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{clients.map((client) => (
|
||||
{filteredClients.map((client) => (
|
||||
<TableRow key={client.id} className="bg-card/40">
|
||||
<TableCell>
|
||||
<Link
|
||||
@@ -296,7 +392,7 @@ function ClientsPage() {
|
||||
{t(
|
||||
"msg.dev.clients.showing",
|
||||
"Showing {{shown}} of {{total}} clients",
|
||||
{ shown: clients.length, total: totalClients },
|
||||
{ shown: filteredClients.length, total: totalClients },
|
||||
)}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
|
||||
Reference in New Issue
Block a user