mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-07-27 00:19:55 +03:00
feat(rtk-query): display api stats
This commit is contained in:
parent
bad8d499d4
commit
69220916b3
|
@ -73,6 +73,7 @@ See also
|
||||||
- tags
|
- tags
|
||||||
- subscriptions
|
- subscriptions
|
||||||
- api slice config
|
- api slice config
|
||||||
|
- api stats
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 { 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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user