feat(rtk-query): display api stats

This commit is contained in:
FaberVitale 2021-06-19 18:52:27 +02:00
parent bad8d499d4
commit 69220916b3
8 changed files with 193 additions and 53 deletions

View File

@ -73,6 +73,7 @@ See also
- tags - tags
- subscriptions - subscriptions
- api slice config - api slice config
- api stats
## TODO ## TODO

View File

@ -134,14 +134,8 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
const currentTags = this.selectors.selectCurrentQueryTags(selectorsSource); const currentTags = this.selectors.selectCurrentQueryTags(selectorsSource);
console.log('inspector', { const currentApiStats =
apiStates, this.selectors.selectApiStatsOfCurrentQuery(selectorsSource);
allVisibleQueries,
selectorsSource,
currentQueryInfo,
currentRtkApi,
currentTags,
});
return ( return (
<div <div
@ -172,6 +166,7 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
querySubscriptions={currentQuerySubscriptions} querySubscriptions={currentQuerySubscriptions}
apiConfig={currentRtkApi?.config ?? null} apiConfig={currentRtkApi?.config ?? null}
isWideLayout={isWideLayout} isWideLayout={isWideLayout}
apiStats={currentApiStats}
/> />
</div> </div>
); );

View File

@ -8,7 +8,7 @@ import {
} from '../types'; } from '../types';
import { QueryPreviewHeader } from './QueryPreviewHeader'; import { QueryPreviewHeader } from './QueryPreviewHeader';
import { QueryPreviewInfo } from './QueryPreviewInfo'; import { QueryPreviewInfo } from './QueryPreviewInfo';
import { QueryPreviewApiConfig } from './QueryPreviewApiConfig'; import { QueryPreviewApi } from './QueryPreviewApi';
import { QueryPreviewSubscriptions } from './QueryPreviewSubscriptions'; import { QueryPreviewSubscriptions } from './QueryPreviewSubscriptions';
import { QueryPreviewTags } from './QueryPreviewTags'; import { QueryPreviewTags } from './QueryPreviewTags';
@ -37,7 +37,7 @@ const tabs: ReadonlyArray<QueryPreviewTabOption> = [
{ {
label: 'api', label: 'api',
value: QueryPreviewTabs.apiConfig, value: QueryPreviewTabs.apiConfig,
component: QueryPreviewApiConfig, component: QueryPreviewApi,
}, },
]; ];
@ -94,6 +94,7 @@ export class QueryPreview extends React.PureComponent<QueryPreviewProps> {
onTabChange, onTabChange,
querySubscriptions, querySubscriptions,
tags, tags,
apiStats,
} = this.props; } = this.props;
const { component: TabComponent } = const { component: TabComponent } =
@ -136,6 +137,7 @@ export class QueryPreview extends React.PureComponent<QueryPreviewProps> {
tags={tags} tags={tags}
apiConfig={apiConfig} apiConfig={apiConfig}
isWideLayout={isWideLayout} isWideLayout={isWideLayout}
apiStats={apiStats}
/> />
</div> </div>
); );

View File

@ -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<QueryPreviewTabProps> {
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 (
<TreeView
data={this.selectData(this.props)}
isWideLayout={isWideLayout}
base16Theme={base16Theme}
styling={styling}
invertTheme={invertTheme}
/>
);
}
}

View File

@ -1,30 +0,0 @@
import React, { ReactNode, PureComponent } from 'react';
import { QueryPreviewTabProps } from '../types';
import { TreeView } from './TreeView';
export class QueryPreviewApiConfig extends PureComponent<QueryPreviewTabProps> {
render(): ReactNode {
const {
queryInfo,
isWideLayout,
base16Theme,
styling,
invertTheme,
apiConfig,
} = this.props;
if (!queryInfo) {
return null;
}
return (
<TreeView
data={apiConfig}
isWideLayout={isWideLayout}
base16Theme={base16Theme}
styling={styling}
invertTheme={invertTheme}
/>
);
}
}

View File

