mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-13 08:15:21 +02:00
add local state in filters
This commit is contained in:
parent
6827744327
commit
10265715f9
6 changed files with 103 additions and 66 deletions
|
|
@ -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<ActiveFilter[]>(() => {
|
||||
return decodeFiltersFromURL(searchParams, superadminFilterAttributes);
|
||||
});
|
||||
|
||||
// Dialog state for comment editing
|
||||
const [isCommentDialogOpen, setIsCommentDialogOpen] = useState(false);
|
||||
const [commentRunId, setCommentRunId] = useState<number | null>(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<string, unknown>) => {
|
||||
const calculateDuration = (isCompleted: boolean, usageInfo?: Record<string, unknown>) => {
|
||||
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() {
|
|||
</TableCell>
|
||||
<TableCell className="text-sm whitespace-pre-wrap break-words">
|
||||
<span className={!run.is_completed ? "font-semibold text-blue-600" : ""}>
|
||||
{calculateDuration(run.created_at, run.is_completed, run.usage_info)}
|
||||
{calculateDuration(run.is_completed, run.usage_info)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
|
|
|
|||
|
|
@ -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<DateRangeFilterProps> = ({
|
|||
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<DateRangeFilterProps> = ({
|
|||
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<DateRangeFilterProps> = ({
|
|||
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')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -151,8 +166,9 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
|
|||
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')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<NumberFilterProps> = ({
|
|||
max,
|
||||
step = 1,
|
||||
}) => {
|
||||
// Local state for fast typing - only syncs to parent on blur
|
||||
const [localValue, setLocalValue] = useState<string>(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<HTMLInputElement>) => {
|
||||
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<NumberFilterProps> = ({
|
|||
<Input
|
||||
id="number-filter"
|
||||
type="number"
|
||||
value={value.value ?? ''}
|
||||
value={localValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder}
|
||||
min={min}
|
||||
max={max}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
|
@ -24,13 +26,23 @@ export const NumberRangeFilter: React.FC<NumberRangeFilterProps> = ({
|
|||
step = 1,
|
||||
presets = [],
|
||||
}) => {
|
||||
const handleMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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<string>(value.min?.toString() ?? "");
|
||||
const [localMax, setLocalMax] = useState<string>(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<HTMLInputElement>) => {
|
||||
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<NumberRangeFilterProps> = ({
|
|||
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<NumberRangeFilterProps> = ({
|
|||
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}
|
||||
|
|
|
|||
|
|
@ -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<TagInputFilterProps> = ({ 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<HTMLInputElement>) => {
|
||||
const tags = e.target.value
|
||||
.split(/[,\n]/)
|
||||
|
|
|
|||
|
|
@ -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<TextFilterProps> = ({
|
|||
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 (
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={value.value || ""}
|
||||
onChange={(e) => onChange({ value: e.target.value })}
|
||||
value={localValue}
|
||||
onChange={(e) => setLocalValue(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder}
|
||||
maxLength={maxLength}
|
||||
className={error ? "border-red-500" : ""}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue