From 10265715f9076668269eb1562bd9975effb40f61 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 30 Jan 2026 16:02:37 +0530 Subject: [PATCH] add local state in filters --- ui/src/app/superadmin/runs/page.tsx | 61 ++++++------------- ui/src/components/filters/DateRangeFilter.tsx | 30 ++++++--- ui/src/components/filters/NumberFilter.tsx | 22 +++++-- .../components/filters/NumberRangeFilter.tsx | 30 ++++++--- ui/src/components/filters/TagInputFilter.tsx | 7 ++- ui/src/components/filters/TextFilter.tsx | 19 +++++- 6 files changed, 103 insertions(+), 66 deletions(-) diff --git a/ui/src/app/superadmin/runs/page.tsx b/ui/src/app/superadmin/runs/page.tsx index c1b4b32c..ff429b42 100644 --- a/ui/src/app/superadmin/runs/page.tsx +++ b/ui/src/app/superadmin/runs/page.tsx @@ -81,7 +81,6 @@ export default function RunsPage() { const [isExecutingFilters, setIsExecutingFilters] = useState(false); const [autoRefresh, setAutoRefresh] = useState(false); const [isAutoRefreshing, setIsAutoRefreshing] = useState(false); - const [currentTime, setCurrentTime] = useState(Date.now()); const limit = 50; // Initialize filters from URL @@ -89,6 +88,11 @@ export default function RunsPage() { return decodeFiltersFromURL(searchParams, superadminFilterAttributes); }); + // Applied filters are the ones actually used for fetching (only updated on Apply click) + const [appliedFilters, setAppliedFilters] = useState(() => { + return decodeFiltersFromURL(searchParams, superadminFilterAttributes); + }); + // Dialog state for comment editing const [isCommentDialogOpen, setIsCommentDialogOpen] = useState(false); const [commentRunId, setCommentRunId] = useState(null); @@ -171,49 +175,36 @@ export default function RunsPage() { useEffect(() => { // Fetch runs when token is available and when page changes if (accessToken) { - fetchRuns(currentPage, activeFilters); + fetchRuns(currentPage, appliedFilters); } - }, [currentPage, accessToken, activeFilters, fetchRuns]); + }, [currentPage, accessToken, appliedFilters, fetchRuns]); // Auto-refresh every 5 seconds when enabled and filters are active useEffect(() => { - // Only set up interval if auto-refresh is enabled and there are active filters - if (!autoRefresh || activeFilters.length === 0) { + // Only set up interval if auto-refresh is enabled and there are applied filters + if (!autoRefresh || appliedFilters.length === 0) { return; } const intervalId = setInterval(() => { // Pass true to indicate this is an auto-refresh - fetchRuns(currentPage, activeFilters, true); + fetchRuns(currentPage, appliedFilters, true); }, 5000); // Cleanup interval on unmount or when dependencies change return () => clearInterval(intervalId); - }, [currentPage, activeFilters, fetchRuns, autoRefresh]); - - // Update current time every second to show live duration for running calls - useEffect(() => { - const hasRunningCalls = runs.some(run => !run.is_completed); - if (!hasRunningCalls) { - return; - } - - const intervalId = setInterval(() => { - setCurrentTime(Date.now()); - }, 1000); - - return () => clearInterval(intervalId); - }, [runs]); + }, [currentPage, appliedFilters, fetchRuns, autoRefresh]); const handlePageChange = (page: number) => { setCurrentPage(page); - updatePageInUrl(page, activeFilters); - fetchRuns(page, activeFilters); + updatePageInUrl(page, appliedFilters); + fetchRuns(page, appliedFilters); }; const handleApplyFilters = useCallback(async () => { setIsExecutingFilters(true); setCurrentPage(1); // Reset to first page when applying filters + setAppliedFilters(activeFilters); // Update applied filters updatePageInUrl(1, activeFilters); await fetchRuns(1, activeFilters); setIsExecutingFilters(false); @@ -226,6 +217,7 @@ export default function RunsPage() { const handleClearFilters = useCallback(async () => { setIsExecutingFilters(true); setCurrentPage(1); + setAppliedFilters([]); // Clear applied filters updatePageInUrl(1, []); // Clear filters from URL await fetchRuns(1, []); // Fetch all runs without filters setIsExecutingFilters(false); @@ -265,29 +257,10 @@ export default function RunsPage() { const formatDate = (dateString: string) => new Date(dateString).toLocaleString(); - const calculateDuration = (createdAt: string, isCompleted: boolean, usageInfo?: Record) => { + const calculateDuration = (isCompleted: boolean, usageInfo?: Record) => { if (isCompleted && typeof usageInfo?.call_duration_seconds === 'number') { return `${Number(usageInfo.call_duration_seconds).toFixed(2)}s`; } - - if (!isCompleted) { - const startTime = new Date(createdAt).getTime(); - const duration = Math.floor((currentTime - startTime) / 1000); - - // If duration exceeds 5 minutes (300 seconds), show "-" as it's likely an error - if (duration > 300) { - return '-'; - } - - if (duration < 60) { - return `${duration}s`; - } else { - const minutes = Math.floor(duration / 60); - const seconds = duration % 60; - return `${minutes}m ${seconds}s`; - } - } - return '-'; }; @@ -451,7 +424,7 @@ export default function RunsPage() { - {calculateDuration(run.created_at, run.is_completed, run.usage_info)} + {calculateDuration(run.is_completed, run.usage_info)} diff --git a/ui/src/components/filters/DateRangeFilter.tsx b/ui/src/components/filters/DateRangeFilter.tsx index 414a6560..0a43b27c 100644 --- a/ui/src/components/filters/DateRangeFilter.tsx +++ b/ui/src/components/filters/DateRangeFilter.tsx @@ -1,5 +1,5 @@ import { CalendarIcon } from "lucide-react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; @@ -25,6 +25,19 @@ export const DateRangeFilter: React.FC = ({ const [isFromOpen, setIsFromOpen] = useState(false); const [isToOpen, setIsToOpen] = useState(false); + // Local state for time inputs - only syncs to parent on blur + const [fromTime, setFromTime] = useState(value.from?.toTimeString().slice(0, 5) ?? ""); + const [toTime, setToTime] = useState(value.to?.toTimeString().slice(0, 5) ?? ""); + + // Sync local time state when parent value changes + useEffect(() => { + setFromTime(value.from?.toTimeString().slice(0, 5) ?? ""); + }, [value.from]); + + useEffect(() => { + setToTime(value.to?.toTimeString().slice(0, 5) ?? ""); + }, [value.to]); + const formatDate = (date: Date | null) => { if (!date) return "Select date"; return date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); @@ -55,10 +68,11 @@ export const DateRangeFilter: React.FC = ({ setIsToOpen(false); }; - const handleTimeChange = (type: 'from' | 'to', timeString: string) => { + const handleTimeBlur = (type: 'from' | 'to') => { + const timeString = type === 'from' ? fromTime : toTime; const [hours, minutes] = timeString.split(':').map(Number); const date = type === 'from' ? value.from : value.to; - if (date) { + if (date && !isNaN(hours) && !isNaN(minutes)) { const newDate = new Date(date); newDate.setHours(hours, minutes); onChange({ ...value, [type]: newDate }); @@ -112,8 +126,9 @@ export const DateRangeFilter: React.FC = ({ id="from-time" type="time" className="w-full mt-1 px-3 py-2 border rounded-md" - value={value.from.toTimeString().slice(0, 5)} - onChange={(e) => handleTimeChange('from', e.target.value)} + value={fromTime} + onChange={(e) => setFromTime(e.target.value)} + onBlur={() => handleTimeBlur('from')} /> )} @@ -151,8 +166,9 @@ export const DateRangeFilter: React.FC = ({ id="to-time" type="time" className="w-full mt-1 px-3 py-2 border rounded-md" - value={value.to.toTimeString().slice(0, 5)} - onChange={(e) => handleTimeChange('to', e.target.value)} + value={toTime} + onChange={(e) => setToTime(e.target.value)} + onBlur={() => handleTimeBlur('to')} /> )} diff --git a/ui/src/components/filters/NumberFilter.tsx b/ui/src/components/filters/NumberFilter.tsx index 69f311db..f1b7c491 100644 --- a/ui/src/components/filters/NumberFilter.tsx +++ b/ui/src/components/filters/NumberFilter.tsx @@ -1,3 +1,5 @@ +import { useEffect, useState } from "react"; + import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { NumberValue } from "@/types/filters"; @@ -21,12 +23,23 @@ export const NumberFilter: React.FC = ({ max, step = 1, }) => { + // Local state for fast typing - only syncs to parent on blur + const [localValue, setLocalValue] = useState(value.value?.toString() ?? ''); + + // Sync local state when parent value changes (e.g., from URL or clear) + useEffect(() => { + setLocalValue(value.value?.toString() ?? ''); + }, [value.value]); + const handleChange = (e: React.ChangeEvent) => { - const newValue = e.target.value; - if (newValue === '') { + setLocalValue(e.target.value); + }; + + const handleBlur = () => { + if (localValue === '') { onChange({ value: null }); } else { - const num = parseInt(newValue, 10); + const num = parseInt(localValue, 10); if (!isNaN(num)) { onChange({ value: num }); } @@ -40,8 +53,9 @@ export const NumberFilter: React.FC = ({ = ({ step = 1, presets = [], }) => { - const handleMinChange = (e: React.ChangeEvent) => { - const newValue = e.target.value === "" ? null : Number(e.target.value); + // Local state for fast typing - only syncs to parent on blur + const [localMin, setLocalMin] = useState(value.min?.toString() ?? ""); + const [localMax, setLocalMax] = useState(value.max?.toString() ?? ""); + + // Sync local state when parent value changes (e.g., from URL, clear, or presets) + useEffect(() => { + setLocalMin(value.min?.toString() ?? ""); + setLocalMax(value.max?.toString() ?? ""); + }, [value.min, value.max]); + + const handleMinBlur = () => { + const newValue = localMin === "" ? null : Number(localMin); onChange({ ...value, min: newValue }); }; - const handleMaxChange = (e: React.ChangeEvent) => { - const newValue = e.target.value === "" ? null : Number(e.target.value); + const handleMaxBlur = () => { + const newValue = localMax === "" ? null : Number(localMax); onChange({ ...value, max: newValue }); }; @@ -64,8 +76,9 @@ export const NumberRangeFilter: React.FC = ({ id="min-value" type="number" placeholder={`Min ${unit || 'value'}`} - value={value.min ?? ""} - onChange={handleMinChange} + value={localMin} + onChange={(e) => setLocalMin(e.target.value)} + onBlur={handleMinBlur} min={min} max={max} step={step} @@ -80,8 +93,9 @@ export const NumberRangeFilter: React.FC = ({ id="max-value" type="number" placeholder={`Max ${unit || 'value'}`} - value={value.max ?? ""} - onChange={handleMaxChange} + value={localMax} + onChange={(e) => setLocalMax(e.target.value)} + onBlur={handleMaxBlur} min={min} max={max} step={step} diff --git a/ui/src/components/filters/TagInputFilter.tsx b/ui/src/components/filters/TagInputFilter.tsx index d02466c0..103583b5 100644 --- a/ui/src/components/filters/TagInputFilter.tsx +++ b/ui/src/components/filters/TagInputFilter.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useState } from "react"; +import { ChangeEvent, useEffect, useState } from "react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -14,6 +14,11 @@ interface TagInputFilterProps { export const TagInputFilter: React.FC = ({ value, onChange, error, placeholder="Enter tags (comma separated)" }) => { const [text, setText] = useState(value.codes.join(", ")); + // Sync local state when parent value changes (e.g., from URL or clear) + useEffect(() => { + setText(value.codes.join(", ")); + }, [value.codes]); + const handleBlur = (e: ChangeEvent) => { const tags = e.target.value .split(/[,\n]/) diff --git a/ui/src/components/filters/TextFilter.tsx b/ui/src/components/filters/TextFilter.tsx index f3794203..3354ea90 100644 --- a/ui/src/components/filters/TextFilter.tsx +++ b/ui/src/components/filters/TextFilter.tsx @@ -1,3 +1,5 @@ +import { useEffect, useState } from "react"; + import { Input } from "@/components/ui/input"; import { TextValue } from "@/types/filters"; @@ -16,12 +18,25 @@ export const TextFilter: React.FC = ({ placeholder = "Enter text", maxLength, }) => { + // Local state for fast typing - only syncs to parent on blur + const [localValue, setLocalValue] = useState(value.value || ""); + + // Sync local state when parent value changes (e.g., from URL or clear) + useEffect(() => { + setLocalValue(value.value || ""); + }, [value.value]); + + const handleBlur = () => { + onChange({ value: localValue }); + }; + return (
onChange({ value: e.target.value })} + value={localValue} + onChange={(e) => setLocalValue(e.target.value)} + onBlur={handleBlur} placeholder={placeholder} maxLength={maxLength} className={error ? "border-red-500" : ""}