@ -1,6 +1,6 @@
import { Action, createSelector, Selector } from '@reduxjs/toolkit'; import { Action, createSelector, Selector } from '@reduxjs/toolkit';
import { RtkQueryInspectorProps } from './RtkQueryInspector'; import { RtkQueryInspectorProps } from './RtkQueryInspector';
import { QueryInfo, RtkQueryTag, SelectorsSource } from './types'; import { ApiStats, QueryInfo, RtkQueryTag, SelectorsSource } from './types';
import { Comparator, queryComparators } from './utils/comparators'; import { Comparator, queryComparators } from './utils/comparators';
import { FilterList, queryListFilters } from './utils/filters'; import { FilterList, queryListFilters } from './utils/filters';
import { escapeRegExpSpecialCharacter } from './utils/regexp'; import { escapeRegExpSpecialCharacter } from './utils/regexp';
@ -9,6 +9,7 @@ import {
extractAllApiQueries, extractAllApiQueries,
flipComparator, flipComparator,
getQueryTagsOf, getQueryTagsOf,
generateApiStatsOfCurrentQuery,
} from './utils/rtk-query'; } from './utils/rtk-query';
type InspectorSelector<S, Output> = Selector<SelectorsSource<S>, Output>; type InspectorSelector<S, Output> = Selector<SelectorsSource<S>, Output>;
@ -50,6 +51,7 @@ export interface InspectorSelectors<S> {
readonly selectorCurrentQueryInfo: InspectorSelector<S, QueryInfo | null>; readonly selectorCurrentQueryInfo: InspectorSelector<S, QueryInfo | null>;
readonly selectSearchQueryRegex: InspectorSelector<S, RegExp | null>; readonly selectSearchQueryRegex: InspectorSelector<S, RegExp | null>;
readonly selectCurrentQueryTags: InspectorSelector<S, RtkQueryTag[]>; readonly selectCurrentQueryTags: InspectorSelector<S, RtkQueryTag[]>;
readonly selectApiStatsOfCurrentQuery: InspectorSelector<S, ApiStats | null>;
} }
export function createInspectorSelectors<S>(): InspectorSelectors<S> { export function createInspectorSelectors<S>(): InspectorSelectors<S> {
@ -135,6 +137,13 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
(apiState, currentQueryInfo) => getQueryTagsOf(currentQueryInfo, apiState) (apiState, currentQueryInfo) => getQueryTagsOf(currentQueryInfo, apiState)
); );
const selectApiStatsOfCurrentQuery = createSelector(
selectApiStates,
selectorCurrentQueryInfo,
(apiState, currentQueryInfo) =>
generateApiStatsOfCurrentQuery(currentQueryInfo, apiState)
);
return { return {
selectQueryComparator, selectQueryComparator,
selectApiStates, selectApiStates,
@ -143,5 +152,6 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
selectSearchQueryRegex, selectSearchQueryRegex,
selectorCurrentQueryInfo, selectorCurrentQueryInfo,
selectCurrentQueryTags, selectCurrentQueryTags,
selectApiStatsOfCurrentQuery,
}; };
} }

View File

