feat: add search functionality for processes and improve pagination controls
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState, useCallback, useRef } from 'react'
|
import React, { useEffect, useState, useCallback, useRef } from 'react'
|
||||||
import { EventsOn, EventsOff } from '../wailsjs/runtime/runtime'
|
import { EventsOn, EventsOff } from '../wailsjs/runtime/runtime'
|
||||||
import { Activity, Cpu, Database, Monitor, ChevronDown, ChevronRight, Layers, X, RefreshCw, OctagonX } from 'lucide-react'
|
import { Activity, Cpu, Database, Monitor, ChevronDown, ChevronRight, Layers, X, RefreshCw, OctagonX, Search } from 'lucide-react'
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
|
||||||
import { Progress } from '@/components/ui/progress'
|
import { Progress } from '@/components/ui/progress'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -525,6 +525,7 @@ export default function App() {
|
|||||||
const [gpuOpen, setGpuOpen] = useState(true)
|
const [gpuOpen, setGpuOpen] = useState(true)
|
||||||
const [gpuSortBy, setGpuSortBy] = useState<'vram' | 'ram'>('vram')
|
const [gpuSortBy, setGpuSortBy] = useState<'vram' | 'ram'>('vram')
|
||||||
const [navStack, setNavStack] = useState<NavItem[]>([])
|
const [navStack, setNavStack] = useState<NavItem[]>([])
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
|
||||||
const openDetail = useCallback((pid: number) => {
|
const openDetail = useCallback((pid: number) => {
|
||||||
setSelectedPid(pid)
|
setSelectedPid(pid)
|
||||||
@@ -554,15 +555,18 @@ export default function App() {
|
|||||||
: 0
|
: 0
|
||||||
const gpuUtilPct = metrics?.gpu_util_percent ?? 0
|
const gpuUtilPct = metrics?.gpu_util_percent ?? 0
|
||||||
|
|
||||||
const gpuProcesses = [...(metrics?.gpu_processes ?? [])].sort((a, b) =>
|
const searchLower = search.toLowerCase()
|
||||||
gpuSortBy === 'vram' ? b.vram_mb - a.vram_mb : b.ram - a.ram
|
const gpuProcesses = [...(metrics?.gpu_processes ?? [])]
|
||||||
)
|
.filter(p => !search || p.name.toLowerCase().includes(searchLower) || String(p.pid).includes(search))
|
||||||
|
.sort((a, b) => gpuSortBy === 'vram' ? b.vram_mb - a.vram_mb : b.ram - a.ram)
|
||||||
const gpuTotalPages = Math.ceil(gpuProcesses.length / gpuPageSize)
|
const gpuTotalPages = Math.ceil(gpuProcesses.length / gpuPageSize)
|
||||||
const safeGpuPage = Math.min(gpuPage, Math.max(0, gpuTotalPages - 1))
|
const safeGpuPage = Math.min(gpuPage, Math.max(0, gpuTotalPages - 1))
|
||||||
const pagedGpuProcesses = gpuProcesses.slice(safeGpuPage * gpuPageSize, (safeGpuPage + 1) * gpuPageSize)
|
const pagedGpuProcesses = gpuProcesses.slice(safeGpuPage * gpuPageSize, (safeGpuPage + 1) * gpuPageSize)
|
||||||
|
|
||||||
const sortedProcesses = metrics
|
const sortedProcesses = metrics
|
||||||
? [...metrics.processes].sort((a, b) => b[sortBy] - a[sortBy])
|
? [...metrics.processes]
|
||||||
|
.filter(p => !search || p.name.toLowerCase().includes(searchLower) || String(p.pid).includes(search))
|
||||||
|
.sort((a, b) => b[sortBy] - a[sortBy])
|
||||||
: []
|
: []
|
||||||
const totalPages = Math.ceil(sortedProcesses.length / pageSize)
|
const totalPages = Math.ceil(sortedProcesses.length / pageSize)
|
||||||
const safePage = Math.min(page, Math.max(0, totalPages - 1))
|
const safePage = Math.min(page, Math.max(0, totalPages - 1))
|
||||||
@@ -590,6 +594,26 @@ export default function App() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="p-5 space-y-4 max-w-4xl mx-auto">
|
<main className="p-5 space-y-4 max-w-4xl mx-auto">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-zinc-600 pointer-events-none" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by name or PID…"
|
||||||
|
value={search}
|
||||||
|
onChange={e => { setSearch(e.target.value); setPage(0); setGpuPage(0) }}
|
||||||
|
className="w-full bg-zinc-900/60 border border-zinc-800 rounded-lg pl-8 pr-8 py-2 text-sm text-zinc-300 placeholder-zinc-700 focus:outline-none focus:border-zinc-600 transition-colors"
|
||||||
|
/>
|
||||||
|
{search && (
|
||||||
|
<button
|
||||||
|
onClick={() => { setSearch(''); setPage(0); setGpuPage(0) }}
|
||||||
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Processes */}
|
{/* Processes */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
@@ -717,14 +741,14 @@ export default function App() {
|
|||||||
<span className="text-xs text-zinc-600">
|
<span className="text-xs text-zinc-600">
|
||||||
{safePage * pageSize + 1}–{Math.min((safePage + 1) * pageSize, sortedProcesses.length)} of {sortedProcesses.length}
|
{safePage * pageSize + 1}–{Math.min((safePage + 1) * pageSize, sortedProcesses.length)} of {sortedProcesses.length}
|
||||||
</span>
|
</span>
|
||||||
<select
|
<div className="flex rounded border border-zinc-800 overflow-hidden" title="Items per page">
|
||||||
value={pageSize}
|
{[10, 20, 50, 100].map(n => (
|
||||||
onChange={e => { setPageSize(Number(e.target.value)); setPage(0) }}
|
<button key={n} onClick={() => { setPageSize(n); setPage(0) }}
|
||||||
className="text-xs bg-zinc-900 border border-zinc-800 text-zinc-400 rounded px-1.5 py-0.5 cursor-pointer hover:border-zinc-600 transition-colors"
|
className={cn('px-1.5 py-0.5 text-xs transition-colors', pageSize === n ? 'bg-zinc-700 text-zinc-200' : 'bg-zinc-900 text-zinc-600 hover:text-zinc-400 hover:bg-zinc-800')}>
|
||||||
title="Items per page"
|
{n}
|
||||||
>
|
</button>
|
||||||
{[10, 20, 50, 100].map(n => <option key={n} value={n}>{n} / page</option>)}
|
))}
|
||||||
</select>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
@@ -910,14 +934,14 @@ export default function App() {
|
|||||||
<span className="text-xs text-zinc-600">
|
<span className="text-xs text-zinc-600">
|
||||||
{safeGpuPage * gpuPageSize + 1}–{Math.min((safeGpuPage + 1) * gpuPageSize, gpuProcesses.length)} of {gpuProcesses.length}
|
{safeGpuPage * gpuPageSize + 1}–{Math.min((safeGpuPage + 1) * gpuPageSize, gpuProcesses.length)} of {gpuProcesses.length}
|
||||||
</span>
|
</span>
|
||||||
<select
|
<div className="flex rounded border border-zinc-800 overflow-hidden" title="Items per page">
|
||||||
value={gpuPageSize}
|
{[4, 10, 20, 50].map(n => (
|
||||||
onChange={e => { setGpuPageSize(Number(e.target.value)); setGpuPage(0) }}
|
<button key={n} onClick={() => { setGpuPageSize(n); setGpuPage(0) }}
|
||||||
className="text-xs bg-zinc-900 border border-zinc-800 text-zinc-400 rounded px-1.5 py-0.5 cursor-pointer hover:border-zinc-600 transition-colors"
|
className={cn('px-1.5 py-0.5 text-xs transition-colors', gpuPageSize === n ? 'bg-zinc-700 text-zinc-200' : 'bg-zinc-900 text-zinc-600 hover:text-zinc-400 hover:bg-zinc-800')}>
|
||||||
title="Items per page"
|
{n}
|
||||||
>
|
</button>
|
||||||
{[4, 10, 20, 50].map(n => <option key={n} value={n}>{n} / page</option>)}
|
))}
|
||||||
</select>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button onClick={() => setGpuPage(0)} disabled={safeGpuPage === 0}
|
<button onClick={() => setGpuPage(0)} disabled={safeGpuPage === 0}
|
||||||
|
|||||||
Reference in New Issue
Block a user