mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-07-27 00:19:55 +03:00
feat(rtk-query-monitor): add query preview tabs
This commit is contained in:
parent
1f189f3bdc
commit
5a485a47ab
|
@ -4,9 +4,14 @@ import type { PokemonName } from '../pokemon.data';
|
|||
export const pokemonApi = createApi({
|
||||
reducerPath: 'pokemonApi',
|
||||
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
|
||||
tagTypes: ['pokemon'],
|
||||
endpoints: (builder) => ({
|
||||
getPokemonByName: builder.query({
|
||||
query: (name: PokemonName) => `pokemon/${name}`,
|
||||
providesTags: (result, error, name: PokemonName) => [
|
||||
{ type: 'pokemon' },
|
||||
{ type: 'pokemon', id: name },
|
||||
],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -6,14 +6,20 @@ import { Base16Theme } from 'react-base16-styling';
|
|||
import {
|
||||
QueryFormValues,
|
||||
QueryInfo,
|
||||
QueryPreviewTabs,
|
||||
RtkQueryInspectorMonitorState,
|
||||
StyleUtils,
|
||||
} from './types';
|
||||
import { createInspectorSelectors, computeSelectorSource } from './selectors';
|
||||
import { changeQueryFormValues, selectQueryKey } from './reducers';
|
||||
import {
|
||||
changeQueryFormValues,
|
||||
selectedPreviewTab,
|
||||
selectQueryKey,
|
||||
} from './reducers';
|
||||
import { QueryList } from './components/QueryList';
|
||||
import { StyleUtils } from './styles/createStylingFromTheme';
|
||||
import { QueryForm } from './components/QueryForm';
|
||||
import { QueryPreview } from './components/QueryPreview';
|
||||
import { getApiStateOf, getQuerySubscriptionsOf } from './utils/rtk-query';
|
||||
|
||||
type SelectorsSource<S> = {
|
||||
userState: S | null;
|
||||
|
@ -104,6 +110,10 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
|
|||
this.props.dispatch(selectQueryKey(queryInfo) as AnyAction);
|
||||
};
|
||||
|
||||
handleTabChange = (tab: QueryPreviewTabs): void => {
|
||||
this.props.dispatch(selectedPreviewTab(tab) as AnyAction);
|
||||
};
|
||||
|
||||
render(): ReactNode {
|
||||
const { selectorsSource, isWideLayout } = this.state;
|
||||
const {
|
||||
|
@ -118,11 +128,21 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
|
|||
selectorsSource
|
||||
);
|
||||
|
||||
const currentRtkApi = getApiStateOf(currentQueryInfo, apiStates);
|
||||
const currentQuerySubscriptions = getQuerySubscriptionsOf(
|
||||
currentQueryInfo,
|
||||
apiStates
|
||||
);
|
||||
|
||||
const currentTags = this.selectors.selectCurrentQueryTags(selectorsSource);
|
||||
|
||||
console.log('inspector', {
|
||||
apiStates,
|
||||
allVisibleQueries,
|
||||
selectorsSource,
|
||||
currentQueryInfo,
|
||||
currentRtkApi,
|
||||
currentTags,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -147,8 +167,13 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
|
|||
/>
|
||||
</div>
|
||||
<QueryPreview
|
||||
selectedQueryInfo={currentQueryInfo}
|
||||
queryInfo={currentQueryInfo}
|
||||
selectedTab={selectorsSource.monitorState.selectedPreviewTab}
|
||||
onTabChange={this.handleTabChange}
|
||||
styling={styling}
|
||||
tags={currentTags}
|
||||
querySubscriptions={currentQuerySubscriptions}
|
||||
apiConfig={currentRtkApi?.config ?? null}
|
||||
isWideLayout={isWideLayout}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,20 +1,46 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import JSONTree from 'react-json-tree';
|
||||
import { StylingFunction } from 'react-base16-styling';
|
||||
import { DATA_TYPE_KEY } from '../monitor-config';
|
||||
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
|
||||
import { createTreeItemLabelRenderer } from '../styles/tree';
|
||||
import {
|
||||
getJsonTreeTheme,
|
||||
StyleUtilsContext,
|
||||
} from '../styles/createStylingFromTheme';
|
||||
import { createTreeItemLabelRenderer, getItemString } from '../styles/tree';
|
||||
import { QueryInfo } from '../types';
|
||||
QueryPreviewTabOption,
|
||||
QueryPreviewTabs,
|
||||
QueryPreviewTabProps,
|
||||
} from '../types';
|
||||
import { QueryPreviewHeader } from './QueryPreviewHeader';
|
||||
import { QueryPreviewInfo } from './QueryPreviewInfo';
|
||||
import { QueryPreviewApiConfig } from './QueryPreviewApiConfig';
|
||||
import { QueryPreviewSubscriptions } from './QueryPreviewSubscriptions';
|
||||
import { QueryPreviewTags } from './QueryPreviewTags';
|
||||
|
||||
export interface QueryPreviewProps {
|
||||
selectedQueryInfo: QueryInfo | null;
|
||||
styling: StylingFunction;
|
||||
isWideLayout: boolean;
|
||||
export interface QueryPreviewProps
|
||||
extends Omit<QueryPreviewTabProps, 'base16Theme' | 'invertTheme'> {
|
||||
selectedTab: QueryPreviewTabs;
|
||||
onTabChange: (tab: QueryPreviewTabs) => void;
|
||||
}
|
||||
|
||||
const tabs: ReadonlyArray<QueryPreviewTabOption> = [
|
||||
{
|
||||
label: 'query',
|
||||
value: QueryPreviewTabs.queryinfo,
|
||||
component: QueryPreviewInfo,
|
||||
},
|
||||
{
|
||||
label: 'tags',
|
||||
value: QueryPreviewTabs.queryTags,
|
||||
component: QueryPreviewTags,
|
||||
},
|
||||
{
|
||||
label: 'subs',
|
||||
value: QueryPreviewTabs.querySubscriptions,
|
||||
component: QueryPreviewSubscriptions,
|
||||
},
|
||||
{
|
||||
label: 'api',
|
||||
value: QueryPreviewTabs.apiConfig,
|
||||
component: QueryPreviewApiConfig,
|
||||
},
|
||||
];
|
||||
|
||||
export class QueryPreview extends React.PureComponent<QueryPreviewProps> {
|
||||
readonly labelRenderer: ReturnType<typeof createTreeItemLabelRenderer>;
|
||||
|
||||
|
@ -25,68 +51,55 @@ export class QueryPreview extends React.PureComponent<QueryPreviewProps> {
|
|||
}
|
||||
|
||||
render(): ReactNode {
|
||||
const { selectedQueryInfo, isWideLayout } = this.props;
|
||||
const {
|
||||
queryInfo,
|
||||
isWideLayout,
|
||||
selectedTab,
|
||||
apiConfig,
|
||||
onTabChange,
|
||||
querySubscriptions,
|
||||
tags,
|
||||
} = this.props;
|
||||
|
||||
if (!selectedQueryInfo) {
|
||||
const { component: TabComponent } =
|
||||
tabs.find((tab) => tab.value === selectedTab) || tabs[0];
|
||||
|
||||
if (!queryInfo) {
|
||||
return (
|
||||
<StyleUtilsContext.Consumer>
|
||||
{({ styling }) => <div {...styling('queryPreview')} />}
|
||||
{({ styling }) => (
|
||||
<div {...styling('queryPreview')}>
|
||||
<QueryPreviewHeader
|
||||
selectedTab={selectedTab}
|
||||
onTabChange={onTabChange}
|
||||
tabs={tabs}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</StyleUtilsContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
query: {
|
||||
endpointName,
|
||||
fulfilledTimeStamp,
|
||||
status,
|
||||
startedTimeStamp,
|
||||
data,
|
||||
},
|
||||
reducerPath,
|
||||
} = selectedQueryInfo;
|
||||
|
||||
const startedAt = startedTimeStamp
|
||||
? new Date(startedTimeStamp).toISOString()
|
||||
: '-';
|
||||
|
||||
const latestFetch = fulfilledTimeStamp
|
||||
? new Date(fulfilledTimeStamp).toISOString()
|
||||
: '-';
|
||||
|
||||
return (
|
||||
<StyleUtilsContext.Consumer>
|
||||
{({ styling, base16Theme, invertTheme }) => {
|
||||
return (
|
||||
<div {...styling('queryPreview')}>
|
||||
<React.Fragment>
|
||||
<div {...styling('previewHeader')}></div>
|
||||
<ul>
|
||||
<li>{`reducerPath: ${reducerPath ?? '-'}`}</li>
|
||||
<li>{`endpointName: ${endpointName ?? '-'}`}</li>
|
||||
<li>{`status: ${status}`}</li>
|
||||
<li>{`loaded at: ${latestFetch}`}</li>
|
||||
<li>{`requested at: ${startedAt}`}</li>
|
||||
</ul>
|
||||
<div {...styling('treeWrapper')}>
|
||||
<JSONTree
|
||||
data={data}
|
||||
labelRenderer={this.labelRenderer}
|
||||
theme={getJsonTreeTheme(base16Theme)}
|
||||
invertTheme={invertTheme}
|
||||
getItemString={(type, data) =>
|
||||
getItemString(
|
||||
styling,
|
||||
type,
|
||||
data,
|
||||
DATA_TYPE_KEY,
|
||||
isWideLayout
|
||||
)
|
||||
}
|
||||
hideRoot
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
<QueryPreviewHeader
|
||||
selectedTab={selectedTab}
|
||||
onTabChange={onTabChange}
|
||||
tabs={tabs}
|
||||
/>
|
||||
<TabComponent
|
||||
styling={styling}
|
||||
base16Theme={base16Theme}
|
||||
invertTheme={invertTheme}
|
||||
querySubscriptions={querySubscriptions}
|
||||
queryInfo={queryInfo}
|
||||
tags={tags}
|
||||
apiConfig={apiConfig}
|
||||
isWideLayout={isWideLayout}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
|
||||
import { QueryPreviewTabOption, QueryPreviewTabs } from '../types';
|
||||
import { emptyArray } from '../utils/object';
|
||||
|
||||
export interface QueryPreviewHeaderProps {
|
||||
tabs: ReadonlyArray<QueryPreviewTabOption>;
|
||||
onTabChange: (tab: QueryPreviewTabs) => void;
|
||||
selectedTab: QueryPreviewTabs;
|
||||
}
|
||||
|
||||
export class QueryPreviewHeader extends React.Component<
|
||||
QueryPreviewHeaderProps
|
||||
> {
|
||||
handleTabClick = (tab: QueryPreviewTabOption): void => {
|
||||
if (this.props.selectedTab !== tab.value) {
|
||||
this.props.onTabChange(tab.value);
|
||||
}
|
||||
};
|
||||
|
||||
render(): ReactNode {
|
||||
const { tabs, selectedTab } = this.props;
|
||||
|
||||
return (
|
||||
<StyleUtilsContext.Consumer>
|
||||
{({ styling }) => (
|
||||
<div {...styling('previewHeader')}>
|
||||
<div {...styling('tabSelector')}>
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
onClick={() => this.handleTabClick(tab)}
|
||||
key={tab.value}
|
||||
{...styling(
|
||||
[
|
||||
'selectorButton',
|
||||
tab.value === selectedTab && 'selectorButtonSelected',
|
||||
],
|
||||
tab.value === selectedTab
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</StyleUtilsContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
tabs: emptyArray,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import { createSelector, Selector } from '@reduxjs/toolkit';
|
||||
import React, { ReactNode, PureComponent } from 'react';
|
||||
import { QueryInfo, QueryPreviewTabProps, RtkQueryState } from '../types';
|
||||
import { identity } from '../utils/object';
|
||||
import { TreeView } from './TreeView';
|
||||
|
||||
type ComputedQueryInfo = {
|
||||
startedAt: string;
|
||||
latestFetchAt: string;
|
||||
};
|
||||
|
||||
interface FormattedQuery extends ComputedQueryInfo {
|
||||
queryKey: string;
|
||||
query: RtkQueryState;
|
||||
}
|
||||
|
||||
export class QueryPreviewInfo extends PureComponent<QueryPreviewTabProps> {
|
||||
selectFormattedQuery: Selector<
|
||||
QueryPreviewTabProps['queryInfo'],
|
||||
FormattedQuery | null
|
||||
> = createSelector(
|
||||
identity,
|
||||
(queryInfo: QueryInfo | null): FormattedQuery | null => {
|
||||
if (!queryInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { query, queryKey } = queryInfo;
|
||||
|
||||
const startedAt = query.startedTimeStamp
|
||||
? new Date(query.startedTimeStamp).toISOString()
|
||||
: '-';
|
||||
|
||||
const latestFetchAt = query.fulfilledTimeStamp
|
||||
? new Date(query.fulfilledTimeStamp).toISOString()
|
||||
: '-';
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
startedAt,
|
||||
latestFetchAt,
|
||||
query: queryInfo.query,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
render(): ReactNode {
|
||||
const {
|
||||
queryInfo,
|
||||
isWideLayout,
|
||||
base16Theme,
|
||||
styling,
|
||||
invertTheme,
|
||||
} = this.props;
|
||||
|
||||
const formattedQuery = this.selectFormattedQuery(queryInfo);
|
||||
|
||||
if (!formattedQuery) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TreeView
|
||||
data={formattedQuery}
|
||||
isWideLayout={isWideLayout}
|
||||
base16Theme={base16Theme}
|
||||
styling={styling}
|
||||
invertTheme={invertTheme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import React, { ReactNode, PureComponent } from 'react';
|
||||
import { QueryPreviewTabProps } from '../types';
|
||||
import { TreeView } from './TreeView';
|
||||
|
||||
export class QueryPreviewSubscriptions extends PureComponent<
|
||||
QueryPreviewTabProps
|
||||
> {
|
||||
render(): ReactNode {
|
||||
const {
|
||||
queryInfo,
|
||||
isWideLayout,
|
||||
base16Theme,
|
||||
styling,
|
||||
invertTheme,
|
||||
querySubscriptions,
|
||||
} = this.props;
|
||||
|
||||
if (!querySubscriptions || !queryInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TreeView
|
||||
data={querySubscriptions}
|
||||
isWideLayout={isWideLayout}
|
||||
base16Theme={base16Theme}
|
||||
styling={styling}
|
||||
invertTheme={invertTheme}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import React, { ReactNode, PureComponent } from 'react';
|
||||
import { QueryPreviewTabProps } from '../types';
|
||||
import { TreeView } from './TreeView';
|
||||
|
||||
interface QueryPreviewTagsState {
|
||||
data: { tags: QueryPreviewTabProps['tags'] };
|
||||
}
|
||||
|
||||
export class QueryPreviewTags extends PureComponent<
|
||||
QueryPreviewTabProps,
|
||||
QueryPreviewTagsState
|
||||
> {
|
||||
static getDerivedStateFromProps(
|
||||
{ tags }: QueryPreviewTabProps,
|
||||
state: QueryPreviewTagsState
|
||||
): QueryPreviewTagsState | null {
|
||||
if (tags !== state.data.tags) {
|
||||
return {
|
||||
data: { tags },
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
constructor(props: QueryPreviewTabProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
data: { tags: props.tags },
|
||||
};
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
const {
|
||||
queryInfo,
|
||||
isWideLayout,
|
||||
base16Theme,
|
||||
styling,
|
||||
invertTheme,
|
||||
} = this.props;
|
||||
|
||||
if (!queryInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TreeView
|
||||
data={this.state.data}
|
||||
isWideLayout={isWideLayout}
|
||||
base16Theme={base16Theme}
|
||||
styling={styling}
|
||||
invertTheme={invertTheme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import React, { ComponentProps, ReactNode } from 'react';
|
||||
import JSONTree from 'react-json-tree';
|
||||
import { DATA_TYPE_KEY } from '../monitor-config';
|
||||
import { getJsonTreeTheme } from '../styles/createStylingFromTheme';
|
||||
import { createTreeItemLabelRenderer, getItemString } from '../styles/tree';
|
||||
import { StyleUtils } from '../types';
|
||||
|
||||
export interface TreeViewProps extends StyleUtils {
|
||||
data: unknown;
|
||||
isWideLayout: boolean;
|
||||
before?: ReactNode;
|
||||
after?: ReactNode;
|
||||
children?: ReactNode;
|
||||
keyPath?: ComponentProps<typeof JSONTree>['keyPath'];
|
||||
}
|
||||
|
||||
export class TreeView extends React.PureComponent<TreeViewProps> {
|
||||
readonly labelRenderer: ReturnType<typeof createTreeItemLabelRenderer>;
|
||||
|
||||
constructor(props: TreeViewProps) {
|
||||
super(props);
|
||||
this.labelRenderer = createTreeItemLabelRenderer(this.props.styling);
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
const {
|
||||
styling,
|
||||
base16Theme,
|
||||
invertTheme,
|
||||
isWideLayout,
|
||||
data,
|
||||
before,
|
||||
after,
|
||||
children,
|
||||
keyPath,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div {...styling('treeWrapper')}>
|
||||
{before}
|
||||
<JSONTree
|
||||
keyPath={keyPath}
|
||||
data={data}
|
||||
labelRenderer={this.labelRenderer}
|
||||
theme={getJsonTreeTheme(base16Theme)}
|
||||
invertTheme={invertTheme}
|
||||
getItemString={(type, data) =>
|
||||
getItemString(styling, type, data, DATA_TYPE_KEY, isWideLayout)
|
||||
}
|
||||
hideRoot
|
||||
/>
|
||||
{after}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1 +1,3 @@
|
|||
export const DATA_TYPE_KEY = Symbol.for('__serializedType__');
|
||||
|
||||
export const missingTagId = '__internal_without_id';
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
QueryInfo,
|
||||
RtkQueryInspectorMonitorState,
|
||||
QueryFormValues,
|
||||
QueryPreviewTabs,
|
||||
} from './types';
|
||||
import { QueryComparators } from './utils/comparators';
|
||||
|
||||
|
@ -16,6 +17,7 @@ const initialState: RtkQueryInspectorMonitorState = {
|
|||
searchValue: '',
|
||||
},
|
||||
},
|
||||
selectedPreviewTab: QueryPreviewTabs.queryinfo,
|
||||
selectedQueryKey: null,
|
||||
};
|
||||
|
||||
|
@ -42,6 +44,9 @@ const monitorSlice = createSlice({
|
|||
reducerPath: action.payload.reducerPath,
|
||||
};
|
||||
},
|
||||
selectedPreviewTab(state, action: PayloadAction<QueryPreviewTabs>) {
|
||||
state.selectedPreviewTab = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -53,4 +58,8 @@ export function reducer<S, A extends Action<unknown>>(
|
|||
return monitorSlice.reducer(state, action);
|
||||
}
|
||||
|
||||
export const { selectQueryKey, changeQueryFormValues } = monitorSlice.actions;
|
||||
export const {
|
||||
selectQueryKey,
|
||||
changeQueryFormValues,
|
||||
selectedPreviewTab,
|
||||
} = monitorSlice.actions;
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { Action, createSelector, Selector } from '@reduxjs/toolkit';
|
||||
import { RtkQueryInspectorProps } from './RtkQueryInspector';
|
||||
import { QueryInfo, SelectorsSource } from './types';
|
||||
import { QueryInfo, RtkQueryTag, SelectorsSource } from './types';
|
||||
import { Comparator, queryComparators } from './utils/comparators';
|
||||
import { escapeRegExpSpecialCharacter } from './utils/regexp';
|
||||
import {
|
||||
getApiStatesOf,
|
||||
extractAllApiQueries,
|
||||
flipComparator,
|
||||
getQueryTagsOf,
|
||||
} from './utils/rtk-query';
|
||||
|
||||
type InspectorSelector<S, Output> = Selector<SelectorsSource<S>, Output>;
|
||||
|
@ -47,6 +48,7 @@ export interface InspectorSelectors<S> {
|
|||
readonly selectAllVisbileQueries: InspectorSelector<S, QueryInfo[]>;
|
||||
readonly selectorCurrentQueryInfo: InspectorSelector<S, QueryInfo | null>;
|
||||
readonly selectSearchQueryRegex: InspectorSelector<S, RegExp | null>;
|
||||
readonly selectCurrentQueryTags: InspectorSelector<S, RtkQueryTag[]>;
|
||||
}
|
||||
|
||||
export function createInspectorSelectors<S>(): InspectorSelectors<S> {
|
||||
|
@ -116,6 +118,12 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
|
|||
}
|
||||
);
|
||||
|
||||
const selectCurrentQueryTags = createSelector(
|
||||
selectApiStates,
|
||||
selectorCurrentQueryInfo,
|
||||
(apiState, currentQueryInfo) => getQueryTagsOf(currentQueryInfo, apiState)
|
||||
);
|
||||
|
||||
return {
|
||||
selectQueryComparator,
|
||||
selectApiStates,
|
||||
|
@ -123,5 +131,6 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
|
|||
selectAllVisbileQueries,
|
||||
selectSearchQueryRegex,
|
||||
selectorCurrentQueryInfo,
|
||||
selectCurrentQueryTags,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,13 +4,12 @@ import {
|
|||
createStyling,
|
||||
getBase16Theme,
|
||||
invertTheme,
|
||||
StylingFunction,
|
||||
StylingConfig,
|
||||
} from 'react-base16-styling';
|
||||
import rgba from 'hex-rgba';
|
||||
import * as reduxThemes from 'redux-devtools-themes';
|
||||
import { Action } from 'redux';
|
||||
import { RtkQueryInspectorMonitorProps } from '../types';
|
||||
import { RtkQueryInspectorMonitorProps, StyleUtils } from '../types';
|
||||
import { createContext } from 'react';
|
||||
|
||||
jss.setup(preset());
|
||||
|
@ -319,12 +318,6 @@ export const createStylingFromTheme = createStyling(getDefaultThemeStyling, {
|
|||
base16Themes: { ...reduxThemes },
|
||||
});
|
||||
|
||||
export interface StyleUtils {
|
||||
base16Theme: reduxThemes.Base16Theme;
|
||||
styling: StylingFunction;
|
||||
invertTheme: boolean;
|
||||
}
|
||||
|
||||
export function createThemeState<S, A extends Action<unknown>>(
|
||||
props: RtkQueryInspectorMonitorProps<S, A>
|
||||
): StyleUtils {
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
import { LiftedAction, LiftedState } from '@redux-devtools/instrument';
|
||||
import type { createApi } from '@reduxjs/toolkit/query';
|
||||
import { Dispatch } from 'react';
|
||||
import { Base16Theme } from 'react-base16-styling';
|
||||
import { ComponentType, Dispatch } from 'react';
|
||||
import { Base16Theme, StylingFunction } from 'react-base16-styling';
|
||||
import { Action } from 'redux';
|
||||
import * as themes from 'redux-devtools-themes';
|
||||
import { QueryComparators } from './utils/comparators';
|
||||
|
||||
export enum QueryPreviewTabs {
|
||||
queryinfo,
|
||||
apiConfig,
|
||||
querySubscriptions,
|
||||
queryTags,
|
||||
}
|
||||
|
||||
export interface QueryFormValues {
|
||||
queryComparator: QueryComparators;
|
||||
isAscendingQueryComparatorOrder: boolean;
|
||||
|
@ -16,6 +23,7 @@ export interface RtkQueryInspectorMonitorState {
|
|||
values: QueryFormValues;
|
||||
};
|
||||
readonly selectedQueryKey: Pick<QueryInfo, 'reducerPath' | 'queryKey'> | null;
|
||||
readonly selectedPreviewTab: QueryPreviewTabs;
|
||||
}
|
||||
|
||||
export interface RtkQueryInspectorMonitorProps<S, A extends Action<unknown>>
|
||||
|
@ -42,6 +50,10 @@ export type RtkQueryState = NonNullable<
|
|||
RtkQueryApiState['queries'][keyof RtkQueryApiState]
|
||||
>;
|
||||
|
||||
export type RtkQueryApiConfig = RtkQueryApiState['config'];
|
||||
|
||||
export type RtkQueryProvided = RtkQueryApiState['provided'];
|
||||
|
||||
export interface ExternalProps<S, A extends Action<unknown>> {
|
||||
dispatch: Dispatch<
|
||||
Action | LiftedAction<S, A, RtkQueryInspectorMonitorState>
|
||||
|
@ -79,3 +91,35 @@ export interface SelectorsSource<S> {
|
|||
userState: S | null;
|
||||
monitorState: RtkQueryInspectorMonitorState;
|
||||
}
|
||||
|
||||
export interface StyleUtils {
|
||||
readonly base16Theme: Base16Theme;
|
||||
readonly styling: StylingFunction;
|
||||
readonly invertTheme: boolean;
|
||||
}
|
||||
|
||||
export type RTKQuerySubscribers = NonNullable<
|
||||
RtkQueryApiState['subscriptions'][keyof RtkQueryApiState['subscriptions']]
|
||||
>;
|
||||
|
||||
export interface RtkQueryTag {
|
||||
type: string;
|
||||
id?: number | string;
|
||||
}
|
||||
|
||||
export interface QueryPreviewTabProps extends StyleUtils {
|
||||
queryInfo: QueryInfo | null;
|
||||
apiConfig: RtkQueryApiState['config'] | null;
|
||||
querySubscriptions: RTKQuerySubscribers | null;
|
||||
isWideLayout: boolean;
|
||||
tags: RtkQueryTag[];
|
||||
}
|
||||
|
||||
export interface TabOption<S, P> extends SelectOption<S> {
|
||||
component: ComponentType<P>;
|
||||
}
|
||||
|
||||
export type QueryPreviewTabOption = TabOption<
|
||||
QueryPreviewTabs,
|
||||
QueryPreviewTabProps
|
||||
>;
|
||||
|
|
|
@ -10,13 +10,15 @@ export enum QueryComparators {
|
|||
queryKey = 'key',
|
||||
status = 'status',
|
||||
endpointName = 'endpointName',
|
||||
apiReducerPath = 'apiReducerPath',
|
||||
}
|
||||
|
||||
export const sortQueryOptions: SelectOption<QueryComparators>[] = [
|
||||
{ label: 'fulfilledTimeStamp', value: QueryComparators.fulfilledTimeStamp },
|
||||
{ label: 'query key', value: QueryComparators.queryKey },
|
||||
{ label: 'status ', value: QueryComparators.status },
|
||||
{ label: 'status', value: QueryComparators.status },
|
||||
{ label: 'endpoint', value: QueryComparators.endpointName },
|
||||
{ label: 'reducerPath', value: QueryComparators.apiReducerPath },
|
||||
];
|
||||
|
||||
function sortQueryByFulfilled(
|
||||
|
@ -46,7 +48,10 @@ function sortQueryByStatus(
|
|||
return thisTerm - thatTerm;
|
||||
}
|
||||
|
||||
function compareStrings(a: string, b: string): number {
|
||||
function compareJSONPrimitive<T extends string | number | boolean | null>(
|
||||
a: T,
|
||||
b: T
|
||||
): number {
|
||||
if (a === b) {
|
||||
return 0;
|
||||
}
|
||||
|
@ -58,7 +63,7 @@ function sortByQueryKey(
|
|||
thisQueryInfo: QueryInfo,
|
||||
thatQueryInfo: QueryInfo
|
||||
): number {
|
||||
return compareStrings(thisQueryInfo.queryKey, thatQueryInfo.queryKey);
|
||||
return compareJSONPrimitive(thisQueryInfo.queryKey, thatQueryInfo.queryKey);
|
||||
}
|
||||
|
||||
function sortQueryByEndpointName(
|
||||
|
@ -68,7 +73,17 @@ function sortQueryByEndpointName(
|
|||
const thisEndpointName = thisQueryInfo.query.endpointName ?? '';
|
||||
const thatEndpointName = thatQueryInfo.query.endpointName ?? '';
|
||||
|
||||
return compareStrings(thisEndpointName, thatEndpointName);
|
||||
return compareJSONPrimitive(thisEndpointName, thatEndpointName);
|
||||
}
|
||||
|
||||
function sortByApiReducerPath(
|
||||
thisQueryInfo: QueryInfo,
|
||||
thatQueryInfo: QueryInfo
|
||||
): number {
|
||||
return compareJSONPrimitive(
|
||||
thisQueryInfo.reducerPath,
|
||||
thatQueryInfo.reducerPath
|
||||
);
|
||||
}
|
||||
|
||||
export const queryComparators: Readonly<Record<
|
||||
|
@ -79,4 +94,5 @@ export const queryComparators: Readonly<Record<
|
|||
[QueryComparators.status]: sortQueryByStatus,
|
||||
[QueryComparators.endpointName]: sortQueryByEndpointName,
|
||||
[QueryComparators.queryKey]: sortByQueryKey,
|
||||
[QueryComparators.apiReducerPath]: sortByApiReducerPath,
|
||||
};
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { isPlainObject } from '@reduxjs/toolkit';
|
||||
import type { createApi } from '@reduxjs/toolkit/query';
|
||||
import { QueryInfo, RtkQueryInspectorMonitorState } from '../types';
|
||||
import {
|
||||
QueryInfo,
|
||||
RtkQueryInspectorMonitorState,
|
||||
RtkQueryApiState,
|
||||
RTKQuerySubscribers,
|
||||
RtkQueryTag,
|
||||
} from '../types';
|
||||
import { missingTagId } from '../monitor-config';
|
||||
import { Comparator } from './comparators';
|
||||
import { emptyArray } from './object';
|
||||
|
||||
export type RtkQueryApiState = ReturnType<
|
||||
ReturnType<typeof createApi>['reducer']
|
||||
>;
|
||||
|
||||
const rtkqueryApiStateKeys: ReadonlyArray<keyof RtkQueryApiState> = [
|
||||
'queries',
|
||||
'mutations',
|
||||
|
@ -107,3 +109,76 @@ export function isQuerySelected(
|
|||
selectedQueryKey.reducerPath === queryInfo.reducerPath
|
||||
);
|
||||
}
|
||||
|
||||
export function getApiStateOf(
|
||||
queryInfo: QueryInfo | null,
|
||||
apiStates: ReturnType<typeof getApiStatesOf>
|
||||
): RtkQueryApiState | null {
|
||||
if (!apiStates || !queryInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return apiStates[queryInfo.reducerPath] ?? null;
|
||||
}
|
||||
|
||||
export function getQuerySubscriptionsOf(
|
||||
queryInfo: QueryInfo | null,
|
||||
apiStates: ReturnType<typeof getApiStatesOf>
|
||||
): RTKQuerySubscribers | null {
|
||||
if (!apiStates || !queryInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
apiStates[queryInfo.reducerPath]?.subscriptions?.[queryInfo.queryKey] ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export function getProvidedOf(
|
||||
queryInfo: QueryInfo | null,
|
||||
apiStates: ReturnType<typeof getApiStatesOf>
|
||||
): RtkQueryApiState['provided'] | null {
|
||||
if (!apiStates || !queryInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return apiStates[queryInfo.reducerPath]?.provided ?? null;
|
||||
}
|
||||
|
||||
export function getQueryTagsOf(
|
||||
queryInfo: QueryInfo | null,
|
||||
apiStates: ReturnType<typeof getApiStatesOf>
|
||||
): RtkQueryTag[] {
|
||||
if (!apiStates || !queryInfo) {
|
||||
return emptyArray;
|
||||
}
|
||||
|
||||
const provided = apiStates[queryInfo.reducerPath].provided;
|
||||
|
||||
const tagTypes = Object.keys(provided);
|
||||
console.log({ tagTypes, provided });
|
||||
if (tagTypes.length < 1) {
|
||||
return emptyArray;
|
||||
}
|
||||
|
||||
const output: RtkQueryTag[] = [];
|
||||
|
||||
for (const [type, tagIds] of Object.entries(provided)) {
|
||||
if (tagIds) {
|
||||
for (const [id, queryKeys] of Object.entries(tagIds)) {
|
||||
if (queryKeys.includes(queryInfo.queryKey as any)) {
|
||||
const tag: RtkQueryTag = { type };
|
||||
|
||||
if (id !== missingTagId) {
|
||||
tag.id = id;
|
||||
}
|
||||
|
||||
output.push(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user