debebounce sidebar

This commit is contained in:
Nate Kelley 2025-03-25 23:52:36 -06:00
parent 60d59ff313
commit f8f6bc7a8c
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
9 changed files with 78 additions and 51 deletions

View File

@ -49,13 +49,15 @@ export function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions)
flush: debounced.flush flush: debounced.flush
}; };
} }
export function useDebounce<T>(value: T, options: DebounceOptions = {}) { export function useDebounce<T>(value: T, options: DebounceOptions = {}) {
const { wait = 1000, maxWait } = options; const { wait = 1000, maxWait, leading = false, trailing = true } = options;
const [debouncedValue, setDebouncedValue] = useState<T>(value); const [debouncedValue, setDebouncedValue] = useState<T>(value);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const maxTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); const maxTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const lastUpdateTime = useRef<number>(Date.now()); const lastUpdateTime = useRef<number>(Date.now());
const valueRef = useRef<T>(value); const valueRef = useRef<T>(value);
const isFirstCallRef = useRef<boolean>(true);
// Keep latest value in ref // Keep latest value in ref
useEffect(() => { useEffect(() => {
@ -70,17 +72,29 @@ export function useDebounce<T>(value: T, options: DebounceOptions = {}) {
if (maxTimeoutRef.current) { if (maxTimeoutRef.current) {
clearTimeout(maxTimeoutRef.current); clearTimeout(maxTimeoutRef.current);
} }
isFirstCallRef.current = true;
}, []); }, []);
useEffect(() => { useEffect(() => {
clearTimeouts(); clearTimeouts();
const now = Date.now(); const now = Date.now();
// Set up the regular debounce timeout // Handle leading edge
timeoutRef.current = setTimeout(() => { if (leading && isFirstCallRef.current) {
setDebouncedValue(valueRef.current); setDebouncedValue(value);
lastUpdateTime.current = Date.now(); lastUpdateTime.current = now;
}, wait); isFirstCallRef.current = false;
return;
}
// Only set up trailing timeout if trailing is true
if (trailing) {
timeoutRef.current = setTimeout(() => {
setDebouncedValue(valueRef.current);
lastUpdateTime.current = Date.now();
}, wait);
}
// Handle maxWait // Handle maxWait
if (maxWait) { if (maxWait) {
@ -96,8 +110,9 @@ export function useDebounce<T>(value: T, options: DebounceOptions = {}) {
}, maxWaitTimeRemaining); }, maxWaitTimeRemaining);
} }
isFirstCallRef.current = false;
return () => clearTimeouts(); return () => clearTimeouts();
}, [value, wait, maxWait, clearTimeouts]); }, [value, wait, maxWait, leading, trailing, clearTimeouts]);
return debouncedValue; return debouncedValue;
} }

View File

@ -44,13 +44,6 @@ export const useSelectedFile = ({
return false; return false;
}, [selectedFile?.type, metricVersionNumber, dashboardVersionNumber]); }, [selectedFile?.type, metricVersionNumber, dashboardVersionNumber]);
console.log(
'isVersionHistoryMode',
isVersionHistoryMode,
metricVersionNumber,
dashboardVersionNumber
);
const [renderViewLayoutKey, setRenderViewLayoutKey] = useState<ChatLayoutView>( const [renderViewLayoutKey, setRenderViewLayoutKey] = useState<ChatLayoutView>(
selectedLayout || 'chat' selectedLayout || 'chat'
); );

View File

