diff --git a/web/src/api/buster_rest/chats/queryRequests.test.tsx b/web/src/api/buster_rest/chats/queryRequests.test.tsx index fbe4f20db..cb74cde12 100644 --- a/web/src/api/buster_rest/chats/queryRequests.test.tsx +++ b/web/src/api/buster_rest/chats/queryRequests.test.tsx @@ -93,7 +93,7 @@ describe('Chat Query Hooks', () => { expect(requests.getListChats).toHaveBeenCalledWith({ admin_view: false, page_token: 0, - page_size: 3000, + page_size: 3500, search: 'test' }); }); diff --git a/web/src/api/buster_rest/chats/queryRequests.ts b/web/src/api/buster_rest/chats/queryRequests.ts index 1863df046..4e8bacc3f 100644 --- a/web/src/api/buster_rest/chats/queryRequests.ts +++ b/web/src/api/buster_rest/chats/queryRequests.ts @@ -37,14 +37,14 @@ export const useGetListChats = ( filters?: Omit[0], 'page_token' | 'page_size'> ) => { const filtersCompiled: Parameters[0] = useMemo( - () => ({ admin_view: false, page_token: 0, page_size: 3000, ...filters }), + () => ({ admin_view: false, page_token: 0, page_size: 3500, ...filters }), [filters] ); const queryFn = useMemoizedFn(() => getListChats(filtersCompiled)); return useQuery({ - ...chatQueryKeys.chatsGetList(filters), + ...chatQueryKeys.chatsGetList(filtersCompiled), queryFn }); }; @@ -67,14 +67,14 @@ export const useGetListLogs = ( filters?: Omit[0], 'page_token' | 'page_size'> ) => { const filtersCompiled: Parameters[0] = useMemo( - () => ({ page_token: 0, page_size: 3000, ...filters }), + () => ({ page_token: 0, page_size: 3500, ...filters }), [filters] ); const queryFn = useMemoizedFn(() => getListLogs(filtersCompiled)); return useQuery({ - ...chatQueryKeys.logsGetList(filters), + ...chatQueryKeys.logsGetList(filtersCompiled), queryFn }); }; diff --git a/web/src/api/buster_rest/chats/requests.test.ts b/web/src/api/buster_rest/chats/requests.test.ts index 1b0626aad..3c4cae677 100644 --- a/web/src/api/buster_rest/chats/requests.test.ts +++ b/web/src/api/buster_rest/chats/requests.test.ts @@ -44,7 +44,7 @@ describe('Chat API Requests', () => { // Verify the API was called with correct parameters expect(mainApi.get).toHaveBeenCalledWith('/chats', { - params: { page_token: 0, page_size: 3000 } + params: { page_token: 0, page_size: 3500 } }); // Verify the result matches the mock data diff --git a/web/src/api/buster_rest/chats/requests.ts b/web/src/api/buster_rest/chats/requests.ts index cbf8bb693..0b7bc3796 100644 --- a/web/src/api/buster_rest/chats/requests.ts +++ b/web/src/api/buster_rest/chats/requests.ts @@ -9,7 +9,7 @@ export const getListChats = async (params?: { page_token: number; page_size: number; }): Promise => { - const { page_token = 0, page_size = 3000 } = params || {}; + const { page_token = 0, page_size = 3500 } = params || {}; return mainApi .get(`${CHATS_BASE}`, { params: { page_token, page_size } @@ -20,7 +20,7 @@ export const getListChats = async (params?: { export const getListLogs = async ( params?: Parameters[0] ): Promise => { - const { page_token = 0, page_size = 3000 } = params || {}; + const { page_token = 0, page_size = 3500 } = params || {}; return mainApi .get(`/logs`, { params: { page_token, page_size } diff --git a/web/src/api/buster_rest/collections/queryRequests.ts b/web/src/api/buster_rest/collections/queryRequests.ts index 7e18f1af0..6ad7d2f4b 100644 --- a/web/src/api/buster_rest/collections/queryRequests.ts +++ b/web/src/api/buster_rest/collections/queryRequests.ts @@ -1,4 +1,10 @@ -import { useMutation, useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'; +import { + QueryClient, + useMutation, + useQuery, + useQueryClient, + UseQueryOptions +} from '@tanstack/react-query'; import { collectionQueryKeys } from '@/api/query_keys/collection'; import { collectionsGetList, @@ -19,25 +25,45 @@ import { useBusterAssetsContextSelector } from '@/context/Assets/BusterAssetsPro import { create } from 'mutative'; import type { BusterCollection } from '@/api/asset_interfaces/collection'; import { RustApiError } from '../errors'; +import { isQueryStale } from '@/lib'; export const useGetCollectionsList = ( - filters: Omit[0], 'page' | 'page_size'>, + filters: Omit[0], 'page_token' | 'page_size'>, options?: Omit< UseQueryOptions>, RustApiError>, 'queryKey' | 'queryFn' | 'initialData' > ) => { const payload = useMemo(() => { - return { page: 0, page_size: 3000, ...filters }; + return { ...filters, page_token: 0, page_size: 3500 }; }, [filters]); return useQuery({ - ...collectionQueryKeys.collectionsGetList(filters), + ...collectionQueryKeys.collectionsGetList(payload), queryFn: () => collectionsGetList(payload), ...options }); }; +export const prefetchGetCollectionsList = async ( + queryClient: QueryClient, + params?: Parameters[0] +) => { + const options = collectionQueryKeys.collectionsGetList(params); + const isStale = isQueryStale(options, queryClient); + if (!isStale) return queryClient; + + const lastQueryKey = options.queryKey[options.queryKey.length - 1]; + const compiledParams = lastQueryKey as Parameters[0]; + + await queryClient.prefetchQuery({ + ...options, + queryFn: () => collectionsGetList(compiledParams) + }); + + return queryClient; +}; + const useFetchCollection = () => { const getAssetPassword = useBusterAssetsContextSelector((state) => state.getAssetPassword); diff --git a/web/src/api/buster_rest/collections/requests.ts b/web/src/api/buster_rest/collections/requests.ts index b907a76f9..cda390ed2 100644 --- a/web/src/api/buster_rest/collections/requests.ts +++ b/web/src/api/buster_rest/collections/requests.ts @@ -1,4 +1,3 @@ -import { ShareRole } from '@/api/asset_interfaces'; import type { BusterCollection, BusterCollectionListItem } from '@/api/asset_interfaces/collection'; import { ShareDeleteRequest, @@ -10,7 +9,7 @@ import type { ShareAssetType } from '@/api/asset_interfaces'; export const collectionsGetList = async (params: { /** Current page number (1-based indexing) */ - page: number; + page_token: number; /** Number of items to display per page */ page_size: number; /** When true, returns only collections shared with the current user */ diff --git a/web/src/api/buster_rest/dashboards/queryRequests.ts b/web/src/api/buster_rest/dashboards/queryRequests.ts index b0cd1a6d2..4fec799ef 100644 --- a/web/src/api/buster_rest/dashboards/queryRequests.ts +++ b/web/src/api/buster_rest/dashboards/queryRequests.ts @@ -1,4 +1,10 @@ -import { useMutation, useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'; +import { + QueryClient, + useMutation, + useQuery, + useQueryClient, + UseQueryOptions +} from '@tanstack/react-query'; import { dashboardsGetList, dashboardsCreateDashboard, @@ -37,6 +43,7 @@ import { useGetLatestMetricVersionMemoized } from '../metrics'; import { useBusterAssetsContextSelector } from '@/context/Assets/BusterAssetsProvider'; import last from 'lodash/last'; import { createDashboardFullConfirmModal } from './confirmModals'; +import { isQueryStale } from '@/lib'; export const useGetDashboard = ( { @@ -700,13 +707,29 @@ export const useGetDashboardsList = ( return { ...params, page_token: 0, - page_size: 3000 + page_size: 3500 }; }, [params]); return useQuery({ - ...dashboardQueryKeys.dashboardGetList(params), + ...dashboardQueryKeys.dashboardGetList(filters), queryFn: () => dashboardsGetList(filters), ...options }); }; + +export const prefetchGetDashboardsList = async ( + queryClient: QueryClient, + params?: Parameters[0] +) => { + const options = dashboardQueryKeys.dashboardGetList(params); + const isStale = isQueryStale(options, queryClient); + if (!isStale) return queryClient; + + const lastQueryKey = options.queryKey[options.queryKey.length - 1]; + const compiledParams = lastQueryKey as Parameters[0]; + + await queryClient.prefetchQuery({ ...options, queryFn: () => dashboardsGetList(compiledParams) }); + + return queryClient; +}; diff --git a/web/src/api/buster_rest/metrics/getMetricListQueryRequests.ts b/web/src/api/buster_rest/metrics/getMetricListQueryRequests.ts index 376236583..fa529fa64 100644 --- a/web/src/api/buster_rest/metrics/getMetricListQueryRequests.ts +++ b/web/src/api/buster_rest/metrics/getMetricListQueryRequests.ts @@ -1,9 +1,10 @@ import { useMemo } from 'react'; import { listMetrics } from './requests'; -import { useQuery, UseQueryOptions } from '@tanstack/react-query'; +import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query'; import { useMemoizedFn } from '@/hooks'; import { metricsQueryKeys } from '@/api/query_keys/metric'; import { RustApiError } from '../errors'; +import { isQueryStale } from '@/lib'; export const useGetMetricsList = ( params: Omit[0], 'page_token' | 'page_size'>, @@ -13,16 +14,36 @@ export const useGetMetricsList = ( > ) => { const compiledParams: Parameters[0] = useMemo( - () => ({ ...params, page_token: 0, page_size: 3500 }), + () => ({ status: [], ...params, page_token: 0, page_size: 3500 }), [params] ); const queryFn = useMemoizedFn(() => listMetrics(compiledParams)); return useQuery({ - ...metricsQueryKeys.metricsGetList(params), + ...metricsQueryKeys.metricsGetList(compiledParams), queryFn, select: options?.select, ...options }); }; + +export const prefetchGetMetricsList = async ( + queryClient: QueryClient, + params?: Parameters[0] +) => { + const options = metricsQueryKeys.metricsGetList(params); + const isStale = isQueryStale(options, queryClient); + if (!isStale) return queryClient; + + const lastQueryKey = options.queryKey[options.queryKey.length - 1]; + const compiledParams = lastQueryKey as Parameters[0]; + + await queryClient.prefetchQuery({ + ...options, + queryFn: async () => { + return await listMetrics(compiledParams); + } + }); + return queryClient; +}; diff --git a/web/src/api/buster_rest/terms/queryRequests.ts b/web/src/api/buster_rest/terms/queryRequests.ts index 76bf44194..784a19581 100644 --- a/web/src/api/buster_rest/terms/queryRequests.ts +++ b/web/src/api/buster_rest/terms/queryRequests.ts @@ -7,10 +7,10 @@ import { useMemoizedFn } from '@/hooks'; import { useBusterNotifications } from '@/context/BusterNotifications'; export const useGetTermsList = ( - params?: Omit[0], 'page' | 'page_size'> + params?: Omit[0], 'page_token' | 'page_size'> ) => { const compiledParams: Parameters[0] = useMemo( - () => ({ page: 0, page_size: 3000, ...params }), + () => ({ page_token: 0, page_size: 3500, ...params }), [params] ); diff --git a/web/src/api/buster_rest/terms/requests.ts b/web/src/api/buster_rest/terms/requests.ts index 3ed8b68ef..a52f87a35 100644 --- a/web/src/api/buster_rest/terms/requests.ts +++ b/web/src/api/buster_rest/terms/requests.ts @@ -3,7 +3,7 @@ import { BusterTerm, BusterTermListItem } from '@/api/asset_interfaces/terms'; export const getTermsList = async (params: { /** The page number to retrieve */ - page: number; + page_token: number; /** The number of items per page */ page_size: number; }) => { diff --git a/web/src/api/query_keys/chat.ts b/web/src/api/query_keys/chat.ts index 1195bb882..9189dc8f3 100644 --- a/web/src/api/query_keys/chat.ts +++ b/web/src/api/query_keys/chat.ts @@ -29,7 +29,7 @@ const chatsGetList = ( filters?: Omit[0], 'page_token' | 'page_size'> ) => queryOptions({ - queryKey: ['chats', 'list', filters || {}] as const, + queryKey: ['chats', 'list', filters || { page_token: 0, page_size: 3500 }] as const, staleTime: 60 * 1000, // 1 minute initialData: [], initialDataUpdatedAt: 0 @@ -47,7 +47,7 @@ const logsGetList = ( filters?: Omit[0], 'page_token' | 'page_size'> ) => queryOptions({ - queryKey: ['logs', 'list', filters || {}] as const, + queryKey: ['logs', 'list', filters || { page_token: 0, page_size: 3500 }] as const, staleTime: 60 * 1000, // 1 minute initialData: [], initialDataUpdatedAt: 0 diff --git a/web/src/api/query_keys/collection.ts b/web/src/api/query_keys/collection.ts index 09489fc76..06aee99a6 100644 --- a/web/src/api/query_keys/collection.ts +++ b/web/src/api/query_keys/collection.ts @@ -3,10 +3,10 @@ import type { BusterCollectionListItem, BusterCollection } from '@/api/asset_int import { collectionsGetList as collectionsGetListRequest } from '@/api/buster_rest/collections/requests'; const collectionsGetList = ( - filters?: Omit[0], 'page' | 'page_size'> + filters?: Omit[0], 'page_token' | 'page_size'> ) => queryOptions({ - queryKey: ['collections', 'list', filters || {}] as const, + queryKey: ['collections', 'list', filters || { page_token: 0, page_size: 3500 }] as const, staleTime: 60 * 1000, initialData: [], initialDataUpdatedAt: 0 diff --git a/web/src/api/query_keys/dashboard.ts b/web/src/api/query_keys/dashboard.ts index 3b5d3aebd..d58a5d5f3 100644 --- a/web/src/api/query_keys/dashboard.ts +++ b/web/src/api/query_keys/dashboard.ts @@ -9,7 +9,7 @@ const dashboardGetList = ( filters?: Omit[0], 'page_token' | 'page_size'> ) => queryOptions({ - queryKey: ['dashboard', 'list', filters || {}] as const, + queryKey: ['dashboard', 'list', filters || { page_token: 0, page_size: 3500 }] as const, initialData: [], staleTime: 60 * 1000, initialDataUpdatedAt: 0 diff --git a/web/src/api/query_keys/metric.ts b/web/src/api/query_keys/metric.ts index f41a64413..05d6d85bb 100644 --- a/web/src/api/query_keys/metric.ts +++ b/web/src/api/query_keys/metric.ts @@ -17,7 +17,11 @@ export const metricsGetList = ( filters?: Omit[0], 'page_token' | 'page_size'> ) => queryOptions({ - queryKey: ['metrics', 'list', filters || { page_token: 0, page_size: 3500 }] as const, + queryKey: [ + 'metrics', + 'list', + filters || { status: [], page_token: 0, page_size: 3500 } + ] as const, initialData: [], initialDataUpdatedAt: 0 }); diff --git a/web/src/components/features/dropdowns/SaveToCollectionsDropdown.tsx b/web/src/components/features/dropdowns/SaveToCollectionsDropdown.tsx index 762383616..ba3dc7a21 100644 --- a/web/src/components/features/dropdowns/SaveToCollectionsDropdown.tsx +++ b/web/src/components/features/dropdowns/SaveToCollectionsDropdown.tsx @@ -1,5 +1,4 @@ import { Dropdown, type DropdownItem, type DropdownProps } from '@/components/ui/dropdown'; -import { AppTooltip } from '@/components/ui/tooltip'; import { useAppLayoutContextSelector } from '@/context/BusterAppLayout'; import { BusterRoutes, createBusterRoute } from '@/routes/busterRoutes'; import { useMemoizedFn } from '@/hooks'; diff --git a/web/src/context/RoutePrefetcher.tsx b/web/src/context/RoutePrefetcher.tsx index 791c500dc..f85eb9048 100644 --- a/web/src/context/RoutePrefetcher.tsx +++ b/web/src/context/RoutePrefetcher.tsx @@ -6,6 +6,10 @@ import { BusterRoutes, createBusterRoute } from '@/routes'; import { BusterAppRoutes } from '@/routes/busterRoutes/busterAppRoutes'; import { useAsyncEffect } from '@/hooks'; import { timeout } from '@/lib'; +import { prefetchGetMetricsList } from '@/api/buster_rest/metrics'; +import { QueryClient, useQueryClient } from '@tanstack/react-query'; +import { prefetchGetDashboardsList } from '@/api/buster_rest/dashboards'; +import { prefetchGetCollectionsList } from '@/api/buster_rest/collections'; const HIGH_PRIORITY_ROUTES = [ BusterRoutes.APP_HOME, @@ -25,14 +29,25 @@ const LOW_PRIORITY_ROUTES = [ BusterRoutes.APP_CHAT_ID_METRIC_ID_CHART ]; +const LOW_PRIORITY_PREFETCH: ((queryClient: QueryClient) => Promise)[] = [ + (queryClient) => prefetchGetMetricsList(queryClient), + (queryClient) => prefetchGetDashboardsList(queryClient), + (queryClient) => prefetchGetCollectionsList(queryClient) +]; + export const RoutePrefetcher: React.FC<{}> = React.memo(() => { const router = useRouter(); + const queryClient = useQueryClient(); const debounceTimerRef = useRef(null); const isPreFetchedHighPriorityRef = useRef(false); const isPreFetchedLowPriorityRef = useRef(false); useAsyncEffect(async () => { - const prefetchRoutes = (routes: BusterRoutes[], priority: 'high' | 'low') => { + const prefetchRoutes = ( + routes: BusterRoutes[], + prefetchFns: typeof LOW_PRIORITY_PREFETCH, + priority: 'high' | 'low' + ) => { if (priority === 'high' && isPreFetchedHighPriorityRef.current) return; if (priority === 'low' && isPreFetchedLowPriorityRef.current) return; @@ -41,6 +56,10 @@ export const RoutePrefetcher: React.FC<{}> = React.memo(() => { router.prefetch(path); }); + prefetchFns.forEach((prefetchFn) => { + prefetchFn(queryClient); + }); + if (priority === 'high') { isPreFetchedHighPriorityRef.current = true; } else { @@ -49,7 +68,7 @@ export const RoutePrefetcher: React.FC<{}> = React.memo(() => { }; if (!isPreFetchedHighPriorityRef.current) { - prefetchRoutes(HIGH_PRIORITY_ROUTES, 'high'); + prefetchRoutes(HIGH_PRIORITY_ROUTES, [], 'high'); } // Wait for page load @@ -72,7 +91,7 @@ export const RoutePrefetcher: React.FC<{}> = React.memo(() => { // Set a new debounce timer - will trigger if no network activity for 1500ms debounceTimerRef.current = setTimeout(() => { - prefetchRoutes(LOW_PRIORITY_ROUTES, 'low'); + prefetchRoutes(LOW_PRIORITY_ROUTES, LOW_PRIORITY_PREFETCH, 'low'); observer.disconnect(); }, 1000); }); @@ -82,7 +101,7 @@ export const RoutePrefetcher: React.FC<{}> = React.memo(() => { // Fallback - ensure prefetch happens even if network is already quiet fallbackTimer = setTimeout(() => { - prefetchRoutes(LOW_PRIORITY_ROUTES, 'low'); + prefetchRoutes(LOW_PRIORITY_ROUTES, LOW_PRIORITY_PREFETCH, 'low'); observer.disconnect(); }, 3000); } catch (error) { @@ -92,7 +111,7 @@ export const RoutePrefetcher: React.FC<{}> = React.memo(() => { clearTimeout(debounceTimerRef.current); } // Still prefetch low priority routes as fallback - prefetchRoutes(LOW_PRIORITY_ROUTES, 'low'); + prefetchRoutes(LOW_PRIORITY_ROUTES, LOW_PRIORITY_PREFETCH, 'low'); } return () => { diff --git a/web/src/controllers/CollectionListController/CollectionListController.tsx b/web/src/controllers/CollectionListController/CollectionListController.tsx index 07e9bce3f..8f25d18b2 100644 --- a/web/src/controllers/CollectionListController/CollectionListController.tsx +++ b/web/src/controllers/CollectionListController/CollectionListController.tsx @@ -10,7 +10,7 @@ import { collectionsGetList } from '@/api/buster_rest/collections/requests'; export const CollectionListController: React.FC = () => { const [openNewCollectionModal, setOpenNewCollectionModal] = useState(false); const [collectionListFilters, setCollectionListFilters] = useState< - Omit[0], 'page' | 'page_size'> + Omit[0], 'page_token' | 'page_size'> >({}); const { data: collectionsList, isFetched: isCollectionListFetched } = diff --git a/web/src/controllers/CollectionListController/CollectionListHeader.tsx b/web/src/controllers/CollectionListController/CollectionListHeader.tsx index 742362feb..63e9bd39a 100644 --- a/web/src/controllers/CollectionListController/CollectionListHeader.tsx +++ b/web/src/controllers/CollectionListController/CollectionListHeader.tsx @@ -16,7 +16,10 @@ import { BusterCollectionListItem } from '@/api/asset_interfaces/collection'; import { useGetCollection } from '@/api/buster_rest/collections'; import { collectionsGetList } from '@/api/buster_rest/collections/requests'; -type CollectionListFilters = Omit[0], 'page' | 'page_size'>; +type CollectionListFilters = Omit< + Parameters[0], + 'page_token' | 'page_size' +>; type SetCollectionListFilters = (filters: CollectionListFilters) => void; export const CollectionListHeader: React.FC<{ diff --git a/web/src/lib/index.ts b/web/src/lib/index.ts index 1ebec81c5..b66621bab 100644 --- a/web/src/lib/index.ts +++ b/web/src/lib/index.ts @@ -13,3 +13,4 @@ export * from './columnFormatter'; export * from './colors'; export * from './regression'; export * from './canvas'; +export * from './query'; diff --git a/web/src/lib/query.ts b/web/src/lib/query.ts new file mode 100644 index 000000000..ded0fb40c --- /dev/null +++ b/web/src/lib/query.ts @@ -0,0 +1,17 @@ +import { RustApiError } from '@/api/buster_rest/errors'; +import { QueryClient, queryOptions } from '@tanstack/react-query'; + +export const isQueryStale = ( + options: ReturnType>, + queryClient: QueryClient +): boolean => { + const queryState = queryClient.getQueryState(options.queryKey); + const updatedAt = queryState?.dataUpdatedAt; + const staleTime = + (options.staleTime as number) || + (queryClient.getDefaultOptions().queries?.staleTime as number) || + 0; + const isStale = updatedAt ? Date.now() - updatedAt > staleTime : true; + + return isStale; +};