Skip to content

Commit 04c9a56

Browse files
committed
fix bug
1 parent df39f79 commit 04c9a56

File tree

3 files changed

+114
-53
lines changed

3 files changed

+114
-53
lines changed

dashboard/ai-analytics/src/app/components/CostPredictionModal.tsx

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// src/app/components/CostPredictionModal.tsx
22
'use client';
33

4-
import { useState, useEffect, useRef } from 'react';
4+
import { useState, useEffect, useRef, useCallback } from 'react';
55
import { X, Calculator, Copy, Check, Sparkles, ChevronDown, ChevronUp } from 'lucide-react';
66
import { AreaChart, BarChart } from '@tremor/react';
77
import { useTinybirdToken } from '@/providers/TinybirdProvider';
@@ -230,16 +230,31 @@ export default function CostPredictionModal({
230230
}
231231
}, [isOpen]);
232232

233-
// Handle keyboard shortcuts
233+
// Handle keyboard shortcuts and outside clicks
234234
useEffect(() => {
235235
const handleKeyDown = (e: KeyboardEvent) => {
236-
if (e.key === 'Escape') {
236+
if (e.key === 'Escape' && isOpen) {
237237
onClose();
238238
}
239239
};
240240

241-
window.addEventListener('keydown', handleKeyDown);
242-
return () => window.removeEventListener('keydown', handleKeyDown);
241+
// Add event listeners when the modal is open
242+
if (isOpen) {
243+
window.addEventListener('keydown', handleKeyDown);
244+
245+
// Clean up when the modal closes or component unmounts
246+
return () => {
247+
window.removeEventListener('keydown', handleKeyDown);
248+
};
249+
}
250+
}, [isOpen, onClose]);
251+
252+
// Create a memoized handler for backdrop clicks
253+
const handleBackdropClick = useCallback((e: React.MouseEvent) => {
254+
// Ensure we're clicking the backdrop, not its children
255+
if (e.target === e.currentTarget) {
256+
onClose();
257+
}
243258
}, [onClose]);
244259

245260
const handleSubmit = async (e: React.FormEvent) => {
@@ -678,15 +693,21 @@ export default function CostPredictionModal({
678693
<>
679694
{isOpen && (
680695
<>
681-
{/* Modal backdrop */}
696+
{/* Modal backdrop - updated to use the memoized handler */}
682697
<div
683698
className="fixed inset-0 bg-black/70 backdrop-blur-sm z-50"
684-
onClick={onClose}
699+
onClick={handleBackdropClick}
685700
/>
686701

687702
{/* Modal container */}
688-
<div className="fixed inset-0 flex items-center justify-center z-50 p-4">
689-
<div className="bg-gray-900 rounded-xl shadow-xl w-full max-w-3xl max-h-[90vh] flex flex-col">
703+
<div
704+
className="fixed inset-0 flex items-center justify-center z-50 p-4"
705+
onClick={handleBackdropClick} // Also handle clicks on the container outside the modal
706+
>
707+
<div
708+
className="bg-gray-900 rounded-xl shadow-xl w-full max-w-3xl max-h-[90vh] flex flex-col"
709+
onClick={(e) => e.stopPropagation()} // Prevent clicks on the modal itself from closing it
710+
>
690711
{/* Modal header */}
691712
<div className="flex items-center justify-between p-4 border-b border-gray-800">
692713
<div className="flex items-center space-x-2">

dashboard/ai-analytics/src/app/components/DateRangeSelector.tsx

Lines changed: 66 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useState, useEffect } from 'react';
3+
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
44
import { useRouter } from 'next/navigation';
55
import { useSearchParams } from 'next/navigation';
66
import { format, subDays, subHours, subMonths, parse, isValid } from 'date-fns';
@@ -24,27 +24,38 @@ interface DateRangeSelectorProps {
2424
export default function DateRangeSelector({ onDateRangeChange }: DateRangeSelectorProps) {
2525
const router = useRouter();
2626
const searchParams = useSearchParams();
27+
28+
// Use refs for the popover triggers
29+
const rangePopoverTriggerRef = useRef<HTMLButtonElement>(null);
30+
const calendarPopoverTriggerRef = useRef<HTMLButtonElement>(null);
31+
32+
// State for popover open/close
2733
const [isOpen, setIsOpen] = useState(false);
34+
const [calendarOpen, setCalendarOpen] = useState(false);
35+
36+
// Other state
2837
const [selectedRange, setSelectedRange] = useState<string | null>(null);
2938
const [dateRange, setDateRange] = useState<{ start: Date | null; end: Date | null }>({
3039
start: null,
3140
end: null,
3241
});
33-
const [calendarOpen, setCalendarOpen] = useState(false);
3442
const [isPredefinedRange, setIsPredefinedRange] = useState(true);
3543

3644
// Time selection states
3745
const [startHour, setStartHour] = useState("00");
3846
const [startMinute, setStartMinute] = useState("00");
3947
const [endHour, setEndHour] = useState("23");
4048
const [endMinute, setEndMinute] = useState("59");
49+
50+
// Track if we've already initialized from URL
51+
const initializedRef = useRef(false);
4152

4253
// Generate hours and minutes for dropdowns
43-
const hours = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
44-
const minutes = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'));
54+
const hours = useMemo(() => Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0')), []);
55+
const minutes = useMemo(() => Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0')), []);
4556

46-
// Predefined date ranges
47-
const dateRangeOptions: DateRangeOption[] = [
57+
// Predefined date ranges - memoize to prevent recreation
58+
const dateRangeOptions = useMemo<DateRangeOption[]>(() => [
4859
{
4960
label: 'Last Hour',
5061
getValue: () => ({
@@ -87,10 +98,25 @@ export default function DateRangeSelector({ onDateRangeChange }: DateRangeSelect
8798
end: new Date(),
8899
}),
89100
},
90-
];
101+
], []);
102+
103+
// Helper function to parse date strings - memoize to prevent recreation
104+
const parseDate = useCallback((dateStr: string): Date | null => {
105+
const formats = ['yyyy-MM-dd HH:mm:ss', 'yyyy-MM-dd\'T\'HH:mm:ss', 'yyyy-MM-dd HH:mm', 'yyyy-MM-dd', 'MM/dd/yyyy', 'dd/MM/yyyy'];
106+
107+
for (const fmt of formats) {
108+
const date = parse(dateStr, fmt, new Date());
109+
if (isValid(date)) return date;
110+
}
111+
112+
return null;
113+
}, []);
91114

92-
// Initialize from URL params
115+
// Initialize from URL params - add dependency array and use ref to prevent multiple runs
93116
useEffect(() => {
117+
if (initializedRef.current) return;
118+
initializedRef.current = true;
119+
94120
const start_date = searchParams.get('start_date');
95121
const end_date = searchParams.get('end_date');
96122

@@ -157,23 +183,10 @@ export default function DateRangeSelector({ onDateRangeChange }: DateRangeSelect
157183
updateUrlParams(start, end);
158184
}
159185
}
160-
});
161-
162-
// Helper function to parse date strings
163-
const parseDate = (dateStr: string): Date | null => {
164-
// Try different formats, with the primary format first
165-
const formats = ['yyyy-MM-dd HH:mm:ss', 'yyyy-MM-dd\'T\'HH:mm:ss', 'yyyy-MM-dd HH:mm', 'yyyy-MM-dd', 'MM/dd/yyyy', 'dd/MM/yyyy'];
166-
167-
for (const fmt of formats) {
168-
const date = parse(dateStr, fmt, new Date());
169-
if (isValid(date)) return date;
170-
}
171-
172-
return null;
173-
};
186+
}, [searchParams, dateRangeOptions, parseDate]);
174187

175-
// Update URL params when date range changes
176-
const updateUrlParams = (start: Date | null, end: Date | null) => {
188+
// Update URL params when date range changes - memoize to prevent recreation
189+
const updateUrlParams = useCallback((start: Date | null, end: Date | null) => {
177190
if (!start || !end) return;
178191

179192
// Format with time
@@ -207,10 +220,10 @@ export default function DateRangeSelector({ onDateRangeChange }: DateRangeSelect
207220
if (onDateRangeChange) {
208221
onDateRangeChange(startDateStr, endDateStr);
209222
}
210-
};
223+
}, [router, searchParams, isPredefinedRange, startHour, startMinute, endHour, endMinute, onDateRangeChange]);
211224

212-
// Handle predefined range selection
213-
const handleRangeSelect = (option: DateRangeOption) => {
225+
// Handle predefined range selection - memoize to prevent recreation
226+
const handleRangeSelect = useCallback((option: DateRangeOption) => {
214227
const { start, end } = option.getValue();
215228
setDateRange({ start, end });
216229
setSelectedRange(option.label); // Use the predefined label (e.g., "Last 30 days")
@@ -224,10 +237,10 @@ export default function DateRangeSelector({ onDateRangeChange }: DateRangeSelect
224237

225238
updateUrlParams(start, end);
226239
setIsOpen(false);
227-
};
240+
}, [updateUrlParams]);
228241

229-
// Handle calendar date selection
230-
const handleCalendarSelect = (range: { from: Date | undefined; to?: Date | undefined }) => {
242+
// Handle calendar date selection - memoize to prevent recreation
243+
const handleCalendarSelect = useCallback((range: { from: Date | undefined; to?: Date | undefined }) => {
231244
if (!range.from) return;
232245

233246
const start = range.from;
@@ -240,24 +253,34 @@ export default function DateRangeSelector({ onDateRangeChange }: DateRangeSelect
240253

241254
// Don't close the calendar or update URL params yet
242255
// Wait for the Apply button to be clicked
243-
};
256+
}, []);
244257

245-
// Handle time change
246-
const handleTimeChange = () => {
258+
// Handle time change - memoize to prevent recreation
259+
const handleTimeChange = useCallback(() => {
247260
if (!dateRange.start || !dateRange.end) return;
248261

249262
updateUrlParams(dateRange.start, dateRange.end);
250263
setCalendarOpen(false); // Only close the calendar when Apply is clicked
251-
};
264+
}, [dateRange, updateUrlParams]);
265+
266+
// Memoize the open/close handlers to prevent recreation
267+
const handleRangePopoverOpenChange = useCallback((open: boolean) => {
268+
setIsOpen(open);
269+
}, []);
270+
271+
const handleCalendarPopoverOpenChange = useCallback((open: boolean) => {
272+
setCalendarOpen(open);
273+
}, []);
252274

253275
return (
254276
<div className="flex items-center">
255277
{isPredefinedRange ? (
256278
// Predefined range layout - Text with chevron on left, calendar on right
257279
<>
258-
<Popover open={isOpen} onOpenChange={setIsOpen}>
280+
<Popover open={isOpen} onOpenChange={handleRangePopoverOpenChange}>
259281
<PopoverTrigger asChild>
260282
<Button
283+
ref={rangePopoverTriggerRef}
261284
variant="outline"
262285
className={cn(
263286
"flex items-center justify-between gap-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-l-md rounded-r-none px-3 py-2 h-10",
@@ -285,9 +308,10 @@ export default function DateRangeSelector({ onDateRangeChange }: DateRangeSelect
285308
</PopoverContent>
286309
</Popover>
287310

288-
<Popover open={calendarOpen} onOpenChange={setCalendarOpen}>
311+
<Popover open={calendarOpen} onOpenChange={handleCalendarPopoverOpenChange}>
289312
<PopoverTrigger asChild>
290313
<Button
314+
ref={calendarPopoverTriggerRef}
291315
variant="outline"
292316
className={cn(
293317
"flex items-center bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border-l-0 rounded-l-none rounded-r-md px-2 h-10",
@@ -382,11 +406,12 @@ export default function DateRangeSelector({ onDateRangeChange }: DateRangeSelect
382406
</Popover>
383407
</>
384408
) : (
385-
// Custom range layout - Chevron on left, calendar on right
409+
// Custom range layout - similar changes for this section
386410
<>
387-
<Popover open={isOpen} onOpenChange={setIsOpen}>
411+
<Popover open={isOpen} onOpenChange={handleRangePopoverOpenChange}>
388412
<PopoverTrigger asChild>
389413
<Button
414+
ref={rangePopoverTriggerRef}
390415
variant="outline"
391416
className={cn(
392417
"flex items-center bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-l-md rounded-r-none px-2 h-10",
@@ -411,17 +436,18 @@ export default function DateRangeSelector({ onDateRangeChange }: DateRangeSelect
411436
</PopoverContent>
412437
</Popover>
413438

414-
<Popover open={calendarOpen} onOpenChange={setCalendarOpen}>
439+
<Popover open={calendarOpen} onOpenChange={handleCalendarPopoverOpenChange}>
415440
<PopoverTrigger asChild>
416441
<Button
442+
ref={calendarPopoverTriggerRef}
417443
variant="outline"
418444
className={cn(
419445
"flex items-center gap-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border-l-0 rounded-l-none rounded-r-md px-3 py-2 h-10",
420446
"transition-all duration-200 ease-in-out"
421447
)}
422448
>
423449
<span className="text-sm font-medium">
424-
{selectedRange || 'Select date range'}
450+
{selectedRange || 'Select dates'}
425451
</span>
426452
<CalendarIcon className="h-4 w-4 text-gray-500 dark:text-gray-400" />
427453
</Button>

dashboard/ai-analytics/src/providers/TinybirdProvider.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { createContext, useContext, useState, ReactNode } from 'react';
3+
import { createContext, useContext, useState, ReactNode, useCallback, useMemo } from 'react';
44
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
55

66
interface TinybirdContextType {
@@ -15,11 +15,25 @@ const TinybirdContext = createContext<TinybirdContextType | null>(null);
1515
const queryClient = new QueryClient();
1616

1717
export function TinybirdProvider({ children }: { children: ReactNode }) {
18-
const [token, setToken] = useState<string | null>(null);
19-
const [orgName, setOrgName] = useState<string | null>(null);
18+
const [token, setTokenState] = useState<string | null>(null);
19+
const [orgName, setOrgNameState] = useState<string | null>(null);
20+
21+
// Memoize these functions so they don't change on every render
22+
const setToken = useCallback((newToken: string) => {
23+
setTokenState(newToken);
24+
}, []);
25+
26+
const setOrgName = useCallback((newOrgName: string) => {
27+
setOrgNameState(newOrgName);
28+
}, []);
29+
30+
// Create a stable context value
31+
const contextValue = useMemo(() => {
32+
return { token, orgName, setToken, setOrgName };
33+
}, [token, orgName, setToken, setOrgName]);
2034

2135
return (
22-
<TinybirdContext.Provider value={{ token, orgName, setToken, setOrgName }}>
36+
<TinybirdContext.Provider value={contextValue}>
2337
<QueryClientProvider client={queryClient}>
2438
{children}
2539
</QueryClientProvider>

0 commit comments

Comments
 (0)