1
0
forked from baron/baron-sso

검색/필터 바 shell 공통화

This commit is contained in:
2026-05-14 14:42:29 +09:00
parent b3c360c54f
commit faffb6dc05
6 changed files with 398 additions and 357 deletions

View File

@@ -34,6 +34,7 @@ import {
commonTableShellClass,
commonTableViewportClass,
} from "../../../../common/ui/table";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { Button } from "../../components/ui/button";
import {
Card,
@@ -426,7 +427,9 @@ function UserListPage() {
)}
actions={
<>
<div className="mr-2 flex items-center gap-2">
<SearchFilterBar
primary={
<>
<div className="relative w-48">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
@@ -450,7 +453,9 @@ function UserListPage() {
}}
disabled={profile?.role === "tenant_admin"}
>
<option value="">{t("ui.common.all", "전체 테넌트")}</option>
<option value="">
{t("ui.common.all", "전체 테넌트")}
</option>
{tenants.map((t) => (
<option key={t.id} value={t.slug}>
{t.name}
@@ -466,7 +471,9 @@ function UserListPage() {
>
{t("ui.common.search", "검색")}
</Button>
</div>
</>
}
/>
<Button
variant="outline"

View File

@@ -30,6 +30,7 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { PageHeader } from "../../../../common/core/components/page";
import type { DevAuditLog } from "../../lib/devApi";
import { fetchDevAuditLogs } from "../../lib/devApi";
@@ -227,12 +228,14 @@ function AuditLogsPage() {
<Card className="glass-panel">
<CardContent className="space-y-4 pt-6">
<SearchFilterBar
primary={
<form
onSubmit={(e) => {
e.preventDefault();
query.refetch();
}}
className="grid gap-2 md:grid-cols-[1fr,1fr,180px]"
className="grid flex-1 gap-2 md:grid-cols-[1fr,1fr,180px]"
>
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@@ -248,7 +251,9 @@ function AuditLogsPage() {
</div>
<Input
value={searchAction}
onChange={(e) => setSearchAction(e.target.value.toUpperCase())}
onChange={(e) =>
setSearchAction(e.target.value.toUpperCase())
}
placeholder={t(
"ui.dev.audit.filter.action",
"Filter by Action (e.g. ROTATE_SECRET)",
@@ -270,6 +275,8 @@ function AuditLogsPage() {
</option>
</select>
</form>
}
/>
<div
className={

View File

@@ -9,6 +9,7 @@ import {
sortableTableHeadBaseClassName,
sortableTableHeaderClassName,
} from "../../../../common/core/components/sort";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { PageHeader } from "../../../../common/core/components/page";
import {
type SortConfig,
@@ -283,8 +284,8 @@ function ClientsPage() {
<Card className="glass-panel">
<CardHeader className="pb-4 pt-6">
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-3 md:flex-row md:items-center">
<SearchFilterBar
primary={
<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
@@ -297,7 +298,9 @@ function ClientsPage() {
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="flex items-center gap-2">
}
actions={
<>
<Button
variant="ghost"
size="sm"
@@ -324,17 +327,18 @@ function ClientsPage() {
{t("ui.dev.clients.badge.dev_session", "DevFront 세션")}
</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">
</>
}
advancedOpen={isAdvancedFilterOpen}
advanced={
<>
<div className="flex flex-wrap items-center gap-6">
<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]"
className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
>
@@ -354,7 +358,7 @@ function ClientsPage() {
{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]"
className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
@@ -372,7 +376,7 @@ function ClientsPage() {
<Button
variant="ghost"
size="sm"
className="text-xs text-muted-foreground ml-auto"
className="ml-auto text-xs text-muted-foreground"
onClick={() => {
setTypeFilter("all");
setStatusFilter("all");
@@ -381,8 +385,9 @@ function ClientsPage() {
{t("ui.common.reset", "초기화")}
</Button>
</div>
)}
</div>
</>
}
/>
</CardHeader>
<CardContent className="pt-0">
<div className="grid gap-4 md:grid-cols-3">

View File

@@ -28,6 +28,7 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import type { DevAuditLog } from "../../lib/devApi";
import { fetchDevAuditLogs } from "../../lib/devApi";
import { t } from "../../lib/i18n";
@@ -227,7 +228,15 @@ function AuditLogsPage() {
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-2 md:grid-cols-[1fr,1fr,180px]">
<SearchFilterBar
primary={
<form
onSubmit={(e) => {
e.preventDefault();
query.refetch();
}}
className="grid flex-1 gap-2 md:grid-cols-[1fr,1fr,180px]"
>
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
@@ -242,7 +251,9 @@ function AuditLogsPage() {
</div>
<Input
value={searchAction}
onChange={(e) => setSearchAction(e.target.value.toUpperCase())}
onChange={(e) =>
setSearchAction(e.target.value.toUpperCase())
}
placeholder={t(
"ui.dev.audit.filter.action",
"Filter by Action (e.g. ROTATE_SECRET)",
@@ -263,7 +274,9 @@ function AuditLogsPage() {
{t("ui.common.status.failure", "Failure")}
</option>
</select>
</div>
</form>
}
/>
<Table className="table-fixed">
<TableHeader>

View File

@@ -26,6 +26,7 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { fetchClient, fetchConsents, revokeConsent } from "../../lib/devApi";
import { t } from "../../lib/i18n";
import { cn } from "../../lib/utils";
@@ -235,8 +236,8 @@ function ClientConsentsPage() {
<Card className="glass-panel">
<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">
<SearchFilterBar
primary={
<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
@@ -249,8 +250,9 @@ function ClientConsentsPage() {
onChange={(e) => setSubjectInput(e.target.value)}
/>
</div>
</div>
<div className="flex items-center gap-3">
}
actions={
<>
<Button
variant="ghost"
className={cn(
@@ -279,11 +281,11 @@ function ClientConsentsPage() {
<Download className="h-4 w-4" />
{t("ui.dev.clients.consents.export_csv", "Export CSV")}
</Button>
</div>
</div>
{isAdvancedFilterOpen && (
<div className="flex flex-col gap-4 rounded-lg bg-secondary/30 p-4 border border-border/40 animate-in fade-in slide-in-from-top-2 duration-200">
</>
}
advancedOpen={isAdvancedFilterOpen}
advanced={
<>
<div className="flex flex-col gap-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
{t("ui.dev.clients.consents.status_label", "Status:")}
@@ -367,8 +369,9 @@ function ClientConsentsPage() {
{t("ui.common.reset", "초기화")}
</Button>
</div>
</div>
)}
</>
}
/>
</CardContent>
</Card>

View File

@@ -36,6 +36,7 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { fetchClients, fetchDevStats } from "../../lib/devApi";
import { t } from "../../lib/i18n";
import { cn } from "../../lib/utils";
@@ -179,8 +180,9 @@ function ClientsPage() {
</Button>
</div>
</div>
<div className="mt-4 flex flex-col gap-3">
<div className="flex flex-col gap-3 md:flex-row md:items-center">
<SearchFilterBar
className="mt-4"
primary={
<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
@@ -193,7 +195,9 @@ function ClientsPage() {
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="flex items-center gap-2">
}
actions={
<>
<Button
variant="ghost"
size="sm"
@@ -220,17 +224,18 @@ function ClientsPage() {
{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">
</>
}
advancedOpen={isAdvancedFilterOpen}
advanced={
<>
<div className="flex flex-wrap items-center gap-6">
<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]"
className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
>
@@ -250,7 +255,7 @@ function ClientsPage() {
{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]"
className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
@@ -268,7 +273,7 @@ function ClientsPage() {
<Button
variant="ghost"
size="sm"
className="text-xs text-muted-foreground ml-auto"
className="ml-auto text-xs text-muted-foreground"
onClick={() => {
setTypeFilter("all");
setStatusFilter("all");
@@ -277,8 +282,9 @@ function ClientsPage() {
{t("ui.common.reset", "초기화")}
</Button>
</div>
)}
</div>
</>
}
/>
</CardHeader>
<CardContent className="pt-0">
<div className="grid gap-4 md:grid-cols-3">