diff --git a/packages/redux-devtools-rtk-query-inspector-monitor/README.md b/packages/redux-devtools-rtk-query-inspector-monitor/README.md index 36e540b1..cef8dffc 100644 --- a/packages/redux-devtools-rtk-query-inspector-monitor/README.md +++ b/packages/redux-devtools-rtk-query-inspector-monitor/README.md @@ -73,6 +73,7 @@ See also - tags - subscriptions - api slice config + - api stats ## TODO diff --git a/packages/redux-devtools-rtk-query-inspector-monitor/src/RtkQueryInspector.tsx b/packages/redux-devtools-rtk-query-inspector-monitor/src/RtkQueryInspector.tsx index 0342696c..11a9fe3b 100644 --- a/packages/redux-devtools-rtk-query-inspector-monitor/src/RtkQueryInspector.tsx +++ b/packages/redux-devtools-rtk-query-inspector-monitor/src/RtkQueryInspector.tsx @@ -134,14 +134,8 @@ class RtkQueryInspector> extends Component< const currentTags = this.selectors.selectCurrentQueryTags(selectorsSource); - console.log('inspector', { - apiStates, - allVisibleQueries, - selectorsSource, - currentQueryInfo, - currentRtkApi, - currentTags, - }); + const currentApiStats = + this.selectors.selectApiStatsOfCurrentQuery(selectorsSource); return (
> extends Component< querySubscriptions={currentQuerySubscriptions} apiConfig={currentRtkApi?.config ?? null} isWideLayout={isWideLayout} + apiStats={currentApiStats} />
); diff --git a/packages/redux-devtools-rtk-query-inspector-monitor/src/components/QueryPreview.tsx b/packages/redux-devtools-rtk-query-inspector-monitor/src/components/QueryPreview.tsx index 3056d2d3..1f706dfb 100644 --- a/packages/redux-devtools-rtk-query-inspector-monitor/src/components/QueryPreview.tsx +++ b/packages/redux-devtools-rtk-query-inspector-monitor/src/components/QueryPreview.tsx @@ -8,7 +8,7 @@ import { } from '../types'; import { QueryPreviewHeader } from './QueryPreviewHeader'; import { QueryPreviewInfo } from './QueryPreviewInfo'; -import { QueryPreviewApiConfig } from './QueryPreviewApiConfig'; +import { QueryPreviewApi } from './QueryPreviewApi'; import { QueryPreviewSubscriptions } from './QueryPreviewSubscriptions'; import { QueryPreviewTags } from './QueryPreviewTags'; @@ -37,7 +37,7 @@ const tabs: ReadonlyArray = [ { label: 'api', value: QueryPreviewTabs.apiConfig, - component: QueryPreviewApiConfig, + component: QueryPreviewApi, }, ]; @@ -94,6 +94,7 @@ export class QueryPreview extends React.PureComponent { onTabChange, querySubscriptions, tags, + apiStats, } = this.props; const { component: TabComponent } = @@ -136,6 +137,7 @@ export class QueryPreview extends React.PureComponent { tags={tags} apiConfig={apiConfig} isWideLayout={isWideLayout} + apiStats={apiStats} /> ); diff --git a/packages/redux-devtools-rtk-query-inspector-monitor/src/components/QueryPreviewApi.tsx b/packages/redux-devtools-rtk-query-inspector-monitor/src/components/QueryPreviewApi.tsx new file mode 100644 index 00000000..b83cd3c1 --- /dev/null +++ b/packages/redux-devtools-rtk-query-inspector-monitor/src/components/QueryPreviewApi.tsx @@ -0,0 +1,37 @@ +import { createSelector } from '@reduxjs/toolkit'; +import React, { ReactNode, PureComponent } from 'react'; +import { QueryPreviewTabProps } from '../types'; +import { TreeView } from './TreeView'; + +interface TreeDisplayed { + config: QueryPreviewTabProps['apiConfig']; + stats: QueryPreviewTabProps['apiStats']; +} +export class QueryPreviewApi extends PureComponent { + selectData = createSelector( + [ + ({ apiConfig }: QueryPreviewTabProps) => apiConfig, + ({ apiStats }: QueryPreviewTabProps) => apiStats, + ], + (apiConfig, apiStats) => ({ config: apiConfig, stats: apiStats }) + ); + + render(): ReactNode { + const { queryInfo, isWideLayout, base16Theme, styling, invertTheme } = + this.props; + + if (!queryInfo) { + return null; + } + + return ( + + ); + } +} diff --git a/packages/redux-devtools-rtk-query-inspector-monitor/src/components/QueryPreviewApiConfig.tsx b/packages/redux-devtools-rtk-query-inspector-monitor/src/components/QueryPreviewApiConfig.tsx deleted file mode 100644 index 00226b38..00000000 --- a/packages/redux-devtools-rtk-query-inspector-monitor/src/components/QueryPreviewApiConfig.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React, { ReactNode, PureComponent } from 'react'; -import { QueryPreviewTabProps } from '../types'; -import { TreeView } from './TreeView'; - -export class QueryPreviewApiConfig extends PureComponent { - render(): ReactNode { - const { - queryInfo, - isWideLayout, - base16Theme, - styling, - invertTheme, - apiConfig, - } = this.props; - - if (!queryInfo) { - return null; - } - - return ( - - ); - } -} diff --git a/packages/redux-devtools-rtk-query-inspector-monitor/src/selectors.ts b/packages/redux-devtools-rtk-query-inspector-monitor/src/selectors.ts index 65f8225e..863174fd 100644 --- a/packages/redux-devtools-rtk-query-inspector-monitor/src/selectors.ts +++ b/packages/redux-devtools-rtk-query-inspector-monitor/src/selectors.ts @@ -1,6 +1,6 @@ import { Action, createSelector, Selector } from '@reduxjs/toolkit'; import { RtkQueryInspectorProps } from './RtkQueryInspector'; -import { QueryInfo, RtkQueryTag, SelectorsSource } from './types'; +import { ApiStats, QueryInfo, RtkQueryTag, SelectorsSource } from './types'; import { Comparator, queryComparators } from './utils/comparators'; import { FilterList, queryListFilters } from './utils/filters'; import { escapeRegExpSpecialCharacter } from './utils/regexp'; @@ -9,6 +9,7 @@ import { extractAllApiQueries, flipComparator, getQueryTagsOf, + generateApiStatsOfCurrentQuery, } from './utils/rtk-query'; type InspectorSelector = Selector, Output>; @@ -50,6 +51,7 @@ export interface InspectorSelectors { readonly selectorCurrentQueryInfo: InspectorSelector; readonly selectSearchQueryRegex: InspectorSelector; readonly selectCurrentQueryTags: InspectorSelector; + readonly selectApiStatsOfCurrentQuery: InspectorSelector; } export function createInspectorSelectors(): InspectorSelectors { @@ -135,6 +137,13 @@ export function createInspectorSelectors(): InspectorSelectors { (apiState, currentQueryInfo) => getQueryTagsOf(currentQueryInfo, apiState) ); + const selectApiStatsOfCurrentQuery = createSelector( + selectApiStates, + selectorCurrentQueryInfo, + (apiState, currentQueryInfo) => + generateApiStatsOfCurrentQuery(currentQueryInfo, apiState) + ); + return { selectQueryComparator, selectApiStates, @@ -143,5 +152,6 @@ export function createInspectorSelectors(): InspectorSelectors { selectSearchQueryRegex, selectorCurrentQueryInfo, selectCurrentQueryTags, + selectApiStatsOfCurrentQuery, }; } diff --git a/packages/redux-devtools-rtk-query-inspector-monitor/src/types.ts b/packages/redux-devtools-rtk-query-inspector-monitor/src/types.ts index 8b6e3163..defba4df 100644 --- a/packages/redux-devtools-rtk-query-inspector-monitor/src/types.ts +++ b/packages/redux-devtools-rtk-query-inspector-monitor/src/types.ts @@ -1,5 +1,5 @@ import { LiftedAction, LiftedState } from '@redux-devtools/instrument'; -import type { createApi } from '@reduxjs/toolkit/query'; +import type { createApi, QueryStatus } from '@reduxjs/toolkit/query'; import { ComponentType, Dispatch } from 'react'; import { Base16Theme, StylingFunction } from 'react-base16-styling'; import { Action } from 'redux'; @@ -33,14 +33,7 @@ export interface RtkQueryInspectorMonitorProps> dispatch: Dispatch< Action | LiftedAction >; - - preserveScrollTop: boolean; - select: (state: S) => unknown; theme: keyof typeof themes | Base16Theme; - expandActionRoot: boolean; - expandStateRoot: boolean; - markStateDiff: boolean; - hideMainButtons?: boolean; invertTheme?: boolean; } @@ -52,6 +45,10 @@ export type RtkQueryState = NonNullable< RtkQueryApiState['queries'][keyof RtkQueryApiState] >; +export type RtkMutationState = NonNullable< + RtkQueryApiState['mutations'][keyof RtkQueryApiState] +>; + export type RtkQueryApiConfig = RtkQueryApiState['config']; export type RtkQueryProvided = RtkQueryApiState['provided']; @@ -77,6 +74,12 @@ export interface QueryInfo { reducerPath: string; } +export interface MutationInfo { + mutation: RtkMutationState; + queryKey: string; + reducerPath: string; +} + export interface ApiInfo { reducerPath: string; apiState: RtkQueryApiState; @@ -107,12 +110,31 @@ export interface RtkQueryTag { id?: number | string; } +interface Tally { + count: number; +} + +export type QueryTally = { + [key in QueryStatus]?: number; +} & + Tally; + +export interface ApiStats { + readonly tally: { + subscriptions: Tally; + queries: QueryTally; + tagTypes: Tally; + mutations: QueryTally; + }; +} + export interface QueryPreviewTabProps extends StyleUtils { queryInfo: QueryInfo | null; apiConfig: RtkQueryApiState['config'] | null; querySubscriptions: RTKQuerySubscribers | null; isWideLayout: boolean; tags: RtkQueryTag[]; + apiStats: ApiStats | null; } export interface TabOption extends SelectOption { diff --git a/packages/redux-devtools-rtk-query-inspector-monitor/src/utils/rtk-query.ts b/packages/redux-devtools-rtk-query-inspector-monitor/src/utils/rtk-query.ts index ccd36523..282c5a69 100644 --- a/packages/redux-devtools-rtk-query-inspector-monitor/src/utils/rtk-query.ts +++ b/packages/redux-devtools-rtk-query-inspector-monitor/src/utils/rtk-query.ts @@ -8,10 +8,14 @@ import { RtkQueryTag, RTKStatusFlags, RtkQueryState, + MutationInfo, + ApiStats, + QueryTally, } from '../types'; import { missingTagId } from '../monitor-config'; import { Comparator } from './comparators'; import { emptyArray } from './object'; +import { SubscriptionState } from '@reduxjs/toolkit/dist/query/core/apiState'; const rtkqueryApiStateKeys: ReadonlyArray = [ 'queries', @@ -38,18 +42,18 @@ export function isApiSlice(val: unknown): val is RtkQueryApiState { } export function getApiStatesOf( - state: unknown + reduxStoreState: unknown ): null | Readonly> { - if (!isPlainObject(state)) { + if (!isPlainObject(reduxStoreState)) { return null; } const output: null | Record = {}; - const keys = Object.keys(state); + const keys = Object.keys(reduxStoreState); for (let i = 0, len = keys.length; i < len; i++) { const key = keys[i]; - const value = (state as Record)[key]; + const value = (reduxStoreState as Record)[key]; if (isApiSlice(value)) { output[key] = value; @@ -96,6 +100,105 @@ export function extractAllApiQueries( return output; } +export function extractAllApiMutations( + apiStatesByReducerPath: null | Readonly> +): ReadonlyArray { + if (!apiStatesByReducerPath) { + return emptyArray; + } + + const reducerPaths = Object.keys(apiStatesByReducerPath); + const output: MutationInfo[] = []; + + for (let i = 0, len = reducerPaths.length; i < len; i++) { + const reducerPath = reducerPaths[i]; + const api = apiStatesByReducerPath[reducerPath]; + const mutationKeys = Object.keys(api.mutations); + + for (let j = 0, mKeysLen = mutationKeys.length; j < mKeysLen; j++) { + const queryKey = mutationKeys[j]; + const mutation = api.queries[queryKey]; + + if (mutation) { + output.push({ + reducerPath, + queryKey, + mutation, + }); + } + } + } + + return output; +} + +function computeQueryTallyOf( + queryState: RtkQueryApiState['queries'] | RtkQueryApiState['mutations'] +): QueryTally { + const queries = Object.values(queryState); + + const output: QueryTally = { + count: 0, + }; + + for (let i = 0, len = queries.length; i < len; i++) { + const query = queries[i]; + + if (query) { + output.count++; + + if (!output[query.status]) { + output[query.status] = 1; + } else { + (output[query.status] as number)++; + } + } + } + + return output; +} + +function tallySubscriptions( + subsState: SubscriptionState +): ApiStats['tally']['subscriptions'] { + const subsOfQueries = Object.values(subsState); + + const output: ApiStats['tally']['subscriptions'] = { + count: 0, + }; + + for (let i = 0, len = subsOfQueries.length; i < len; i++) { + const subsOfQuery = subsOfQueries[i]; + + if (subsOfQuery) { + output.count += Object.keys(subsOfQuery).length; + } + } + + return output; +} + +export function generateApiStatsOfCurrentQuery( + queryInfo: QueryInfo | null, + apiStates: ReturnType +): ApiStats | null { + if (!apiStates || !queryInfo) { + return null; + } + + const { reducerPath } = queryInfo; + const api = apiStates[reducerPath]; + + return { + tally: { + subscriptions: tallySubscriptions(api.subscriptions), + queries: computeQueryTallyOf(api.queries), + tagTypes: { count: Object.keys(api.provided).length }, + mutations: computeQueryTallyOf(api.mutations), + }, + }; +} + export function flipComparator(comparator: Comparator): Comparator { return function flipped(a: T, b: T) { return comparator(b, a);