feat(rtk-query): display mutations in queryList

This commit is contained in:
FaberVitale 2021-08-01 15:35:22 +02:00
parent 95da893616
commit e4c5720d87
12 changed files with 296 additions and 123 deletions

View File

@ -1,18 +1,18 @@
import React, { PureComponent, ReactNode } from 'react'; import React, { PureComponent, ReactNode } from 'react';
import { StyleUtilsContext } from '../styles/createStylingFromTheme'; import { StyleUtilsContext } from '../styles/createStylingFromTheme';
import { QueryInfo, RtkQueryMonitorState } from '../types'; import { RtkResourceInfo, RtkQueryMonitorState } from '../types';
import { isQuerySelected } from '../utils/rtk-query'; import { isQuerySelected } from '../utils/rtk-query';
export interface QueryListProps { export interface QueryListProps {
queryInfos: QueryInfo[]; resInfos: RtkResourceInfo[];
selectedQueryKey: RtkQueryMonitorState['selectedQueryKey']; selectedQueryKey: RtkQueryMonitorState['selectedQueryKey'];
onSelectQuery: (query: QueryInfo) => void; onSelectQuery: (query: RtkResourceInfo) => void;
} }
export class QueryList extends PureComponent<QueryListProps> { export class QueryList extends PureComponent<QueryListProps> {
static isItemSelected( static isItemSelected(
selectedQueryKey: QueryListProps['selectedQueryKey'], selectedQueryKey: QueryListProps['selectedQueryKey'],
queryInfo: QueryInfo queryInfo: RtkResourceInfo
): boolean { ): boolean {
return ( return (
!!selectedQueryKey && !!selectedQueryKey &&
@ -21,27 +21,43 @@ export class QueryList extends PureComponent<QueryListProps> {
); );
} }
static formatQuery(resInfo: RtkResourceInfo): string {
const key =
resInfo.type === 'query'
? resInfo.queryKey
: `${resInfo.state.endpointName ?? ''} ${resInfo.queryKey}`;
return key;
}
render(): ReactNode { render(): ReactNode {
const { queryInfos, selectedQueryKey, onSelectQuery } = this.props; const { resInfos, selectedQueryKey, onSelectQuery } = this.props;
return ( return (
<StyleUtilsContext.Consumer> <StyleUtilsContext.Consumer>
{({ styling }) => ( {({ styling }) => (
<ul {...styling('queryList')}> <ul {...styling('queryList')}>
{queryInfos.map((queryInfo) => { {resInfos.map((resInfo) => {
const isSelected = isQuerySelected(selectedQueryKey, queryInfo); const isSelected = isQuerySelected(selectedQueryKey, resInfo);
return ( return (
<li <li
key={queryInfo.queryKey} key={resInfo.queryKey}
onClick={() => onSelectQuery(queryInfo)} onClick={() => onSelectQuery(resInfo)}
{...styling( {...styling(
['queryListItem', isSelected && 'queryListItemSelected'], ['queryListItem', isSelected && 'queryListItemSelected'],
isSelected isSelected
)} )}
> >
<p {...styling('queryListItemKey')}>{queryInfo.queryKey}</p> <p {...styling('queryListItemKey')}>
<p {...styling('queryStatus')}>{queryInfo.query.status}</p> {QueryList.formatQuery(resInfo)}
</p>
<div {...styling('queryStatusWrapper')}>
<strong {...styling(['queryStatus', 'queryType'])}>
{resInfo.type === 'query' ? 'Q' : 'M'}
</strong>
<p {...styling('queryStatus')}>{resInfo.state.status}</p>
</div>
</li> </li>
); );
})} })}

View File

@ -4,7 +4,9 @@ import { QueryPreviewTabs, TabOption } from '../types';
import { emptyArray } from '../utils/object'; import { emptyArray } from '../utils/object';
export interface QueryPreviewHeaderProps { export interface QueryPreviewHeaderProps {
tabs: ReadonlyArray<TabOption<QueryPreviewTabs, unknown>>; tabs: ReadonlyArray<
TabOption<QueryPreviewTabs, unknown, 'query' | 'mutation'>
>;
onTabChange: (tab: QueryPreviewTabs) => void; onTabChange: (tab: QueryPreviewTabs) => void;
selectedTab: QueryPreviewTabs; selectedTab: QueryPreviewTabs;
renderTabLabel?: (tab: QueryPreviewHeaderProps['tabs'][number]) => ReactNode; renderTabLabel?: (tab: QueryPreviewHeaderProps['tabs'][number]) => ReactNode;

View File

@ -1,7 +1,7 @@
import { createSelector, Selector } from '@reduxjs/toolkit'; import { createSelector, Selector } from '@reduxjs/toolkit';
import { QueryStatus } from '@reduxjs/toolkit/dist/query'; import { QueryStatus } from '@reduxjs/toolkit/dist/query';
import React, { ReactNode, PureComponent } from 'react'; import React, { ReactNode, PureComponent } from 'react';
import { QueryInfo, RtkQueryState, RTKStatusFlags } from '../types'; import { RtkResourceInfo, RTKStatusFlags } from '../types';
import { formatMs } from '../utils/formatters'; import { formatMs } from '../utils/formatters';
import { identity } from '../utils/object'; import { identity } from '../utils/object';
import { getQueryStatusFlags } from '../utils/rtk-query'; import { getQueryStatusFlags } from '../utils/rtk-query';
@ -13,16 +13,18 @@ type QueryTimings = {
duration: string; duration: string;
}; };
interface FormattedQuery { type FormattedQuery = {
queryKey: string; key: string;
reducerPath: string; reducerPath: string;
timings: QueryTimings; timings: QueryTimings;
statusFlags: RTKStatusFlags; statusFlags: RTKStatusFlags;
query: RtkQueryState; } & (
} | { mutation: RtkResourceInfo['state'] }
| { query: RtkResourceInfo['state'] }
);
export interface QueryPreviewInfoProps { export interface QueryPreviewInfoProps {
queryInfo: QueryInfo; resInfo: RtkResourceInfo;
isWideLayout: boolean; isWideLayout: boolean;
} }
export class QueryPreviewInfo extends PureComponent<QueryPreviewInfoProps> { export class QueryPreviewInfo extends PureComponent<QueryPreviewInfoProps> {
@ -33,23 +35,22 @@ export class QueryPreviewInfo extends PureComponent<QueryPreviewInfoProps> {
): boolean => { ): boolean => {
const lastKey = keyPath[keyPath.length - 1]; const lastKey = keyPath[keyPath.length - 1];
return layer <= 1 && lastKey !== 'query'; return layer <= 1 && lastKey !== 'query' && lastKey !== 'mutation';
}; };
selectFormattedQuery: Selector<QueryInfo, FormattedQuery> = createSelector( selectFormattedQuery: Selector<RtkResourceInfo, FormattedQuery> =
identity, createSelector(identity, (resInfo: RtkResourceInfo): FormattedQuery => {
(queryInfo: QueryInfo): FormattedQuery => { const { state, queryKey, reducerPath } = resInfo;
const { query, queryKey, reducerPath } = queryInfo;
const startedAt = query.startedTimeStamp const startedAt = state.startedTimeStamp
? new Date(query.startedTimeStamp).toISOString() ? new Date(state.startedTimeStamp).toISOString()
: '-'; : '-';
const loadedAt = query.fulfilledTimeStamp const loadedAt = state.fulfilledTimeStamp
? new Date(query.fulfilledTimeStamp).toISOString() ? new Date(state.fulfilledTimeStamp).toISOString()
: '-'; : '-';
const statusFlags = getQueryStatusFlags(query); const statusFlags = getQueryStatusFlags(state);
const timings = { const timings = {
startedAt, startedAt,
@ -58,29 +59,38 @@ export class QueryPreviewInfo extends PureComponent<QueryPreviewInfoProps> {
}; };
if ( if (
query.fulfilledTimeStamp && state.fulfilledTimeStamp &&
query.startedTimeStamp && state.startedTimeStamp &&
query.status !== QueryStatus.pending && state.status !== QueryStatus.pending &&
query.startedTimeStamp <= query.fulfilledTimeStamp state.startedTimeStamp <= state.fulfilledTimeStamp
) { ) {
timings.duration = formatMs( timings.duration = formatMs(
query.fulfilledTimeStamp - query.startedTimeStamp state.fulfilledTimeStamp - state.startedTimeStamp
); );
} }
if (resInfo.type === 'query') {
return { return {
queryKey, key: queryKey,
reducerPath, reducerPath,
query: queryInfo.query, query: resInfo.state,
statusFlags, statusFlags,
timings, timings,
}; };
} }
);
return {
key: queryKey,
reducerPath,
mutation: resInfo.state,
statusFlags,
timings,
};
});
render(): ReactNode { render(): ReactNode {
const { queryInfo, isWideLayout } = this.props; const { resInfo, isWideLayout } = this.props;
const formattedQuery = this.selectFormattedQuery(queryInfo); const formattedQuery = this.selectFormattedQuery(resInfo);
return ( return (
<TreeView <TreeView

View File

@ -3,7 +3,7 @@ import { StyleUtilsContext } from '../styles/createStylingFromTheme';
import { createTreeItemLabelRenderer } from '../styles/tree'; import { createTreeItemLabelRenderer } from '../styles/tree';
import { import {
QueryPreviewTabs, QueryPreviewTabs,
QueryInfo, RtkResourceInfo,
SelectorsSource, SelectorsSource,
TabOption, TabOption,
} from '../types'; } from '../types';
@ -32,12 +32,13 @@ import {
QueryPreviewActions, QueryPreviewActions,
QueryPreviewActionsProps, QueryPreviewActionsProps,
} from '../components/QueryPreviewActions'; } from '../components/QueryPreviewActions';
import { isTabVisible } from '../utils/tabs';
export interface QueryPreviewProps<S = unknown> { export interface QueryPreviewProps<S = unknown> {
readonly selectedTab: QueryPreviewTabs; readonly selectedTab: QueryPreviewTabs;
readonly hasNoApis: boolean; readonly hasNoApis: boolean;
readonly onTabChange: (tab: QueryPreviewTabs) => void; readonly onTabChange: (tab: QueryPreviewTabs) => void;
readonly queryInfo: QueryInfo | null; readonly resInfo: RtkResourceInfo | null;
readonly styling: StylingFunction; readonly styling: StylingFunction;
readonly isWideLayout: boolean; readonly isWideLayout: boolean;
readonly selectorsSource: SelectorsSource<S>; readonly selectorsSource: SelectorsSource<S>;
@ -47,15 +48,15 @@ export interface QueryPreviewProps<S = unknown> {
/** /**
* Tab content is not rendered if there's no selected query. * Tab content is not rendered if there's no selected query.
*/ */
type QueryPreviewTabProps = Omit<QueryPreviewProps<unknown>, 'queryInfo'> & { type QueryPreviewTabProps = Omit<QueryPreviewProps<unknown>, 'resInfo'> & {
queryInfo: QueryInfo; resInfo: RtkResourceInfo;
}; };
const MappedQueryPreviewTags = mapProps< const MappedQueryPreviewTags = mapProps<
QueryPreviewTabProps, QueryPreviewTabProps,
QueryPreviewTagsProps QueryPreviewTagsProps
>(({ selectors, selectorsSource, isWideLayout, queryInfo }) => ({ >(({ selectors, selectorsSource, isWideLayout, resInfo }) => ({
queryInfo, resInfo,
tags: selectors.selectCurrentQueryTags(selectorsSource), tags: selectors.selectCurrentQueryTags(selectorsSource),
isWideLayout, isWideLayout,
}))(QueryPreviewTags); }))(QueryPreviewTags);
@ -63,9 +64,7 @@ const MappedQueryPreviewTags = mapProps<
const MappedQueryPreviewInfo = mapProps< const MappedQueryPreviewInfo = mapProps<
QueryPreviewTabProps, QueryPreviewTabProps,
QueryPreviewInfoProps QueryPreviewInfoProps
>(({ queryInfo, isWideLayout }) => ({ queryInfo, isWideLayout }))( >(({ resInfo, isWideLayout }) => ({ resInfo, isWideLayout }))(QueryPreviewInfo);
QueryPreviewInfo
);
const MappedQuerySubscriptipns = mapProps< const MappedQuerySubscriptipns = mapProps<
QueryPreviewTabProps, QueryPreviewTabProps,
@ -91,26 +90,48 @@ const MappedQueryPreviewActions = mapProps<
actionsOfQuery: selectors.selectActionsOfCurrentQuery(selectorsSource), actionsOfQuery: selectors.selectActionsOfCurrentQuery(selectorsSource),
}))(QueryPreviewActions); }))(QueryPreviewActions);
const tabs: ReadonlyArray<TabOption<QueryPreviewTabs, QueryPreviewTabProps>> = [ const tabs: ReadonlyArray<
TabOption<QueryPreviewTabs, QueryPreviewTabProps, RtkResourceInfo['type']>
> = [
{ {
label: 'query', label: 'query',
value: QueryPreviewTabs.queryinfo, value: QueryPreviewTabs.queryinfo,
component: MappedQueryPreviewInfo, component: MappedQueryPreviewInfo,
visible: {
query: true,
mutation: true,
default: true,
},
}, },
{ {
label: 'actions', label: 'actions',
value: QueryPreviewTabs.actions, value: QueryPreviewTabs.actions,
component: MappedQueryPreviewActions, component: MappedQueryPreviewActions,
visible: {
query: true,
mutation: true,
default: true,
},
}, },
{ {
label: 'tags', label: 'tags',
value: QueryPreviewTabs.queryTags, value: QueryPreviewTabs.queryTags,
component: MappedQueryPreviewTags, component: MappedQueryPreviewTags,
visible: {
query: true,
mutation: false,
default: true,
},
}, },
{ {
label: 'subs', label: 'subs',
value: QueryPreviewTabs.querySubscriptions, value: QueryPreviewTabs.querySubscriptions,
component: MappedQuerySubscriptipns, component: MappedQuerySubscriptipns,
visible: {
query: true,
mutation: false,
default: true,
},
}, },
{ {
label: 'api', label: 'api',
@ -141,24 +162,32 @@ export class QueryPreview<S> extends React.PureComponent<QueryPreviewProps<S>> {
return `${label}(${counterAsString})`; return `${label}(${counterAsString})`;
}; };
renderTabLabel = (tab: TabOption<QueryPreviewTabs, unknown>): ReactNode => { renderTabLabel = (
const { selectors, selectorsSource } = this.props; tab: TabOption<QueryPreviewTabs, unknown, 'query' | 'mutation'>
): ReactNode => {
const { selectors, selectorsSource, resInfo } = this.props;
const tabCount = selectors.selectTabCounters(selectorsSource)[tab.value]; const tabCount = selectors.selectTabCounters(selectorsSource)[tab.value];
if (tabCount > 0) { let tabLabel = tab.label;
return this.renderLabelWithCounter(tab.label, tabCount);
if (tabLabel === 'query' && resInfo?.type === 'mutation') {
tabLabel = resInfo.type;
} }
return tab.label; if (tabCount > 0) {
return this.renderLabelWithCounter(tabLabel, tabCount);
}
return tabLabel;
}; };
render(): ReactNode { render(): ReactNode {
const { queryInfo, selectedTab, onTabChange, hasNoApis } = this.props; const { resInfo, selectedTab, onTabChange, hasNoApis } = this.props;
const { component: TabComponent } = const { component: TabComponent } =
tabs.find((tab) => tab.value === selectedTab) || tabs[0]; tabs.find((tab) => tab.value === selectedTab) || tabs[0];
if (!queryInfo) { if (!resInfo) {
return ( return (
<StyleUtilsContext.Consumer> <StyleUtilsContext.Consumer>
{({ styling }) => ( {({ styling }) => (
@ -167,7 +196,15 @@ export class QueryPreview<S> extends React.PureComponent<QueryPreviewProps<S>> {
selectedTab={selectedTab} selectedTab={selectedTab}
onTabChange={onTabChange} onTabChange={onTabChange}
tabs={ tabs={
tabs as ReadonlyArray<TabOption<QueryPreviewTabs, unknown>> tabs.filter((tab) =>
isTabVisible(tab, 'default')
) as ReadonlyArray<
TabOption<
QueryPreviewTabs,
unknown,
RtkResourceInfo['type']
>
>
} }
renderTabLabel={this.renderTabLabel} renderTabLabel={this.renderTabLabel}
/> />
@ -187,7 +224,15 @@ export class QueryPreview<S> extends React.PureComponent<QueryPreviewProps<S>> {
selectedTab={selectedTab} selectedTab={selectedTab}
onTabChange={onTabChange} onTabChange={onTabChange}
tabs={ tabs={
tabs as ReadonlyArray<TabOption<QueryPreviewTabs, unknown>> tabs.filter((tab) =>
isTabVisible(tab, resInfo.type)
) as ReadonlyArray<
TabOption<
QueryPreviewTabs,
unknown,
RtkResourceInfo['type']
>
>
} }
renderTabLabel={this.renderTabLabel} renderTabLabel={this.renderTabLabel}
/> />

View File

@ -3,11 +3,11 @@ import type { AnyAction, Dispatch, Action } from '@reduxjs/toolkit';
import type { LiftedAction, LiftedState } from '@redux-devtools/core'; import type { LiftedAction, LiftedState } from '@redux-devtools/core';
import { import {
QueryFormValues, QueryFormValues,
QueryInfo,
QueryPreviewTabs, QueryPreviewTabs,
RtkQueryMonitorState, RtkQueryMonitorState,
StyleUtils, StyleUtils,
SelectorsSource, SelectorsSource,
RtkResourceInfo,
} from '../types'; } from '../types';
import { createInspectorSelectors, computeSelectorSource } from '../selectors'; import { createInspectorSelectors, computeSelectorSource } from '../selectors';
import { import {
@ -101,7 +101,7 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends PureComponent<
this.props.dispatch(changeQueryFormValues(values) as AnyAction); this.props.dispatch(changeQueryFormValues(values) as AnyAction);
}; };
handleSelectQuery = (queryInfo: QueryInfo): void => { handleSelectQuery = (queryInfo: RtkResourceInfo): void => {
this.props.dispatch(selectQueryKey(queryInfo) as AnyAction); this.props.dispatch(selectQueryKey(queryInfo) as AnyAction);
}; };
@ -114,10 +114,10 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends PureComponent<
const { const {
styleUtils: { styling }, styleUtils: { styling },
} = this.props; } = this.props;
const allVisibleQueries = const allVisibleRtkResourceInfos =
this.selectors.selectAllVisbileQueries(selectorsSource); this.selectors.selectAllVisbileQueries(selectorsSource);
const currentQueryInfo = const currentResInfo =
this.selectors.selectCurrentQueryInfo(selectorsSource); this.selectors.selectCurrentQueryInfo(selectorsSource);
const apiStates = this.selectors.selectApiStates(selectorsSource); const apiStates = this.selectors.selectApiStates(selectorsSource);
@ -144,14 +144,14 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends PureComponent<
/> />
<QueryList <QueryList
onSelectQuery={this.handleSelectQuery} onSelectQuery={this.handleSelectQuery}
queryInfos={allVisibleQueries} resInfos={allVisibleRtkResourceInfos}
selectedQueryKey={selectorsSource.monitorState.selectedQueryKey} selectedQueryKey={selectorsSource.monitorState.selectedQueryKey}
/> />
</div> </div>
<QueryPreview<S> <QueryPreview<S>
selectorsSource={this.state.selectorsSource} selectorsSource={this.state.selectorsSource}
selectors={this.selectors} selectors={this.selectors}
queryInfo={currentQueryInfo} resInfo={currentResInfo}
selectedTab={selectorsSource.monitorState.selectedPreviewTab} selectedTab={selectorsSource.monitorState.selectedPreviewTab}
onTabChange={this.handleTabChange} onTabChange={this.handleTabChange}
styling={styling} styling={styling}

View File

@ -8,6 +8,7 @@ import {
SelectorsSource, SelectorsSource,
RtkQueryProvided, RtkQueryProvided,
QueryPreviewTabs, QueryPreviewTabs,
RtkResourceInfo,
} from './types'; } 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';
@ -20,6 +21,7 @@ import {
getQueryTagsOf, getQueryTagsOf,
generateApiStatsOfCurrentQuery, generateApiStatsOfCurrentQuery,
getActionsOfCurrentQuery, getActionsOfCurrentQuery,
extractAllApiMutations,
} from './utils/rtk-query'; } from './utils/rtk-query';
type InspectorSelector<S, Output> = Selector<SelectorsSource<S>, Output>; type InspectorSelector<S, Output> = Selector<SelectorsSource<S>, Output>;
@ -60,8 +62,8 @@ export interface InspectorSelectors<S> {
S, S,
ReturnType<typeof extractAllApiQueries> ReturnType<typeof extractAllApiQueries>
>; >;
readonly selectAllVisbileQueries: InspectorSelector<S, QueryInfo[]>; readonly selectAllVisbileQueries: InspectorSelector<S, RtkResourceInfo[]>;
readonly selectCurrentQueryInfo: InspectorSelector<S, QueryInfo | null>; readonly selectCurrentQueryInfo: InspectorSelector<S, RtkResourceInfo | 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>; readonly selectApiStatsOfCurrentQuery: InspectorSelector<S, ApiStats | null>;
@ -86,13 +88,13 @@ export interface InspectorSelectors<S> {
export function createInspectorSelectors<S>(): InspectorSelectors<S> { export function createInspectorSelectors<S>(): InspectorSelectors<S> {
const selectQueryComparator = ({ const selectQueryComparator = ({
monitorState, monitorState,
}: SelectorsSource<S>): Comparator<QueryInfo> => { }: SelectorsSource<S>): Comparator<RtkResourceInfo> => {
return queryComparators[monitorState.queryForm.values.queryComparator]; return queryComparators[monitorState.queryForm.values.queryComparator];
}; };
const selectQueryListFilter = ({ const selectQueryListFilter = ({
monitorState, monitorState,
}: SelectorsSource<S>): FilterList<QueryInfo> => { }: SelectorsSource<S>): FilterList<RtkResourceInfo> => {
return queryListFilters[monitorState.queryForm.values.queryFilter]; return queryListFilters[monitorState.queryForm.values.queryFilter];
}; };
@ -108,6 +110,11 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
extractAllApiQueries extractAllApiQueries
); );
const selectAllMutations = createSelector(
selectApiStates,
extractAllApiMutations
);
const selectSearchQueryRegex = createSelector( const selectSearchQueryRegex = createSelector(
({ monitorState }: SelectorsSource<S>) => ({ monitorState }: SelectorsSource<S>) =>
monitorState.queryForm.values.searchValue, monitorState.queryForm.values.searchValue,
@ -138,13 +145,21 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
selectQueryComparator, selectQueryComparator,
selectQueryListFilter, selectQueryListFilter,
selectAllQueries, selectAllQueries,
selectAllMutations,
selectComparatorOrder, selectComparatorOrder,
selectSearchQueryRegex, selectSearchQueryRegex,
], ],
(comparator, queryListFilter, queryList, isAscending, searchRegex) => { (
comparator,
queryListFilter,
queryList,
mutationsList,
isAscending,
searchRegex
) => {
const filteredList = queryListFilter( const filteredList = queryListFilter(
searchRegex, searchRegex,
queryList as QueryInfo[] (queryList as RtkResourceInfo[]).concat(mutationsList)
); );
const computedComparator = isAscending const computedComparator = isAscending
@ -157,19 +172,29 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
const selectCurrentQueryInfo = createSelector( const selectCurrentQueryInfo = createSelector(
selectAllQueries, selectAllQueries,
selectAllMutations,
({ monitorState }: SelectorsSource<S>) => monitorState.selectedQueryKey, ({ monitorState }: SelectorsSource<S>) => monitorState.selectedQueryKey,
(allQueries, selectedQueryKey) => { (allQueries, allMutations, selectedQueryKey) => {
if (!selectedQueryKey) { if (!selectedQueryKey) {
return null; return null;
} }
const currentQueryInfo = let currentQueryInfo: null | RtkResourceInfo =
allQueries.find( allQueries.find(
(query) => (query) =>
query.queryKey === selectedQueryKey.queryKey && query.queryKey === selectedQueryKey.queryKey &&
selectedQueryKey.reducerPath === query.reducerPath selectedQueryKey.reducerPath === query.reducerPath
) || null; ) || null;
if (!currentQueryInfo) {
currentQueryInfo =
allMutations.find(
(mutation) =>
mutation.queryKey === selectedQueryKey.queryKey &&
selectedQueryKey.reducerPath === mutation.reducerPath
) || null;
}
return currentQueryInfo; return currentQueryInfo;
} }
); );

View File

@ -139,8 +139,10 @@ const getSheetFromColorMap = (map: ColorMap) => {
'-webkit-line-clamp': 2, '-webkit-line-clamp': 2,
whiteSpace: 'normal', whiteSpace: 'normal',
overflow: 'hidden', overflow: 'hidden',
width: '100%',
maxWidth: 'calc(100% - 70px)', maxWidth: 'calc(100% - 70px)',
wordBreak: 'break-all', wordBreak: 'break-all',
margin: 0,
}, },
queryListHeader: { queryListHeader: {
@ -154,6 +156,20 @@ const getSheetFromColorMap = (map: ColorMap) => {
'border-color': map.LIST_BORDER_COLOR, 'border-color': map.LIST_BORDER_COLOR,
}, },
queryStatusWrapper: {
display: 'flex',
width: 'auto',
justifyContent: 'center',
alignItems: 'center',
margin: 0,
flex: '0 0 auto',
overflow: 'hidden',
},
queryType: {
marginRight: 4,
},
queryStatus: { queryStatus: {
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
@ -164,6 +180,7 @@ const getSheetFromColorMap = (map: ColorMap) => {
'font-size': '0.7em', 'font-size': '0.7em',
'line-height': '1em', 'line-height': '1em',
'flex-shrink': 0, 'flex-shrink': 0,
fontWeight: 700,
'background-color': map.ACTION_TIME_BACK_COLOR, 'background-color': map.ACTION_TIME_BACK_COLOR,
color: map.ACTION_TIME_COLOR, color: map.ACTION_TIME_COLOR,
}, },

View File

@ -61,25 +61,33 @@ export interface ExternalProps<S, A extends Action<unknown>> {
} }
export interface QueryInfo { export interface QueryInfo {
query: RtkQueryState; type: 'query';
state: RtkQueryState;
queryKey: string; queryKey: string;
reducerPath: string; reducerPath: string;
} }
export interface MutationInfo { export interface MutationInfo {
mutation: RtkMutationState; type: 'mutation';
state: RtkMutationState;
queryKey: string; queryKey: string;
reducerPath: string; reducerPath: string;
} }
export type RtkResourceInfo = QueryInfo | MutationInfo;
export interface ApiInfo { export interface ApiInfo {
reducerPath: string; reducerPath: string;
apiState: RtkQueryApiState; apiState: RtkQueryApiState;
} }
export interface SelectOption<T = string> { export interface SelectOption<
T = string,
VisConfig extends string = 'default'
> {
label: string; label: string;
value: T; value: T;
visible?: Record<VisConfig | 'default', boolean> | boolean;
} }
export interface SelectorsSource<S> { export interface SelectorsSource<S> {
@ -136,7 +144,8 @@ export interface ApiStats {
}>; }>;
} }
export interface TabOption<S, P> extends SelectOption<S> { export interface TabOption<S, P, V extends string = 'default'>
extends SelectOption<S, V> {
component: ComponentType<P>; component: ComponentType<P>;
} }

View File

@ -1,5 +1,5 @@
import { QueryStatus } from '@reduxjs/toolkit/query'; import { QueryStatus } from '@reduxjs/toolkit/query';
import { QueryInfo, SelectOption } from '../types'; import { RtkResourceInfo, SelectOption } from '../types';
export interface Comparator<T> { export interface Comparator<T> {
(a: T, b: T): number; (a: T, b: T): number;
@ -22,11 +22,11 @@ export const sortQueryOptions: SelectOption<QueryComparators>[] = [
]; ];
function sortQueryByFulfilled( function sortQueryByFulfilled(
thisQueryInfo: QueryInfo, thisQueryInfo: RtkResourceInfo,
thatQueryInfo: QueryInfo thatQueryInfo: RtkResourceInfo
): number { ): number {
const thisFulfilled = thisQueryInfo.query.fulfilledTimeStamp ?? -1; const thisFulfilled = thisQueryInfo.state.fulfilledTimeStamp ?? -1;
const thatFulfilled = thatQueryInfo.query.fulfilledTimeStamp ?? -1; const thatFulfilled = thatQueryInfo.state.fulfilledTimeStamp ?? -1;
return thisFulfilled - thatFulfilled; return thisFulfilled - thatFulfilled;
} }
@ -39,11 +39,11 @@ const mapStatusToFactor = {
}; };
function sortQueryByStatus( function sortQueryByStatus(
thisQueryInfo: QueryInfo, thisQueryInfo: RtkResourceInfo,
thatQueryInfo: QueryInfo thatQueryInfo: RtkResourceInfo
): number { ): number {
const thisTerm = mapStatusToFactor[thisQueryInfo.query.status] || -1; const thisTerm = mapStatusToFactor[thisQueryInfo.state.status] || -1;
const thatTerm = mapStatusToFactor[thatQueryInfo.query.status] || -1; const thatTerm = mapStatusToFactor[thatQueryInfo.state.status] || -1;
return thisTerm - thatTerm; return thisTerm - thatTerm;
} }
@ -60,25 +60,25 @@ function compareJSONPrimitive<T extends string | number | boolean | null>(
} }
function sortByQueryKey( function sortByQueryKey(
thisQueryInfo: QueryInfo, thisQueryInfo: RtkResourceInfo,
thatQueryInfo: QueryInfo thatQueryInfo: RtkResourceInfo
): number { ): number {
return compareJSONPrimitive(thisQueryInfo.queryKey, thatQueryInfo.queryKey); return compareJSONPrimitive(thisQueryInfo.queryKey, thatQueryInfo.queryKey);
} }
function sortQueryByEndpointName( function sortQueryByEndpointName(
thisQueryInfo: QueryInfo, thisQueryInfo: RtkResourceInfo,
thatQueryInfo: QueryInfo thatQueryInfo: RtkResourceInfo
): number { ): number {
const thisEndpointName = thisQueryInfo.query.endpointName ?? ''; const thisEndpointName = thisQueryInfo.state.endpointName ?? '';
const thatEndpointName = thatQueryInfo.query.endpointName ?? ''; const thatEndpointName = thatQueryInfo.state.endpointName ?? '';
return compareJSONPrimitive(thisEndpointName, thatEndpointName); return compareJSONPrimitive(thisEndpointName, thatEndpointName);
} }
function sortByApiReducerPath( function sortByApiReducerPath(
thisQueryInfo: QueryInfo, thisQueryInfo: RtkResourceInfo,
thatQueryInfo: QueryInfo thatQueryInfo: RtkResourceInfo
): number { ): number {
return compareJSONPrimitive( return compareJSONPrimitive(
thisQueryInfo.reducerPath, thisQueryInfo.reducerPath,
@ -87,7 +87,7 @@ function sortByApiReducerPath(
} }
export const queryComparators: Readonly< export const queryComparators: Readonly<
Record<QueryComparators, Comparator<QueryInfo>> Record<QueryComparators, Comparator<RtkResourceInfo>>
> = { > = {
[QueryComparators.fulfilledTimeStamp]: sortQueryByFulfilled, [QueryComparators.fulfilledTimeStamp]: sortQueryByFulfilled,
[QueryComparators.status]: sortQueryByStatus, [QueryComparators.status]: sortQueryByStatus,

View File

@ -1,4 +1,4 @@
import { QueryInfo, SelectOption } from '../types'; import { RtkResourceInfo, SelectOption } from '../types';
export interface FilterList<T> { export interface FilterList<T> {
(regex: RegExp | null, list: T[]): T[]; (regex: RegExp | null, list: T[]): T[];
@ -13,45 +13,52 @@ export enum QueryFilters {
function filterByQueryKey( function filterByQueryKey(
regex: RegExp | null, regex: RegExp | null,
list: QueryInfo[] list: RtkResourceInfo[]
): QueryInfo[] { ): RtkResourceInfo[] {
if (!regex) { if (!regex) {
return list; return list;
} }
return list.filter((queryInfo) => regex.test(queryInfo.queryKey)); return list.filter((RtkResourceInfo) => regex.test(RtkResourceInfo.queryKey));
} }
function filterByReducerPath( function filterByReducerPath(
regex: RegExp | null, regex: RegExp | null,
list: QueryInfo[] list: RtkResourceInfo[]
): QueryInfo[] { ): RtkResourceInfo[] {
if (!regex) { if (!regex) {
return list; return list;
} }
return list.filter((queryInfo) => regex.test(queryInfo.reducerPath)); return list.filter((RtkResourceInfo) =>
regex.test(RtkResourceInfo.reducerPath)
);
} }
function filterByEndpointName( function filterByEndpointName(
regex: RegExp | null, regex: RegExp | null,
list: QueryInfo[] list: RtkResourceInfo[]
): QueryInfo[] { ): RtkResourceInfo[] {
if (!regex) { if (!regex) {
return list; return list;
} }
return list.filter((queryInfo) => return list.filter((RtkResourceInfo) =>
regex.test(queryInfo.query.endpointName ?? 'undefined') regex.test(RtkResourceInfo.state.endpointName ?? 'undefined')
); );
} }
function filterByStatus(regex: RegExp | null, list: QueryInfo[]): QueryInfo[] { function filterByStatus(
regex: RegExp | null,
list: RtkResourceInfo[]
): RtkResourceInfo[] {
if (!regex) { if (!regex) {
return list; return list;
} }
return list.filter((queryInfo) => regex.test(queryInfo.query.status)); return list.filter((RtkResourceInfo) =>
regex.test(RtkResourceInfo.state.status)
);
} }
export const filterQueryOptions: SelectOption<QueryFilters>[] = [ export const filterQueryOptions: SelectOption<QueryFilters>[] = [
@ -62,7 +69,7 @@ export const filterQueryOptions: SelectOption<QueryFilters>[] = [
]; ];
export const queryListFilters: Readonly< export const queryListFilters: Readonly<
Record<QueryFilters, FilterList<QueryInfo>> Record<QueryFilters, FilterList<RtkResourceInfo>>
> = { > = {
[QueryFilters.queryKey]: filterByQueryKey, [QueryFilters.queryKey]: filterByQueryKey,
[QueryFilters.endpointName]: filterByEndpointName, [QueryFilters.endpointName]: filterByEndpointName,

View File

@ -1,4 +1,4 @@
import { AnyAction, isAnyOf, isPlainObject } from '@reduxjs/toolkit'; import { AnyAction, isAllOf, isAnyOf, isPlainObject } from '@reduxjs/toolkit';
import { QueryStatus } from '@reduxjs/toolkit/query'; import { QueryStatus } from '@reduxjs/toolkit/query';
import { import {
QueryInfo, QueryInfo,
@ -15,6 +15,8 @@ import {
ApiTimings, ApiTimings,
QueryTimings, QueryTimings,
SelectorsSource, SelectorsSource,
RtkMutationState,
RtkResourceInfo,
} from '../types'; } from '../types';
import { missingTagId } from '../monitor-config'; import { missingTagId } from '../monitor-config';
import { Comparator } from './comparators'; import { Comparator } from './comparators';
@ -104,13 +106,14 @@ export function extractAllApiQueries(
for (let j = 0, qKeysLen = queryKeys.length; j < qKeysLen; j++) { for (let j = 0, qKeysLen = queryKeys.length; j < qKeysLen; j++) {
const queryKey = queryKeys[j]; const queryKey = queryKeys[j];
const query = api.queries[queryKey]; const state = api.queries[queryKey];
if (query) { if (state) {
output.push({ output.push({
type: 'query',
reducerPath, reducerPath,
queryKey, queryKey,
query, state,
}); });
} }
} }
@ -136,13 +139,14 @@ export function extractAllApiMutations(
for (let j = 0, mKeysLen = mutationKeys.length; j < mKeysLen; j++) { for (let j = 0, mKeysLen = mutationKeys.length; j < mKeysLen; j++) {
const queryKey = mutationKeys[j]; const queryKey = mutationKeys[j];
const mutation = api.queries[queryKey]; const state = api.mutations[queryKey];
if (mutation) { if (state) {
output.push({ output.push({
type: 'mutation',
reducerPath, reducerPath,
queryKey, queryKey,
mutation, state,
}); });
} }
} }
@ -333,7 +337,7 @@ export function flipComparator<T>(comparator: Comparator<T>): Comparator<T> {
export function isQuerySelected( export function isQuerySelected(
selectedQueryKey: RtkQueryMonitorState['selectedQueryKey'], selectedQueryKey: RtkQueryMonitorState['selectedQueryKey'],
queryInfo: QueryInfo queryInfo: RtkResourceInfo
): boolean { ): boolean {
return ( return (
!!selectedQueryKey && !!selectedQueryKey &&
@ -343,7 +347,7 @@ export function isQuerySelected(
} }
export function getApiStateOf( export function getApiStateOf(
queryInfo: QueryInfo | null, queryInfo: RtkResourceInfo | null,
apiStates: ReturnType<typeof getApiStatesOf> apiStates: ReturnType<typeof getApiStatesOf>
): RtkQueryApiState | null { ): RtkQueryApiState | null {
if (!apiStates || !queryInfo) { if (!apiStates || !queryInfo) {
@ -379,10 +383,10 @@ export function getProvidedOf(
} }
export function getQueryTagsOf( export function getQueryTagsOf(
queryInfo: QueryInfo | null, resInfo: RtkResourceInfo | null,
provided: RtkQueryProvided | null provided: RtkQueryProvided | null
): RtkQueryTag[] { ): RtkQueryTag[] {
if (!queryInfo || !provided) { if (!resInfo || resInfo.type === 'mutation' || !provided) {
return emptyArray; return emptyArray;
} }
@ -397,7 +401,7 @@ export function getQueryTagsOf(
for (const [type, tagIds] of Object.entries(provided)) { for (const [type, tagIds] of Object.entries(provided)) {
if (tagIds) { if (tagIds) {
for (const [id, queryKeys] of Object.entries(tagIds)) { for (const [id, queryKeys] of Object.entries(tagIds)) {
if ((queryKeys as unknown[]).includes(queryInfo.queryKey)) { if ((queryKeys as unknown[]).includes(resInfo.queryKey)) {
const tag: RtkQueryTag = { type }; const tag: RtkQueryTag = { type };
if (id !== missingTagId) { if (id !== missingTagId) {
@ -422,7 +426,7 @@ export function getQueryTagsOf(
export function getQueryStatusFlags({ export function getQueryStatusFlags({
status, status,
data, data,
}: RtkQueryState): RTKStatusFlags { }: RtkQueryState | RtkMutationState): RTKStatusFlags {
return { return {
isUninitialized: status === QueryStatus.uninitialized, isUninitialized: status === QueryStatus.uninitialized,
isFetching: status === QueryStatus.pending, isFetching: status === QueryStatus.pending,
@ -446,15 +450,37 @@ function matchesQueryKey(queryKey: string) {
action?.meta?.arg?.queryCacheKey === queryKey; action?.meta?.arg?.queryCacheKey === queryKey;
} }
function macthesRequestId(requestId: string) {
return (action: any): action is AnyAction =>
action?.meta?.requestId === requestId;
}
function matchesReducerPath(reducerPath: string) {
return (action: any): action is AnyAction =>
typeof action?.type === 'string' && action.type.startsWith(reducerPath);
}
export function getActionsOfCurrentQuery( export function getActionsOfCurrentQuery(
currentQuery: QueryInfo | null, currentQuery: RtkResourceInfo | null,
actionById: SelectorsSource<unknown>['actionsById'] actionById: SelectorsSource<unknown>['actionsById']
): AnyAction[] { ): AnyAction[] {
if (!currentQuery) { if (!currentQuery) {
return emptyArray; return emptyArray;
} }
const matcher = isAnyOf(matchesQueryKey(currentQuery.queryKey)); let matcher: ReturnType<typeof macthesRequestId>;
if (currentQuery.type === 'mutation') {
matcher = isAllOf(
matchesReducerPath(currentQuery.reducerPath),
macthesRequestId(currentQuery.queryKey)
);
} else {
matcher = isAllOf(
matchesReducerPath(currentQuery.reducerPath),
matchesQueryKey(currentQuery.queryKey)
);
}
const output: AnyAction[] = []; const output: AnyAction[] = [];

View File

@ -0,0 +1,16 @@
import { TabOption } from '../types';
export function isTabVisible<St, Props, Vis extends string>(
tab: TabOption<St, Props, Vis>,
visKey: Vis | 'default'
): boolean {
if (typeof tab.visible === 'boolean') {
return tab.visible;
}
if (typeof tab.visible === 'object' && tab.visible) {
return !!tab.visible[visKey];
}
return true;
}