diff --git a/apps/web/src/api/buster_rest/reports/index.ts b/apps/web/src/api/buster_rest/reports/index.ts new file mode 100644 index 000000000..a2a03169a --- /dev/null +++ b/apps/web/src/api/buster_rest/reports/index.ts @@ -0,0 +1,3 @@ +// Export all reports-related functionality +export * from './requests'; +export * from './queryRequests'; diff --git a/apps/web/src/api/buster_rest/reports/queryRequests.ts b/apps/web/src/api/buster_rest/reports/queryRequests.ts new file mode 100644 index 000000000..83a508f18 --- /dev/null +++ b/apps/web/src/api/buster_rest/reports/queryRequests.ts @@ -0,0 +1,163 @@ +import { + QueryClient, + type UseQueryOptions, + useMutation, + useQuery, + useQueryClient +} from '@tanstack/react-query'; +import { create } from 'mutative'; +import { useMemoizedFn } from '@/hooks'; +import { queryKeys } from '@/api/query_keys'; +import type { RustApiError } from '../errors'; +import type { + GetReportsListResponse, + GetReportIndividualResponse, + UpdateReportRequest, + UpdateReportResponse +} from '@buster/server-shared/reports'; +import { + getReportsList, + getReportsList_server, + getReportById, + getReportById_server, + updateReport +} from './requests'; + +/** + * Hook to get a list of reports + */ +export const useGetReportsList = (params?: Parameters[0]) => { + const queryFn = useMemoizedFn(() => { + return getReportsList(params); + }); + + const res = useQuery({ + ...queryKeys.reportsGetList(params), + queryFn + }); + + return { + ...res, + data: res.data || { + data: [], + pagination: { page: 1, page_size: 5000, total: 0, total_pages: 0 } + } + }; +}; + +/** + * Prefetch function for reports list (server-side) + */ +export const prefetchGetReportsList = async ( + params?: Parameters[0], + queryClientProp?: QueryClient +) => { + const queryClient = queryClientProp || new QueryClient(); + + await queryClient.prefetchQuery({ + ...queryKeys.reportsGetList(params), + queryFn: () => getReportsList_server(params) + }); + + return queryClient; +}; + +/** + * Hook to get an individual report by ID + */ +export const useGetReportById = ( + reportId: string, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + const queryFn = useMemoizedFn(() => { + return getReportById(reportId); + }); + + return useQuery({ + ...queryKeys.reportsGetById(reportId), + queryFn, + enabled: !!reportId, + ...options + }); +}; + +/** + * Prefetch function for individual report (server-side) + */ +export const prefetchGetReportById = async (reportId: string, queryClientProp?: QueryClient) => { + const queryClient = queryClientProp || new QueryClient(); + + await queryClient.prefetchQuery({ + ...queryKeys.reportsGetById(reportId), + queryFn: () => getReportById_server(reportId) + }); + + return queryClient; +}; + +/** + * Hook to update a report + */ +export const useUpdateReport = () => { + const queryClient = useQueryClient(); + + return useMutation< + UpdateReportResponse, + RustApiError, + { reportId: string; data: UpdateReportRequest }, + { previousReport?: GetReportIndividualResponse } + >({ + mutationFn: ({ reportId, data }) => updateReport(reportId, data), + onMutate: async ({ reportId, data }) => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ + queryKey: queryKeys.reportsGetById(reportId).queryKey + }); + + // Snapshot the previous value + const previousReport = queryClient.getQueryData( + queryKeys.reportsGetById(reportId).queryKey + ); + + // Optimistically update the individual report + if (previousReport) { + queryClient.setQueryData( + queryKeys.reportsGetById(reportId).queryKey, + create(previousReport, (draft) => { + if (data.name !== undefined) draft.name = data.name; + if (data.description !== undefined) draft.description = data.description; + if (data.publicly_accessible !== undefined) + draft.publicly_accessible = data.publicly_accessible; + if (data.content !== undefined) draft.content = data.content; + }) + ); + } + + // Return context with previous values + return { previousReport }; + }, + onError: (_err, { reportId }, context) => { + // If the mutation fails, use the context to roll back + if (context?.previousReport) { + queryClient.setQueryData( + queryKeys.reportsGetById(reportId).queryKey, + context.previousReport + ); + } + }, + onSuccess: (data, { reportId, data: updateData }) => { + // Update the individual report cache with server response + queryClient.setQueryData(queryKeys.reportsGetById(reportId).queryKey, data); + + const nameChanged = updateData.name !== undefined && updateData.name !== data.name; + + // Invalidate the list cache to ensure it's fresh + if (nameChanged) { + queryClient.invalidateQueries({ + queryKey: queryKeys.reportsGetList().queryKey, + refetchType: 'all' + }); + } + } + }); +}; diff --git a/apps/web/src/api/buster_rest/reports/requests.ts b/apps/web/src/api/buster_rest/reports/requests.ts new file mode 100644 index 000000000..e3c0e5831 --- /dev/null +++ b/apps/web/src/api/buster_rest/reports/requests.ts @@ -0,0 +1,55 @@ +import { mainApiV2 } from '../instances'; +import { serverFetch } from '@/api/createServerInstance'; +import { BASE_URL_V2 } from '../config'; +import type { + GetReportsListRequest, + GetReportsListResponse, + GetReportIndividualResponse, + UpdateReportRequest, + UpdateReportResponse +} from '@buster/server-shared/reports'; + +/** + * Get a list of reports with optional filters + */ +export const getReportsList = async (params?: GetReportsListRequest) => { + const { page = 1, page_size = 5000, ...allParams } = params || {}; + return mainApiV2 + .get('/reports', { params: { page, page_size, ...allParams } }) + .then((res) => res.data); +}; + +/** + * Server-side version of getReportsList + */ +export const getReportsList_server = async (params?: Parameters[0]) => { + const { page = 1, page_size = 5000, ...allParams } = params || {}; + return await serverFetch('/reports', { + baseURL: BASE_URL_V2, + params: { page, page_size, ...allParams } + }); +}; + +/** + * Get an individual report by ID + */ +export const getReportById = async (reportId: string) => { + return mainApiV2.get(`/reports/${reportId}`).then((res) => res.data); +}; + +/** + * Server-side version of getReportById + */ +export const getReportById_server = async (reportId: string) => { + return await serverFetch(`/reports/${reportId}`, { + baseURL: BASE_URL_V2, + method: 'GET' + }); +}; + +/** + * Update a report + */ +export const updateReport = async (reportId: string, data: UpdateReportRequest) => { + return mainApiV2.put(`/reports/${reportId}`, data).then((res) => res.data); +}; diff --git a/apps/web/src/api/query_keys/index.ts b/apps/web/src/api/query_keys/index.ts index d5f983e0a..a535bd464 100644 --- a/apps/web/src/api/query_keys/index.ts +++ b/apps/web/src/api/query_keys/index.ts @@ -12,6 +12,7 @@ import { userQueryKeys } from './users'; import { securityQueryKeys } from './security'; import { slackQueryKeys } from './slack'; import { dictionariesQueryKeys } from './dictionaries'; +import { reportsQueryKeys } from './reports'; export const queryKeys = { ...datasetQueryKeys, @@ -27,5 +28,6 @@ export const queryKeys = { ...permissionGroupQueryKeys, ...securityQueryKeys, ...slackQueryKeys, - ...dictionariesQueryKeys + ...dictionariesQueryKeys, + ...reportsQueryKeys }; diff --git a/apps/web/src/api/query_keys/reports.ts b/apps/web/src/api/query_keys/reports.ts new file mode 100644 index 000000000..3d0be8178 --- /dev/null +++ b/apps/web/src/api/query_keys/reports.ts @@ -0,0 +1,25 @@ +import { queryOptions } from '@tanstack/react-query'; +import type { + GetReportsListResponse, + GetReportIndividualResponse, + GetReportsListRequest +} from '@buster/server-shared/reports'; + +const reportsGetList = (filters?: GetReportsListRequest) => + queryOptions({ + queryKey: ['reports', 'list', filters || { page: 1, page_size: 5000 }] as const, + staleTime: 10 * 1000, // 10 seconds + initialData: { data: [], pagination: { page: 1, page_size: 5000, total: 0, total_pages: 0 } }, + initialDataUpdatedAt: 0 + }); + +const reportsGetById = (reportId: string) => + queryOptions({ + queryKey: ['reports', 'get', reportId] as const, + staleTime: 60 * 1000 // 60 seconds + }); + +export const reportsQueryKeys = { + reportsGetList, + reportsGetById +}; diff --git a/apps/web/src/context/BusterReactQuery/createPersister.ts b/apps/web/src/context/BusterReactQuery/createPersister.ts index 14afe9064..81c7b051c 100644 --- a/apps/web/src/context/BusterReactQuery/createPersister.ts +++ b/apps/web/src/context/BusterReactQuery/createPersister.ts @@ -42,7 +42,8 @@ export const persistOptions: PersistQueryClientProviderProps['persistOptions'] = maxAge: PERSIST_TIME, dehydrateOptions: { shouldDehydrateQuery: (query) => { - const isList = query.queryKey[1] === 'list'; + const isList = + query.queryKey[1] === 'list' || query.queryKey[query.queryKey.length - 1] === 'list'; return isList || ALL_PERSISTED_QUERIES.includes(query.queryHash); } },