diff --git a/src/app/api-keys/page.tsx b/src/app/api-keys/page.tsx index 6f6221f..351031a 100644 --- a/src/app/api-keys/page.tsx +++ b/src/app/api-keys/page.tsx @@ -3,6 +3,7 @@ import ConfirmActionDialog from "@/components/ConfirmActionDialog"; import GuardedPage from "@/components/GuardedPage"; import IssueDetailsSummary from "@/components/IssueDetailsSummary"; +import SearchField from "@/components/SearchField"; import { apiKeys as apiKeysApi, type ApiKeyExpiryOption, @@ -156,6 +157,7 @@ function maskKey(prefix: string) { export default function ApiKeysPage() { const [keys, setKeys] = useState([]); + const [search, setSearch] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [showCreate, setShowCreate] = useState(false); @@ -186,6 +188,11 @@ export default function ApiKeysPage() { return { active, revoked, totalRequests, expiringSoon }; }, [keys]); + const filteredKeys = useMemo(() => { + const query = search.trim().toLowerCase(); + if (!query) return keys; + return keys.filter((key) => [key.name, key.keyPrefix, key.isActive ? "active" : "revoked", ...key.permissions].some((value) => value.toLowerCase().includes(query))); + }, [keys, search]); useEffect(() => { void loadKeys(); @@ -490,20 +497,12 @@ export default function ApiKeysPage() {
-

- {keys.length} API keys total -

-
+ setSearch(event.target.value)} containerStyle={{ flex: "1 1 360px", minWidth: 240 }} /> +
+ {keys.length} API keys total
+ ) : filteredKeys.length === 0 ? ( +
+ +

No API keys match your search

+
) : (
- {keys.map((key) => { + {filteredKeys.map((key) => { const canReveal = Boolean(sessionRawKeys[key.id]); const isRevealed = revealedKeyId === key.id && canReveal; const rawValue = sessionRawKeys[key.id]; diff --git a/src/app/globals.css b/src/app/globals.css index d549837..dd446a0 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -378,6 +378,12 @@ body { flex-wrap: wrap; } +.users-toolbar-filter { + flex: 0 1 200px; + width: min(200px, 100%); + align-self: flex-start; +} + .ui-toolbar-note { margin-left: auto; text-align: right; @@ -427,6 +433,20 @@ body { justify-content: center; } + .users-toolbar .ui-toolbar-actions { + flex-direction: column; + } + + .users-toolbar .ui-toolbar-actions > button { + width: 100%; + flex-basis: auto; + } + + .users-toolbar-filter { + flex-basis: 100%; + width: 100%; + } + .ui-pill-switch, .ui-tab-scroll { width: 100%; diff --git a/src/app/users/components/ActiveUsersPanel.tsx b/src/app/users/components/ActiveUsersPanel.tsx index 1553a0f..286b4d1 100644 --- a/src/app/users/components/ActiveUsersPanel.tsx +++ b/src/app/users/components/ActiveUsersPanel.tsx @@ -20,6 +20,7 @@ interface ActiveUsersPanelProps { totalItems: number; startItem: number; endItem: number; + emptyMessage: string; onPageChange: (page: number) => void; onEditAccess: (user: UserRecord) => void; onEditRole: (user: UserRecord) => void; @@ -38,6 +39,7 @@ export default function ActiveUsersPanel({ totalItems, startItem, endItem, + emptyMessage, onPageChange, onEditAccess, onEditRole, @@ -69,7 +71,7 @@ export default function ActiveUsersPanel({ fontSize: 13, }} > - No active users found. + {emptyMessage}
) : ( <> diff --git a/src/app/users/components/ArchivedUsersPanel.tsx b/src/app/users/components/ArchivedUsersPanel.tsx index ead50db..7f61b8b 100644 --- a/src/app/users/components/ArchivedUsersPanel.tsx +++ b/src/app/users/components/ArchivedUsersPanel.tsx @@ -16,6 +16,7 @@ interface ArchivedUsersPanelProps { totalItems: number; startItem: number; endItem: number; + emptyMessage: string; onPageChange: (page: number) => void; } @@ -27,6 +28,7 @@ export default function ArchivedUsersPanel({ totalItems, startItem, endItem, + emptyMessage, onPageChange, }: ArchivedUsersPanelProps) { return ( @@ -77,7 +79,7 @@ export default function ArchivedUsersPanel({ fontSize: 13, }} > - No archived users. + {emptyMessage}
) : ( <> diff --git a/src/app/users/components/PendingInvitesPanel.tsx b/src/app/users/components/PendingInvitesPanel.tsx index b97b1cb..4b1d18d 100644 --- a/src/app/users/components/PendingInvitesPanel.tsx +++ b/src/app/users/components/PendingInvitesPanel.tsx @@ -17,6 +17,7 @@ interface PendingInvitesPanelProps { totalItems: number; startItem: number; endItem: number; + emptyMessage: string; onPageChange: (page: number) => void; onCopyFreshInviteLink: (invitationId: string) => void | Promise; onRevokeInvitation: (invitationId: string) => void | Promise; @@ -32,6 +33,7 @@ export default function PendingInvitesPanel({ totalItems, startItem, endItem, + emptyMessage, onPageChange, onCopyFreshInviteLink, onRevokeInvitation, @@ -83,7 +85,7 @@ export default function PendingInvitesPanel({ fontSize: 13, }} > - No pending invitations. + {emptyMessage}
) : ( <> diff --git a/src/app/users/components/UsersToolbar.tsx b/src/app/users/components/UsersToolbar.tsx index 9243528..8e4d03f 100644 --- a/src/app/users/components/UsersToolbar.tsx +++ b/src/app/users/components/UsersToolbar.tsx @@ -1,9 +1,11 @@ -import { Clock, Loader2, Lock, Plus, RefreshCw, Users } from "lucide-react"; +import SearchField from "@/components/SearchField"; +import { Loader2, Plus, RefreshCw } from "lucide-react"; export type UsersTabKey = "active-users" | "pending-invites" | "archived-users"; interface UsersToolbarProps { activeTab: UsersTabKey; + search: string; activeUsersCount: number; invitationsCount: number; archivedUsersCount: number; @@ -11,12 +13,14 @@ interface UsersToolbarProps { refreshing: boolean; canInvite: boolean; onTabChange: (tab: UsersTabKey) => void; + onSearchChange: (value: string) => void; onRefresh: () => void | Promise; onInvite: () => void; } export default function UsersToolbar({ activeTab, + search, activeUsersCount, invitationsCount, archivedUsersCount, @@ -24,81 +28,50 @@ export default function UsersToolbar({ refreshing, canInvite, onTabChange, + onSearchChange, onRefresh, onInvite, }: UsersToolbarProps) { - const tabs = [ + const tabs: Array<{ key: UsersTabKey; label: string }> = [ { - key: "active-users" as const, + key: "active-users", label: `Active Users (${activeUsersCount})`, - icon: Users, }, { - key: "pending-invites" as const, + key: "pending-invites", label: `Pending Invites (${invitationsCount})`, - icon: Clock, }, { - key: "archived-users" as const, + key: "archived-users", label: `Archived Users (${archivedUsersCount})`, - icon: Lock, }, ]; return (
-
onSearchChange(event.target.value)} + containerStyle={{ flex: "1 1 360px", minWidth: 240 }} + /> + +