@ -6,7 +6,8 @@ import { AppPageLayout, AppSplitter, AppSplitterRef } from '@/components/ui/layo
import { useChatLayoutContextSelector } from '../ChatLayoutContext'; import { useChatLayoutContextSelector } from '../ChatLayoutContext';
import { createAutoSaveId } from '@/components/ui/layouts/AppSplitter/helper'; import { createAutoSaveId } from '@/components/ui/layouts/AppSplitter/helper';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { useMemoizedFn, useUpdateLayoutEffect } from '@/hooks'; import { useDebounce, useMemoizedFn, useUpdateLayoutEffect } from '@/hooks';
import { FileContainerSecondary } from './FileContainerSecondary';
interface FileContainerProps { interface FileContainerProps {
children: React.ReactNode; children: React.ReactNode;
@ -18,15 +19,22 @@ const autoSaveId = 'file-container-splitter';
export const FileContainer: React.FC<FileContainerProps> = ({ children }) => { export const FileContainer: React.FC<FileContainerProps> = ({ children }) => {
const appSplitterRef = useRef<AppSplitterRef>(null); const appSplitterRef = useRef<AppSplitterRef>(null);
const selectedFile = useChatLayoutContextSelector((x) => x.selectedFile);
const selectedFileViewSecondary = useChatLayoutContextSelector( const selectedFileViewSecondary = useChatLayoutContextSelector(
(x) => x.selectedFileViewSecondary (x) => x.selectedFileViewSecondary
); );
const selectedFileViewRenderSecondary = useChatLayoutContextSelector( const selectedFileViewRenderSecondary = useChatLayoutContextSelector(
(x) => x.selectedFileViewRenderSecondary (x) => x.selectedFileViewRenderSecondary
); );
const isOpenSecondary = selectedFileViewRenderSecondary; const isOpenSecondary = selectedFileViewRenderSecondary;
//we need to debounce the selectedFileViewSecondary to avoid flickering
const debouncedSelectedFileViewSecondary = useDebounce(selectedFileViewSecondary, {
wait: 350,
leading: selectedFileViewRenderSecondary
});
const secondaryLayoutDimensions: [string, string] = useMemo(() => { const secondaryLayoutDimensions: [string, string] = useMemo(() => {
const cookieKey = createAutoSaveId(autoSaveId); const cookieKey = createAutoSaveId(autoSaveId);
const cookieValue = Cookies.get(cookieKey); const cookieValue = Cookies.get(cookieKey);
@ -58,6 +66,15 @@ export const FileContainer: React.FC<FileContainerProps> = ({ children }) => {
} }
}); });
const rightChildren = useMemo(() => {
return (
<FileContainerSecondary
selectedFile={selectedFile}
selectedFileViewSecondary={debouncedSelectedFileViewSecondary}
/>
);
}, [debouncedSelectedFileViewSecondary, selectedFile?.id, selectedFile?.type]);
useUpdateLayoutEffect(() => { useUpdateLayoutEffect(() => {
animateOpenSplitter(isOpenSecondary ? 'open' : 'closed'); animateOpenSplitter(isOpenSecondary ? 'open' : 'closed');
}, [isOpenSecondary]); }, [isOpenSecondary]);
@ -70,7 +87,7 @@ export const FileContainer: React.FC<FileContainerProps> = ({ children }) => {
defaultLayout={defaultLayout} defaultLayout={defaultLayout}
initialReady={false} initialReady={false}
leftChildren={children} leftChildren={children}
rightChildren={<div>Right {selectedFileViewSecondary}</div>} rightChildren={rightChildren}
allowResize={selectedFileViewRenderSecondary} allowResize={selectedFileViewRenderSecondary}
preserveSide={'right'} preserveSide={'right'}
rightPanelMinSize={250} rightPanelMinSize={250}

View File

@ -1,3 +1,5 @@
'use client';
import { Button } from '@/components/ui/buttons'; import { Button } from '@/components/ui/buttons';
import { ArrowLeft, History } from '@/components/ui/icons'; import { ArrowLeft, History } from '@/components/ui/icons';
import React, { useMemo, useTransition } from 'react'; import React, { useMemo, useTransition } from 'react';
@ -8,6 +10,7 @@ import { useGetDashboard } from '@/api/buster_rest/dashboards';
import { useAppLayoutContextSelector } from '@/context/BusterAppLayout'; import { useAppLayoutContextSelector } from '@/context/BusterAppLayout';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { timeout } from '@/lib'; import { timeout } from '@/lib';
import { AnimatePresence, motion } from 'framer-motion';
export const FileContainerVersionHistory = React.memo(() => { export const FileContainerVersionHistory = React.memo(() => {
return ( return (

View File

@ -1,31 +1,29 @@
import type { FileType } from '@/api/asset_interfaces/chat';
import type { FileContainerSecondaryProps } from './interfaces'; import type { FileContainerSecondaryProps } from './interfaces';
import type { import { SelectedFileSecondaryRecord } from './secondaryPanelsConfig';
DashboardFileViewSecondary, import { useMemo } from 'react';
MetricFileViewSecondary
} from '../../ChatLayoutContext/useLayoutConfig/interfaces';
const MetricSecondaryRecord: Record< export const FileContainerSecondary: React.FC<FileContainerSecondaryProps> = ({
MetricFileViewSecondary, selectedFile,
React.FC<FileContainerSecondaryProps> selectedFileViewSecondary
> = { }) => {
'chart-edit': () => null, const Component = useMemo(() => {
'sql-edit': () => null, if (!selectedFile || !selectedFileViewSecondary) return null;
'version-history': () => null
};
const DashboardSecondaryRecord: Record< const assosciatedType = SelectedFileSecondaryRecord[selectedFile?.type];
DashboardFileViewSecondary,
React.FC<FileContainerSecondaryProps>
> = {
'version-history': () => null
};
const SelectedFileSecondaryRecord: Record< if (!assosciatedType) return null;
FileType,
Record<string, React.FC<FileContainerSecondaryProps>> return assosciatedType[selectedFileViewSecondary];
> = { }, [selectedFileViewSecondary, selectedFile?.id, selectedFile?.type]);
metric: MetricSecondaryRecord,
dashboard: DashboardSecondaryRecord, return (
reasoning: {} <>
{Component && (
<Component
selectedFile={selectedFile}
selectedFileViewSecondary={selectedFileViewSecondary}
/>
)}
</>
);
}; };

View File

@ -1,6 +1,7 @@
import type { FileViewSecondary } from '@/layouts/ChatLayout/ChatLayoutContext/useLayoutConfig'; import type { FileViewSecondary } from '@/layouts/ChatLayout/ChatLayoutContext/useLayoutConfig';
import { SelectedFile } from '../../interfaces';
export type FileContainerSecondaryProps = { export type FileContainerSecondaryProps = {
selectedFileId: string | undefined;
selectedFileViewSecondary: FileViewSecondary | undefined; selectedFileViewSecondary: FileViewSecondary | undefined;
selectedFile: SelectedFile | null;
}; };

View File

@ -15,7 +15,7 @@ export const DashboardSecondaryRecord: Record<
DashboardFileViewSecondary, DashboardFileViewSecondary,
React.FC<FileContainerSecondaryProps> React.FC<FileContainerSecondaryProps>
> = { > = {
'version-history': ({ selectedFileId }) => ( 'version-history': ({ selectedFile }) => (
<VersionHistoryPanel assetId={selectedFileId || ''} type="dashboard" /> <VersionHistoryPanel assetId={selectedFile?.id || ''} type="dashboard" />
) )
}; };

View File

@ -2,7 +2,7 @@ import { CircleSpinnerLoaderContainer } from '@/components/ui/loaders/CircleSpin
export const loading = () => { export const loading = () => {
return ( return (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center overflow-hidden">
<CircleSpinnerLoaderContainer /> <CircleSpinnerLoaderContainer />
</div> </div>
); );

View File

@ -26,9 +26,9 @@ export const MetricSecondaryRecord: Record<
MetricFileViewSecondary, MetricFileViewSecondary,
React.FC<FileContainerSecondaryProps> React.FC<FileContainerSecondaryProps>
> = { > = {
'chart-edit': ({ selectedFileId }) => <MetricEditController metricId={selectedFileId || ''} />, 'chart-edit': ({ selectedFile }) => <MetricEditController metricId={selectedFile?.id || ''} />,
'sql-edit': ({ selectedFileId }) => <MetricViewResults metricId={selectedFileId || ''} />, 'sql-edit': ({ selectedFile }) => <MetricViewResults metricId={selectedFile?.id || ''} />,
'version-history': ({ selectedFileId }) => ( 'version-history': ({ selectedFile }) => (
<VersionHistoryPanel assetId={selectedFileId || ''} type="metric" /> <VersionHistoryPanel assetId={selectedFile?.id || ''} type="metric" />
) )
}; };