@ -1,5 +1,5 @@
import { LiftedAction, LiftedState } from '@redux-devtools/instrument'; 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 { ComponentType, Dispatch } from 'react';
import { Base16Theme, StylingFunction } from 'react-base16-styling'; import { Base16Theme, StylingFunction } from 'react-base16-styling';
import { Action } from 'redux'; import { Action } from 'redux';
@ -33,14 +33,7 @@ export interface RtkQueryInspectorMonitorProps<S, A extends Action<unknown>>
dispatch: Dispatch< dispatch: Dispatch<
Action | LiftedAction<S, A, RtkQueryInspectorMonitorState> Action | LiftedAction<S, A, RtkQueryInspectorMonitorState>
>; >;
preserveScrollTop: boolean;
select: (state: S) => unknown;
theme: keyof typeof themes | Base16Theme; theme: keyof typeof themes | Base16Theme;
expandActionRoot: boolean;
expandStateRoot: boolean;
markStateDiff: boolean;
hideMainButtons?: boolean;
invertTheme?: boolean; invertTheme?: boolean;
} }
@ -52,6 +45,10 @@ export type RtkQueryState = NonNullable<
RtkQueryApiState['queries'][keyof RtkQueryApiState] RtkQueryApiState['queries'][keyof RtkQueryApiState]
>; >;
export type RtkMutationState = NonNullable<
RtkQueryApiState['mutations'][keyof RtkQueryApiState]
>;
export type RtkQueryApiConfig = RtkQueryApiState['config']; export type RtkQueryApiConfig = RtkQueryApiState['config'];
export type RtkQueryProvided = RtkQueryApiState['provided']; export type RtkQueryProvided = RtkQueryApiState['provided'];
@ -77,6 +74,12 @@ export interface QueryInfo {
reducerPath: string; reducerPath: string;
} }
export interface MutationInfo {
mutation: RtkMutationState;
queryKey: string;
reducerPath: string;
}
export interface ApiInfo { export interface ApiInfo {
reducerPath: string; reducerPath: string;
apiState: RtkQueryApiState; apiState: RtkQueryApiState;
@ -107,12 +110,31 @@ export interface RtkQueryTag {
id?: number | string; 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 { export interface QueryPreviewTabProps extends StyleUtils {
queryInfo: QueryInfo | null; queryInfo: QueryInfo | null;
apiConfig: RtkQueryApiState['config'] | null; apiConfig: RtkQueryApiState['config'] | null;
querySubscriptions: RTKQuerySubscribers | null; querySubscriptions: RTKQuerySubscribers | null;
isWideLayout: boolean; isWideLayout: boolean;
tags: RtkQueryTag[]; tags: RtkQueryTag[];
apiStats: ApiStats | null;
} }
export interface TabOption<S, P> extends SelectOption<S> { export interface TabOption<S, P> extends SelectOption<S> {

View File

@ -8,10 +8,14 @@ import {
RtkQueryTag, RtkQueryTag,
RTKStatusFlags, RTKStatusFlags,
RtkQueryState, RtkQueryState,
MutationInfo,
ApiStats,
QueryTally,
} from '../types'; } from '../types';
import { missingTagId } from '../monitor-config'; import { missingTagId } from '../monitor-config';
import { Comparator } from './comparators'; import { Comparator } from './comparators';
import { emptyArray } from './object'; import { emptyArray } from './object';
import { SubscriptionState } from '@reduxjs/toolkit/dist/query/core/apiState';
const rtkqueryApiStateKeys: ReadonlyArray<keyof RtkQueryApiState> = [ const rtkqueryApiStateKeys: ReadonlyArray<keyof RtkQueryApiState> = [
'queries', 'queries',
@ -38,18 +42,18 @@ export function isApiSlice(val: unknown): val is RtkQueryApiState {
} }
export function getApiStatesOf( export function getApiStatesOf(
state: unknown reduxStoreState: unknown
): null | Readonly<Record<string, RtkQueryApiState>> { ): null | Readonly<Record<string, RtkQueryApiState>> {
if (!isPlainObject(state)) { if (!isPlainObject(reduxStoreState)) {
return null; return null;
} }
const output: null | Record<string, RtkQueryApiState> = {}; const output: null | Record<string, RtkQueryApiState> = {};
const keys = Object.keys(state); const keys = Object.keys(reduxStoreState);
for (let i = 0, len = keys.length; i < len; i++) { for (let i = 0, len = keys.length; i < len; i++) {
const key = keys[i]; const key = keys[i];
const value = (state as Record<string, unknown>)[key]; const value = (reduxStoreState as Record<string, unknown>)[key];
if (isApiSlice(value)) { if (isApiSlice(value)) {
output[key] = value; output[key] = value;
@ -96,6 +100,105 @@ export function extractAllApiQueries(
return output; return output;
} }
export function extractAllApiMutations(
apiStatesByReducerPath: null | Readonly<Record<string, RtkQueryApiState>>
): ReadonlyArray<MutationInfo> {
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<typeof getApiStatesOf>
): 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<T>(comparator: Comparator<T>): Comparator<T> { export function flipComparator<T>(comparator: Comparator<T>): Comparator<T> {
return function flipped(a: T, b: T) { return function flipped(a: T, b: T) {
return comparator(b, a); return comparator(b, a);