diff --git a/packages/react-query-devtools/src/CachePanel/ActiveQuery.tsx b/packages/react-query-devtools/src/CachePanel/ActiveQuery.tsx new file mode 100644 index 0000000000..477f96e366 --- /dev/null +++ b/packages/react-query-devtools/src/CachePanel/ActiveQuery.tsx @@ -0,0 +1,380 @@ +import React from 'react' +import type { QueryCache, QueryClient } from '@tanstack/react-query' + +import useSubscribeToQueryCache from '../useSubscribeToQueryCache' +import { Button, Code, Select, ActiveQueryPanel } from '../styledComponents' + +import { + getQueryStatusLabel, + getQueryStatusColor, + displayValue, +} from '../utils' +import Explorer from '../Explorer' +import type { DevToolsErrorType } from '../types' +import { defaultTheme as theme } from '../theme' + +// eslint-disable-next-line @typescript-eslint/no-empty-function +function noop() {} + +/** + * Panel for the query currently being inspected + * + * It displays query details (key, observers...), query actions, + * the data explorer and the query explorer + */ +const ActiveQuery = ({ + queryCache, + activeQueryHash, + queryClient, + errorTypes, +}: { + queryCache: QueryCache + activeQueryHash: string + queryClient: QueryClient + errorTypes: DevToolsErrorType[] +}) => { + const activeQuery = useSubscribeToQueryCache(queryCache, () => + queryCache.getAll().find((query) => query.queryHash === activeQueryHash), + ) + + const activeQueryState = useSubscribeToQueryCache( + queryCache, + () => + queryCache.getAll().find((query) => query.queryHash === activeQueryHash) + ?.state, + ) + + const isStale = + useSubscribeToQueryCache(queryCache, () => + queryCache + .getAll() + .find((query) => query.queryHash === activeQueryHash) + ?.isStale(), + ) ?? false + + const observerCount = + useSubscribeToQueryCache(queryCache, () => + queryCache + .getAll() + .find((query) => query.queryHash === activeQueryHash) + ?.getObserversCount(), + ) ?? 0 + + const handleRefetch = () => { + const promise = activeQuery?.fetch() + promise?.catch(noop) + } + + const currentErrorTypeName = React.useMemo(() => { + if (activeQuery && activeQueryState?.error) { + const errorType = errorTypes.find( + (type) => + type.initializer(activeQuery).toString() === + activeQueryState.error?.toString(), + ) + return errorType?.name + } + return undefined + }, [activeQuery, activeQueryState?.error, errorTypes]) + + if (!activeQuery || !activeQueryState) { + return null + } + + const triggerError = (errorType?: DevToolsErrorType) => { + const error = + errorType?.initializer(activeQuery) ?? + new Error('Unknown error from devtools') + + const __previousQueryOptions = activeQuery.options + + activeQuery.setState({ + status: 'error', + error, + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + fetchMeta: { + ...activeQuery.state.fetchMeta, + __previousQueryOptions, + } as any, + }) + } + + const restoreQueryAfterLoadingOrError = () => { + activeQuery.fetch( + (activeQuery.state.fetchMeta as any).__previousQueryOptions, + { + // Make sure this fetch will cancel the previous one + cancelRefetch: true, + }, + ) + } + + return ( + +
+ Query Details +
+
+
+ +
+              {displayValue(activeQuery.queryKey, true)}
+            
+
+ + {getQueryStatusLabel(activeQuery)} + +
+
+ Observers: {observerCount} +
+
+ Last Updated:{' '} + + {new Date(activeQueryState.dataUpdatedAt).toLocaleTimeString()} + +
+
+
+ Actions +
+
+ {' '} + {' '} + {' '} + {' '} + {' '} + {errorTypes.length === 0 || activeQuery.state.status === 'error' ? ( + + ) : ( + + )} +
+
+ Data Explorer +
+
+ +
+
+ Query Explorer +
+
+ +
+
+ ) +} + +ActiveQuery.displayName = 'ActiveQuery' + +export default ActiveQuery diff --git a/packages/react-query-devtools/src/CachePanel/CachePanel.tsx b/packages/react-query-devtools/src/CachePanel/CachePanel.tsx new file mode 100644 index 0000000000..a1655cebc5 --- /dev/null +++ b/packages/react-query-devtools/src/CachePanel/CachePanel.tsx @@ -0,0 +1,427 @@ +import React from 'react' +import type { QueryClient } from '@tanstack/react-query' +import { useQueryClient, onlineManager } from '@tanstack/react-query' +import { rankItem } from '@tanstack/match-sorter-utils' + +import { Panel, Button, Input, Select } from '../styledComponents' +import useSubscribeToQueryCache from '../useSubscribeToQueryCache' +import QueryStatusCount from './Header/QueryStatusCount' +import QueryRow from './QueryRow' +import ActiveQuery from './ActiveQuery' +import type { Side } from '../utils' +import { sortFns, getResizeHandleStyle, defaultPanelSize } from '../utils' +import { ThemeProvider, defaultTheme as theme } from '../theme' +import type { DevToolsErrorType } from '../types' +import useLocalStorage from '../useLocalStorage' +import Logo from '../Logo' +import ScreenReader from '../screenreader' + +interface DevtoolsPanelOptions { + /** + * The standard React style object used to style a component with inline styles + */ + style?: React.CSSProperties + /** + * The standard React className property used to style a component with classes + */ + className?: string + /** + * A boolean variable indicating whether the panel is open or closed + */ + isOpen?: boolean + /** + * nonce for style element for CSP + */ + styleNonce?: string + /** + * A function that toggles the open and close state of the panel + */ + setIsOpen: (isOpen: boolean) => void + /** + * Handles the opening and closing the devtools panel + */ + onDragStart: (e: React.MouseEvent) => void + /** + * The position of the React Query devtools panel. + * Defaults to 'bottom'. + */ + position?: Side + /** + * Handles the panel position select change + */ + onPositionChange?: (side: Side) => void + /** + * Show a close button inside the panel + */ + showCloseButton?: boolean + /** + * Use this to add props to the close button. For example, you can add className, style (merge and override default style), onClick (extend default handler), etc. + */ + closeButtonProps?: React.ComponentPropsWithoutRef<'button'> + /** + * Custom instance of QueryClient + */ + queryClient?: QueryClient + /** + * Use this so you can define custom errors that can be shown in the devtools. + */ + errorTypes?: DevToolsErrorType[] +} + +export const ReactQueryDevtoolsPanel = React.forwardRef< + HTMLDivElement, + DevtoolsPanelOptions +>(function ReactQueryDevtoolsPanel(props, ref): React.ReactElement { + const { + isOpen = true, + styleNonce, + setIsOpen, + queryClient, + onDragStart, + onPositionChange, + showCloseButton, + position, + closeButtonProps = {}, + errorTypes = [], + ...panelProps + } = props + + const { onClick: onCloseClick, ...otherCloseButtonProps } = closeButtonProps + + const client = useQueryClient(queryClient) + const queryCache = client.getQueryCache() + + const [sort, setSort] = useLocalStorage( + 'reactQueryDevtoolsSortFn', + Object.keys(sortFns)[0], + ) + + const [filter, setFilter] = useLocalStorage('reactQueryDevtoolsFilter', '') + + const [baseSort, setBaseSort] = useLocalStorage( + 'reactQueryDevtoolsBaseSort', + 1, + ) + + const sortFn = React.useMemo(() => sortFns[sort as string], [sort]) + + const queriesCount = useSubscribeToQueryCache( + queryCache, + () => queryCache.getAll().length, + !isOpen, + ) + + const [activeQueryHash, setActiveQueryHash] = useLocalStorage( + 'reactQueryDevtoolsActiveQueryHash', + '', + ) + + const queries = React.useMemo(() => { + const unsortedQueries = queryCache.getAll() + + if (queriesCount === 0) { + return [] + } + + const filtered = filter + ? unsortedQueries.filter( + (item) => rankItem(item.queryHash, filter).passed, + ) + : [...unsortedQueries] + + const sorted = sortFn + ? filtered.sort((a, b) => sortFn(a, b) * (baseSort as number)) + : filtered + + return sorted + }, [baseSort, sortFn, filter, queriesCount, queryCache]) + + const [isMockOffline, setMockOffline] = React.useState(false) + + return ( + + +