mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-09-26 14:06:41 +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({
|
export const pokemonApi = createApi({
|
||||||
reducerPath: 'pokemonApi',
|
reducerPath: 'pokemonApi',
|
||||||
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
|
||||||
|
tagTypes: ['pokemon'],
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
getPokemonByName: builder.query({
|
getPokemonByName: builder.query({
|
||||||
query: (name: PokemonName) => `pokemon/${name}`,
|
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 {
|
import {
|
||||||
QueryFormValues,
|
QueryFormValues,
|
||||||
QueryInfo,
|
QueryInfo,
|
||||||
|
QueryPreviewTabs,
|
||||||
RtkQueryInspectorMonitorState,
|
RtkQueryInspectorMonitorState,
|
||||||
|
StyleUtils,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { createInspectorSelectors, computeSelectorSource } from './selectors';
|
import { createInspectorSelectors, computeSelectorSource } from './selectors';
|
||||||
import { changeQueryFormValues, selectQueryKey } from './reducers';
|
import {
|
||||||
|
changeQueryFormValues,
|
||||||
|
selectedPreviewTab,
|
||||||
|
selectQueryKey,
|
||||||
|
} from './reducers';
|
||||||
import { QueryList } from './components/QueryList';
|
import { QueryList } from './components/QueryList';
|
||||||
import { StyleUtils } from './styles/createStylingFromTheme';
|
|
||||||
import { QueryForm } from './components/QueryForm';
|
import { QueryForm } from './components/QueryForm';
|
||||||
import { QueryPreview } from './components/QueryPreview';
|
import { QueryPreview } from './components/QueryPreview';
|
||||||
|
import { getApiStateOf, getQuerySubscriptionsOf } from './utils/rtk-query';
|
||||||
|
|
||||||
type SelectorsSource<S> = {
|
type SelectorsSource<S> = {
|
||||||
userState: S | null;
|
userState: S | null;
|
||||||
|
@ -104,6 +110,10 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
|
||||||
this.props.dispatch(selectQueryKey(queryInfo) as AnyAction);
|
this.props.dispatch(selectQueryKey(queryInfo) as AnyAction);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleTabChange = (tab: QueryPreviewTabs): void => {
|
||||||
|
this.props.dispatch(selectedPreviewTab(tab) as AnyAction);
|
||||||
|
};
|
||||||
|
|
||||||
render(): ReactNode {
|
render(): ReactNode {
|
||||||
const { selectorsSource, isWideLayout } = this.state;
|
const { selectorsSource, isWideLayout } = this.state;
|
||||||
const {
|
const {
|
||||||
|
@ -118,11 +128,21 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
|
||||||
selectorsSource
|
selectorsSource
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const currentRtkApi = getApiStateOf(currentQueryInfo, apiStates);
|
||||||
|
const currentQuerySubscriptions = getQuerySubscriptionsOf(
|
||||||
|
currentQueryInfo,
|
||||||
|
apiStates
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentTags = this.selectors.selectCurrentQueryTags(selectorsSource);
|
||||||
|
|
||||||
console.log('inspector', {
|
console.log('inspector', {
|
||||||
apiStates,
|
apiStates,
|
||||||
allVisibleQueries,
|
allVisibleQueries,
|
||||||
selectorsSource,
|
selectorsSource,
|
||||||
currentQueryInfo,
|
currentQueryInfo,
|
||||||
|
currentRtkApi,
|
||||||
|
currentTags,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -147,8 +167,13 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<QueryPreview
|
<QueryPreview
|
||||||
selectedQueryInfo={currentQueryInfo}
|
queryInfo={currentQueryInfo}
|
||||||
|
selectedTab={selectorsSource.monitorState.selectedPreviewTab}
|
||||||
|
onTabChange={this.handleTabChange}
|
||||||
styling={styling}
|
styling={styling}
|
||||||
|
tags={currentTags}
|
||||||
|
querySubscriptions={currentQuerySubscriptions}
|
||||||
|
apiConfig={currentRtkApi?.config ?? null}
|
||||||
isWideLayout={isWideLayout}
|
isWideLayout={isWideLayout}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,20 +1,46 @@
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import JSONTree from 'react-json-tree';
|
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
|
||||||
import { StylingFunction } from 'react-base16-styling';
|
import { createTreeItemLabelRenderer } from '../styles/tree';
|
||||||
import { DATA_TYPE_KEY } from '../monitor-config';
|
|
||||||
import {
|
import {
|
||||||
getJsonTreeTheme,
|
QueryPreviewTabOption,
|
||||||
StyleUtilsContext,
|
QueryPreviewTabs,
|
||||||
} from '../styles/createStylingFromTheme';
|
QueryPreviewTabProps,
|
||||||
import { createTreeItemLabelRenderer, getItemString } from '../styles/tree';
|
} from '../types';
|
||||||
import { QueryInfo } 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 {
|
export interface QueryPreviewProps
|
||||||
selectedQueryInfo: QueryInfo | null;
|
extends Omit<QueryPreviewTabProps, 'base16Theme' | 'invertTheme'> {
|
||||||
styling: StylingFunction;
|
selectedTab: QueryPreviewTabs;
|
||||||
isWideLayout: boolean;
|
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> {
|
export class QueryPreview extends React.PureComponent<QueryPreviewProps> {
|
||||||
readonly labelRenderer: ReturnType<typeof createTreeItemLabelRenderer>;
|
readonly labelRenderer: ReturnType<typeof createTreeItemLabelRenderer>;
|
||||||
|
|
||||||
|
@ -25,68 +51,55 @@ export class QueryPreview extends React.PureComponent<QueryPreviewProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): ReactNode {
|
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 (
|
return (
|
||||||
<StyleUtilsContext.Consumer>
|
<StyleUtilsContext.Consumer>
|
||||||
{({ styling }) => <div {...styling('queryPreview')} />}
|
{({ styling }) => (
|
||||||
|
<div {...styling('queryPreview')}>
|
||||||
|
<QueryPreviewHeader
|
||||||
|
selectedTab={selectedTab}
|
||||||
|
onTabChange={onTabChange}
|
||||||
|
tabs={tabs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</StyleUtilsContext.Consumer>
|
</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 (
|
return (
|
||||||
<StyleUtilsContext.Consumer>
|
<StyleUtilsContext.Consumer>
|
||||||
{({ styling, base16Theme, invertTheme }) => {
|
{({ styling, base16Theme, invertTheme }) => {
|
||||||
return (
|
return (
|
||||||
<div {...styling('queryPreview')}>
|
<div {...styling('queryPreview')}>
|
||||||
<React.Fragment>
|
<QueryPreviewHeader
|
||||||
<div {...styling('previewHeader')}></div>
|
selectedTab={selectedTab}
|
||||||
<ul>
|
onTabChange={onTabChange}
|
||||||
<li>{`reducerPath: ${reducerPath ?? '-'}`}</li>
|
tabs={tabs}
|
||||||
<li>{`endpointName: ${endpointName ?? '-'}`}</li>
|
/>
|
||||||
<li>{`status: ${status}`}</li>
|
<TabComponent
|
||||||
<li>{`loaded at: ${latestFetch}`}</li>
|
styling={styling}
|
||||||
<li>{`requested at: ${startedAt}`}</li>
|
base16Theme={base16Theme}
|
||||||
</ul>
|
invertTheme={invertTheme}
|
||||||
<div {...styling('treeWrapper')}>
|
querySubscriptions={querySubscriptions}
|
||||||
<JSONTree
|
queryInfo={queryInfo}
|
||||||
data={data}
|
tags={tags}
|
||||||
labelRenderer={this.labelRenderer}
|
apiConfig={apiConfig}
|
||||||
theme={getJsonTreeTheme(base16Theme)}
|
isWideLayout={isWideLayout}
|
||||||
invertTheme={invertTheme}
|
|
||||||
getItemString={(type, data) =>
|
|
||||||
getItemString(
|
|
||||||
styling,
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
DATA_TYPE_KEY,
|
|
||||||
isWideLayout
|
|
||||||
)
|
|
||||||
}
|
|
||||||
hideRoot
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
</div>
|
</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 DATA_TYPE_KEY = Symbol.for('__serializedType__');
|
||||||
|
|
||||||
|
export const missingTagId = '__internal_without_id';
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
QueryInfo,
|
QueryInfo,
|
||||||
RtkQueryInspectorMonitorState,
|
RtkQueryInspectorMonitorState,
|
||||||
QueryFormValues,
|
QueryFormValues,
|
||||||
|
QueryPreviewTabs,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { QueryComparators } from './utils/comparators';
|
import { QueryComparators } from './utils/comparators';
|
||||||
|
|
||||||
|
@ -16,6 +17,7 @@ const initialState: RtkQueryInspectorMonitorState = {
|
||||||
searchValue: '',
|
searchValue: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
selectedPreviewTab: QueryPreviewTabs.queryinfo,
|
||||||
selectedQueryKey: null,
|
selectedQueryKey: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -42,6 +44,9 @@ const monitorSlice = createSlice({
|
||||||
reducerPath: action.payload.reducerPath,
|
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);
|
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 { Action, createSelector, Selector } from '@reduxjs/toolkit';
|
||||||
import { RtkQueryInspectorProps } from './RtkQueryInspector';
|
import { RtkQueryInspectorProps } from './RtkQueryInspector';
|
||||||
import { QueryInfo, SelectorsSource } from './types';
|
import { QueryInfo, RtkQueryTag, SelectorsSource } from './types';
|
||||||
import { Comparator, queryComparators } from './utils/comparators';
|
import { Comparator, queryComparators } from './utils/comparators';
|
||||||
import { escapeRegExpSpecialCharacter } from './utils/regexp';
|
import { escapeRegExpSpecialCharacter } from './utils/regexp';
|
||||||
import {
|
import {
|
||||||
getApiStatesOf,
|
getApiStatesOf,
|
||||||
extractAllApiQueries,
|
extractAllApiQueries,
|
||||||
flipComparator,
|
flipComparator,
|
||||||
|
getQueryTagsOf,
|
||||||
} from './utils/rtk-query';
|
} from './utils/rtk-query';
|
||||||
|
|
||||||
type InspectorSelector<S, Output> = Selector<SelectorsSource<S>, Output>;
|
type InspectorSelector<S, Output> = Selector<SelectorsSource<S>, Output>;
|
||||||
|
@ -47,6 +48,7 @@ export interface InspectorSelectors<S> {
|
||||||
readonly selectAllVisbileQueries: InspectorSelector<S, QueryInfo[]>;
|
readonly selectAllVisbileQueries: InspectorSelector<S, QueryInfo[]>;
|
||||||
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[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createInspectorSelectors<S>(): InspectorSelectors<S> {
|
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 {
|
return {
|
||||||
selectQueryComparator,
|
selectQueryComparator,
|
||||||
selectApiStates,
|
selectApiStates,
|
||||||
|
@ -123,5 +131,6 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
|
||||||
selectAllVisbileQueries,
|
selectAllVisbileQueries,
|
||||||
selectSearchQueryRegex,
|
selectSearchQueryRegex,
|
||||||
selectorCurrentQueryInfo,
|
selectorCurrentQueryInfo,
|
||||||
|
selectCurrentQueryTags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,12 @@ import {
|
||||||
createStyling,
|
createStyling,
|
||||||
getBase16Theme,
|
getBase16Theme,
|
||||||
invertTheme,
|
invertTheme,
|
||||||
StylingFunction,
|
|
||||||
StylingConfig,
|
StylingConfig,
|
||||||
} from 'react-base16-styling';
|
} from 'react-base16-styling';
|
||||||
import rgba from 'hex-rgba';
|
import rgba from 'hex-rgba';
|
||||||
import * as reduxThemes from 'redux-devtools-themes';
|
import * as reduxThemes from 'redux-devtools-themes';
|
||||||
import { Action } from 'redux';
|
import { Action } from 'redux';
|
||||||
import { RtkQueryInspectorMonitorProps } from '../types';
|
import { RtkQueryInspectorMonitorProps, StyleUtils } from '../types';
|
||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
jss.setup(preset());
|
jss.setup(preset());
|
||||||
|
@ -319,12 +318,6 @@ export const createStylingFromTheme = createStyling(getDefaultThemeStyling, {
|
||||||
base16Themes: { ...reduxThemes },
|
base16Themes: { ...reduxThemes },
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface StyleUtils {
|
|
||||||
base16Theme: reduxThemes.Base16Theme;
|
|
||||||
styling: StylingFunction;
|
|
||||||
invertTheme: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createThemeState<S, A extends Action<unknown>>(
|
export function createThemeState<S, A extends Action<unknown>>(
|
||||||
props: RtkQueryInspectorMonitorProps<S, A>
|
props: RtkQueryInspectorMonitorProps<S, A>
|
||||||
): StyleUtils {
|
): StyleUtils {
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
import { LiftedAction, LiftedState } from '@redux-devtools/instrument';
|
import { LiftedAction, LiftedState } from '@redux-devtools/instrument';
|
||||||
import type { createApi } from '@reduxjs/toolkit/query';
|
import type { createApi } from '@reduxjs/toolkit/query';
|
||||||
import { Dispatch } from 'react';
|
import { ComponentType, Dispatch } from 'react';
|
||||||
import { Base16Theme } from 'react-base16-styling';
|
import { Base16Theme, StylingFunction } from 'react-base16-styling';
|
||||||
import { Action } from 'redux';
|
import { Action } from 'redux';
|
||||||
import * as themes from 'redux-devtools-themes';
|
import * as themes from 'redux-devtools-themes';
|
||||||
import { QueryComparators } from './utils/comparators';
|
import { QueryComparators } from './utils/comparators';
|
||||||
|
|
||||||
|
export enum QueryPreviewTabs {
|
||||||
|
queryinfo,
|
||||||
|
apiConfig,
|
||||||
|
querySubscriptions,
|
||||||
|
queryTags,
|
||||||
|
}
|
||||||
|
|
||||||
export interface QueryFormValues {
|
export interface QueryFormValues {
|
||||||
queryComparator: QueryComparators;
|
queryComparator: QueryComparators;
|
||||||
isAscendingQueryComparatorOrder: boolean;
|
isAscendingQueryComparatorOrder: boolean;
|
||||||
|
@ -16,6 +23,7 @@ export interface RtkQueryInspectorMonitorState {
|
||||||
values: QueryFormValues;
|
values: QueryFormValues;
|
||||||
};
|
};
|
||||||
readonly selectedQueryKey: Pick<QueryInfo, 'reducerPath' | 'queryKey'> | null;
|
readonly selectedQueryKey: Pick<QueryInfo, 'reducerPath' | 'queryKey'> | null;
|
||||||
|
readonly selectedPreviewTab: QueryPreviewTabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RtkQueryInspectorMonitorProps<S, A extends Action<unknown>>
|
export interface RtkQueryInspectorMonitorProps<S, A extends Action<unknown>>
|
||||||
|
@ -42,6 +50,10 @@ export type RtkQueryState = NonNullable<
|
||||||
RtkQueryApiState['queries'][keyof RtkQueryApiState]
|
RtkQueryApiState['queries'][keyof RtkQueryApiState]
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type RtkQueryApiConfig = RtkQueryApiState['config'];
|
||||||
|
|
||||||
|
export type RtkQueryProvided = RtkQueryApiState['provided'];
|
||||||
|
|
||||||
export interface ExternalProps<S, A extends Action<unknown>> {
|
export interface ExternalProps<S, A extends Action<unknown>> {
|
||||||
dispatch: Dispatch<
|
dispatch: Dispatch<
|
||||||
Action | LiftedAction<S, A, RtkQueryInspectorMonitorState>
|
Action | LiftedAction<S, A, RtkQueryInspectorMonitorState>
|
||||||
|
@ -79,3 +91,35 @@ export interface SelectorsSource<S> {
|
||||||
userState: S | null;
|
userState: S | null;
|
||||||
monitorState: RtkQueryInspectorMonitorState;
|
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,6 +10,7 @@ export enum QueryComparators {
|
||||||
queryKey = 'key',
|
queryKey = 'key',
|
||||||
status = 'status',
|
status = 'status',
|
||||||
endpointName = 'endpointName',
|
endpointName = 'endpointName',
|
||||||
|
apiReducerPath = 'apiReducerPath',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sortQueryOptions: SelectOption<QueryComparators>[] = [
|
export const sortQueryOptions: SelectOption<QueryComparators>[] = [
|
||||||
|
@ -17,6 +18,7 @@ export const sortQueryOptions: SelectOption<QueryComparators>[] = [
|
||||||
{ label: 'query key', value: QueryComparators.queryKey },
|
{ label: 'query key', value: QueryComparators.queryKey },
|
||||||
{ label: 'status', value: QueryComparators.status },
|
{ label: 'status', value: QueryComparators.status },
|
||||||
{ label: 'endpoint', value: QueryComparators.endpointName },
|
{ label: 'endpoint', value: QueryComparators.endpointName },
|
||||||
|
{ label: 'reducerPath', value: QueryComparators.apiReducerPath },
|
||||||
];
|
];
|
||||||
|
|
||||||
function sortQueryByFulfilled(
|
function sortQueryByFulfilled(
|
||||||
|
@ -46,7 +48,10 @@ function sortQueryByStatus(
|
||||||
return thisTerm - thatTerm;
|
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) {
|
if (a === b) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -58,7 +63,7 @@ function sortByQueryKey(
|
||||||
thisQueryInfo: QueryInfo,
|
thisQueryInfo: QueryInfo,
|
||||||
thatQueryInfo: QueryInfo
|
thatQueryInfo: QueryInfo
|
||||||
): number {
|
): number {
|
||||||
return compareStrings(thisQueryInfo.queryKey, thatQueryInfo.queryKey);
|
return compareJSONPrimitive(thisQueryInfo.queryKey, thatQueryInfo.queryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortQueryByEndpointName(
|
function sortQueryByEndpointName(
|
||||||
|
@ -68,7 +73,17 @@ function sortQueryByEndpointName(
|
||||||
const thisEndpointName = thisQueryInfo.query.endpointName ?? '';
|
const thisEndpointName = thisQueryInfo.query.endpointName ?? '';
|
||||||
const thatEndpointName = thatQueryInfo.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<
|
export const queryComparators: Readonly<Record<
|
||||||
|
@ -79,4 +94,5 @@ export const queryComparators: Readonly<Record<
|
||||||
[QueryComparators.status]: sortQueryByStatus,
|
[QueryComparators.status]: sortQueryByStatus,
|
||||||
[QueryComparators.endpointName]: sortQueryByEndpointName,
|
[QueryComparators.endpointName]: sortQueryByEndpointName,
|
||||||
[QueryComparators.queryKey]: sortByQueryKey,
|
[QueryComparators.queryKey]: sortByQueryKey,
|
||||||
|
[QueryComparators.apiReducerPath]: sortByApiReducerPath,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { isPlainObject } from '@reduxjs/toolkit';
|
import { isPlainObject } from '@reduxjs/toolkit';
|
||||||
import type { createApi } from '@reduxjs/toolkit/query';
|
import {
|
||||||
import { QueryInfo, RtkQueryInspectorMonitorState } from '../types';
|
QueryInfo,
|
||||||
|
RtkQueryInspectorMonitorState,
|
||||||
|
RtkQueryApiState,
|
||||||
|
RTKQuerySubscribers,
|
||||||
|
RtkQueryTag,
|
||||||
|
} from '../types';
|
||||||
|
import { missingTagId } from '../monitor-config';
|
||||||
import { Comparator } from './comparators';
|
import { Comparator } from './comparators';
|
||||||
import { emptyArray } from './object';
|
import { emptyArray } from './object';
|
||||||
|
|
||||||
export type RtkQueryApiState = ReturnType<
|
|
||||||
ReturnType<typeof createApi>['reducer']
|
|
||||||
>;
|
|
||||||
|
|
||||||
const rtkqueryApiStateKeys: ReadonlyArray<keyof RtkQueryApiState> = [
|
const rtkqueryApiStateKeys: ReadonlyArray<keyof RtkQueryApiState> = [
|
||||||
'queries',
|
'queries',
|
||||||
'mutations',
|
'mutations',
|
||||||
|
@ -107,3 +109,76 @@ export function isQuerySelected(
|
||||||
selectedQueryKey.reducerPath === queryInfo.reducerPath
|
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