mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-07-26 07:59:48 +03:00
feat(rtk-query): display api stats
This commit is contained in:
parent
bad8d499d4
commit
69220916b3
|
@ -73,6 +73,7 @@ See also
|
|||
- tags
|
||||
- subscriptions
|
||||
- api slice config
|
||||
- api stats
|
||||
|
||||
## TODO
|
||||
|
||||
|
|
|
@ -134,14 +134,8 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
|
|||
|
||||
const currentTags = this.selectors.selectCurrentQueryTags(selectorsSource);
|
||||
|
||||
console.log('inspector', {
|
||||
apiStates,
|
||||
allVisibleQueries,
|
||||
selectorsSource,
|
||||
currentQueryInfo,
|
||||
currentRtkApi,
|
||||
currentTags,
|
||||
});
|
||||
const currentApiStats =
|
||||
this.selectors.selectApiStatsOfCurrentQuery(selectorsSource);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -172,6 +166,7 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
|
|||
querySubscriptions={currentQuerySubscriptions}
|
||||
apiConfig={currentRtkApi?.config ?? null}
|
||||
isWideLayout={isWideLayout}
|
||||
apiStats={currentApiStats}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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<QueryPreviewTabOption> = [
|
|||
{
|
||||
label: 'api',
|
||||
value: QueryPreviewTabs.apiConfig,
|
||||
component: QueryPreviewApiConfig,
|
||||
component: QueryPreviewApi,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -94,6 +94,7 @@ export class QueryPreview extends React.PureComponent<QueryPreviewProps> {
|
|||
onTabChange,
|
||||
querySubscriptions,
|
||||
tags,
|
||||
apiStats,
|
||||
} = this.props;
|
||||
|
||||
const { component: TabComponent } =
|
||||
|
@ -136,6 +137,7 @@ export class QueryPreview extends React.PureComponent<QueryPreviewProps> {
|
|||
tags={tags}
|
||||
apiConfig={apiConfig}
|
||||
isWideLayout={isWideLayout}
|
||||
apiStats={apiStats}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<S, Output> = Selector<SelectorsSource<S>, Output>;
|
||||
|
@ -50,6 +51,7 @@ export interface InspectorSelectors<S> {
|
|||
readonly selectorCurrentQueryInfo: InspectorSelector<S, QueryInfo | null>;
|
||||
readonly selectSearchQueryRegex: InspectorSelector<S, RegExp | null>;
|
||||
readonly selectCurrentQueryTags: InspectorSelector<S, RtkQueryTag[]>;
|
||||
readonly selectApiStatsOfCurrentQuery: InspectorSelector<S, ApiStats | null>;
|
||||
}
|
||||
|
||||
export function createInspectorSelectors<S>(): InspectorSelectors<S> {
|
||||
|
@ -135,6 +137,13 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
|
|||
(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<S>(): InspectorSelectors<S> {
|
|||
selectSearchQueryRegex,
|
||||
selectorCurrentQueryInfo,
|
||||
selectCurrentQueryTags,
|
||||
selectApiStatsOfCurrentQuery,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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<S, A extends Action<unknown>>
|
|||
dispatch: Dispatch<
|
||||
Action | LiftedAction<S, A, RtkQueryInspectorMonitorState>
|
||||
>;
|
||||
|
||||
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<S, P> extends SelectOption<S> {
|
||||
|
|
|
@ -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<keyof RtkQueryApiState> = [
|
||||
'queries',
|
||||
|
@ -38,18 +42,18 @@ export function isApiSlice(val: unknown): val is RtkQueryApiState {
|
|||
}
|
||||
|
||||
export function getApiStatesOf(
|
||||
state: unknown
|
||||
reduxStoreState: unknown
|
||||
): null | Readonly<Record<string, RtkQueryApiState>> {
|
||||
if (!isPlainObject(state)) {
|
||||
if (!isPlainObject(reduxStoreState)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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++) {
|
||||
const key = keys[i];
|
||||
const value = (state as Record<string, unknown>)[key];
|
||||
const value = (reduxStoreState as Record<string, unknown>)[key];
|
||||
|
||||
if (isApiSlice(value)) {
|
||||
output[key] = value;
|
||||
|
@ -96,6 +100,105 @@ export function extractAllApiQueries(
|
|||
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> {
|
||||
return function flipped(a: T, b: T) {
|
||||
return comparator(b, a);
|
||||
|
|
Loading…
Reference in New Issue
Block a user