diff --git a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreview.tsx b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreview.tsx index f5f1e667..5aae4f12 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreview.tsx +++ b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreview.tsx @@ -2,51 +2,101 @@ import React, { ReactNode } from 'react'; import { StyleUtilsContext } from '../styles/createStylingFromTheme'; import { createTreeItemLabelRenderer } from '../styles/tree'; import { - QueryPreviewTabOption, QueryPreviewTabs, - QueryPreviewTabProps, + QueryInfo, + SelectorsSource, + TabOption, } from '../types'; import { QueryPreviewHeader } from './QueryPreviewHeader'; -import { QueryPreviewInfo } from './QueryPreviewInfo'; -import { QueryPreviewApi } from './QueryPreviewApi'; -import { QueryPreviewSubscriptions } from './QueryPreviewSubscriptions'; -import { QueryPreviewTags } from './QueryPreviewTags'; +import { QueryPreviewInfo, QueryPreviewInfoProps } from './QueryPreviewInfo'; +import { QueryPreviewApi, QueryPreviewApiProps } from './QueryPreviewApi'; +import { + QueryPreviewSubscriptions, + QueryPreviewSubscriptionsProps, +} from './QueryPreviewSubscriptions'; +import { QueryPreviewTags, QueryPreviewTagsProps } from './QueryPreviewTags'; import { NoRtkQueryApi } from './NoRtkQueryApi'; +import { InspectorSelectors } from '../selectors'; +import { StylingFunction } from 'react-base16-styling'; +import { mapProps } from '../containers/mapProps'; -export interface QueryPreviewProps - extends Omit { - selectedTab: QueryPreviewTabs; - hasNoApis: boolean; - onTabChange: (tab: QueryPreviewTabs) => void; +export interface QueryPreviewProps { + readonly selectedTab: QueryPreviewTabs; + readonly hasNoApis: boolean; + readonly onTabChange: (tab: QueryPreviewTabs) => void; + readonly queryInfo: QueryInfo | null; + readonly styling: StylingFunction; + readonly isWideLayout: boolean; + readonly selectorsSource: SelectorsSource; + readonly selectors: InspectorSelectors; } -const tabs: ReadonlyArray = [ +/** + * Tab content is not rendered if there's no selected query. + */ +type QueryPreviewTabProps = Omit, 'queryInfo'> & { + queryInfo: QueryInfo; +}; + +const MappedQueryPreviewTags = mapProps< + QueryPreviewTabProps, + QueryPreviewTagsProps +>(({ selectors, selectorsSource, isWideLayout, queryInfo }) => ({ + queryInfo, + tags: selectors.selectCurrentQueryTags(selectorsSource), + isWideLayout, +}))(QueryPreviewTags); + +const MappedQueryPreviewInfo = mapProps< + QueryPreviewTabProps, + QueryPreviewInfoProps +>(({ queryInfo, isWideLayout }) => ({ queryInfo, isWideLayout }))( + QueryPreviewInfo +); + +const MappedQuerySubscriptipns = mapProps< + QueryPreviewTabProps, + QueryPreviewSubscriptionsProps +>(({ selectors, selectorsSource, isWideLayout }) => ({ + isWideLayout, + subscriptions: selectors.selectSubscriptionsOfCurrentQuery(selectorsSource), +}))(QueryPreviewSubscriptions); + +const MappedApiPreview = mapProps( + ({ isWideLayout, selectors, selectorsSource }) => ({ + isWideLayout, + apiState: selectors.selectApiOfCurrentQuery(selectorsSource), + apiStats: selectors.selectApiStatsOfCurrentQuery(selectorsSource), + }) +)(QueryPreviewApi); + +const tabs: ReadonlyArray> = [ { label: 'query', value: QueryPreviewTabs.queryinfo, - component: QueryPreviewInfo, + component: MappedQueryPreviewInfo, }, { label: 'tags', value: QueryPreviewTabs.queryTags, - component: QueryPreviewTags, + component: MappedQueryPreviewTags, }, { label: 'subs', value: QueryPreviewTabs.querySubscriptions, - component: QueryPreviewSubscriptions, + component: MappedQuerySubscriptipns, }, { label: 'api', value: QueryPreviewTabs.apiConfig, - component: QueryPreviewApi, + component: MappedApiPreview, }, ]; -export class QueryPreview extends React.PureComponent { +export class QueryPreview extends React.PureComponent> { readonly labelRenderer: ReturnType; - constructor(props: QueryPreviewProps) { + constructor(props: QueryPreviewProps) { super(props); this.labelRenderer = createTreeItemLabelRenderer(this.props.styling); @@ -65,40 +115,19 @@ export class QueryPreview extends React.PureComponent { return `${label} (${counterAsString})`; }; - renderTabLabel = (tab: QueryPreviewTabOption): ReactNode => { - const { queryInfo, tags, querySubscriptions } = this.props; - if (queryInfo) { - if (tab.value === QueryPreviewTabs.queryTags && tags.length > 0) { - return this.renderLabelWithCounter(tab.label, tags.length); - } + renderTabLabel = (tab: TabOption): ReactNode => { + const { selectors, selectorsSource } = this.props; + const tabCount = selectors.selectTabCounters(selectorsSource)[tab.value]; - if ( - tab.value === QueryPreviewTabs.querySubscriptions && - querySubscriptions - ) { - const subsCount = Object.keys(querySubscriptions).length; - - if (subsCount > 0) { - return this.renderLabelWithCounter(tab.label, subsCount); - } - } + if (tabCount > 0) { + return this.renderLabelWithCounter(tab.label, tabCount); } return tab.label; }; render(): ReactNode { - const { - queryInfo, - isWideLayout, - selectedTab, - apiState, - onTabChange, - querySubscriptions, - tags, - apiStats, - hasNoApis, - } = this.props; + const { queryInfo, selectedTab, onTabChange, hasNoApis } = this.props; const { component: TabComponent } = tabs.find((tab) => tab.value === selectedTab) || tabs[0]; @@ -111,7 +140,9 @@ export class QueryPreview extends React.PureComponent { > + } renderTabLabel={this.renderTabLabel} /> {hasNoApis && } @@ -123,26 +154,18 @@ export class QueryPreview extends React.PureComponent { return ( - {({ styling, base16Theme, invertTheme }) => { + {({ styling }) => { return (
> + } renderTabLabel={this.renderTabLabel} /> - +
); }} diff --git a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewApi.tsx b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewApi.tsx index 3cceeaf6..6f902d70 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewApi.tsx +++ b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewApi.tsx @@ -1,18 +1,19 @@ import { createSelector } from '@reduxjs/toolkit'; import React, { ReactNode, PureComponent } from 'react'; -import { QueryPreviewTabProps } from '../types'; +import { ApiStats, RtkQueryApiState } from '../types'; import { TreeView } from './TreeView'; -interface TreeDisplayed { - reducerPath: string; - api: QueryPreviewTabProps['apiState']; - stats: QueryPreviewTabProps['apiStats']; +export interface QueryPreviewApiProps { + apiStats: ApiStats | null; + apiState: RtkQueryApiState | null; + isWideLayout: boolean; } -export class QueryPreviewApi extends PureComponent { + +export class QueryPreviewApi extends PureComponent { selectData = createSelector( [ - ({ apiState }: QueryPreviewTabProps) => apiState, - ({ apiStats }: QueryPreviewTabProps) => apiStats, + ({ apiState }: QueryPreviewApiProps) => apiState, + ({ apiStats }: QueryPreviewApiProps) => apiStats, ], (apiState, apiStats) => ({ reducerPath: apiState?.config?.reducerPath ?? null, @@ -22,20 +23,10 @@ export class QueryPreviewApi extends PureComponent { ); render(): ReactNode { - const { queryInfo, isWideLayout, base16Theme, styling, invertTheme } = - this.props; - - if (!queryInfo) { - return null; - } - return ( ); } diff --git a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewHeader.tsx b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewHeader.tsx index c20cdcc8..710575cd 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewHeader.tsx +++ b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewHeader.tsx @@ -1,17 +1,17 @@ import React, { ReactNode } from 'react'; import { StyleUtilsContext } from '../styles/createStylingFromTheme'; -import { QueryPreviewTabOption, QueryPreviewTabs } from '../types'; +import { QueryPreviewTabs, TabOption } from '../types'; import { emptyArray } from '../utils/object'; export interface QueryPreviewHeaderProps { - tabs: ReadonlyArray; + tabs: ReadonlyArray>; onTabChange: (tab: QueryPreviewTabs) => void; selectedTab: QueryPreviewTabs; - renderTabLabel?: (tab: QueryPreviewTabOption) => ReactNode; + renderTabLabel?: (tab: QueryPreviewHeaderProps['tabs'][number]) => ReactNode; } export class QueryPreviewHeader extends React.Component { - handleTabClick = (tab: QueryPreviewTabOption): void => { + handleTabClick = (tab: QueryPreviewHeaderProps['tabs'][number]): void => { if (this.props.selectedTab !== tab.value) { this.props.onTabChange(tab.value); } diff --git a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewInfo.tsx b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewInfo.tsx index b892a5b2..3588cf47 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewInfo.tsx +++ b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewInfo.tsx @@ -1,11 +1,6 @@ import { createSelector, Selector } from '@reduxjs/toolkit'; import React, { ReactNode, PureComponent } from 'react'; -import { - QueryInfo, - QueryPreviewTabProps, - RtkQueryState, - RTKStatusFlags, -} from '../types'; +import { QueryInfo, RtkQueryState, RTKStatusFlags } from '../types'; import { identity } from '../utils/object'; import { getQueryStatusFlags } from '../utils/rtk-query'; import { TreeView } from './TreeView'; @@ -22,17 +17,14 @@ interface FormattedQuery extends ComputedQueryInfo { query: RtkQueryState; } -export class QueryPreviewInfo extends PureComponent { - selectFormattedQuery: Selector< - QueryPreviewTabProps['queryInfo'], - FormattedQuery | null - > = createSelector( +export interface QueryPreviewInfoProps { + queryInfo: QueryInfo; + isWideLayout: boolean; +} +export class QueryPreviewInfo extends PureComponent { + selectFormattedQuery: Selector = createSelector( identity, - (queryInfo: QueryInfo | null): FormattedQuery | null => { - if (!queryInfo) { - return null; - } - + (queryInfo: QueryInfo): FormattedQuery => { const { query, queryKey, reducerPath } = queryInfo; const startedAt = query.startedTimeStamp @@ -57,23 +49,9 @@ export class QueryPreviewInfo extends PureComponent { ); render(): ReactNode { - const { queryInfo, isWideLayout, base16Theme, styling, invertTheme } = - this.props; - + const { queryInfo, isWideLayout } = this.props; const formattedQuery = this.selectFormattedQuery(queryInfo); - if (!formattedQuery) { - return null; - } - - return ( - - ); + return ; } } diff --git a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewSubscriptions.tsx b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewSubscriptions.tsx index 40c958cf..11446e34 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewSubscriptions.tsx +++ b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewSubscriptions.tsx @@ -1,60 +1,18 @@ import React, { ReactNode, PureComponent } from 'react'; -import { QueryPreviewTabProps } from '../types'; +import { RtkQueryApiState } from '../types'; import { TreeView } from './TreeView'; -export interface QueryPreviewSubscriptionsState { - data: { subscriptions: QueryPreviewTabProps['querySubscriptions'] }; +export interface QueryPreviewSubscriptionsProps { + subscriptions: RtkQueryApiState['subscriptions'][keyof RtkQueryApiState['subscriptions']]; + isWideLayout: boolean; } -export class QueryPreviewSubscriptions extends PureComponent< - QueryPreviewTabProps, - QueryPreviewSubscriptionsState -> { - static getDerivedStateFromProps( - props: QueryPreviewTabProps, - state: QueryPreviewSubscriptionsState - ): Partial | null { - if (props.querySubscriptions !== state.data.subscriptions) { - return { - data: { subscriptions: props.querySubscriptions }, - }; - } - - return null; - } - - constructor(props: QueryPreviewTabProps) { - super(props); - - this.state = { - data: { subscriptions: props.querySubscriptions }, - }; - } - +export class QueryPreviewSubscriptions extends PureComponent { render(): ReactNode { - const { - queryInfo, - isWideLayout, - base16Theme, - styling, - invertTheme, - querySubscriptions, - } = this.props; - - if (!querySubscriptions || !queryInfo) { - return null; - } + const { subscriptions } = this.props; return ( - <> - - + ); } } diff --git a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewTags.tsx b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewTags.tsx index d62534de..ba576d78 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewTags.tsx +++ b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewTags.tsx @@ -1,29 +1,21 @@ import React, { ReactNode, PureComponent } from 'react'; -import { QueryPreviewTabProps } from '../types'; +import { RtkQueryTag } from '../types'; import { TreeView } from './TreeView'; interface QueryPreviewTagsState { - data: { tags: QueryPreviewTabProps['tags'] }; + data: { tags: RtkQueryTag[] }; +} + +export interface QueryPreviewTagsProps { + tags: RtkQueryTag[]; + isWideLayout: boolean; } export class QueryPreviewTags extends PureComponent< - QueryPreviewTabProps, + QueryPreviewTagsProps, QueryPreviewTagsState > { - static getDerivedStateFromProps( - { tags }: QueryPreviewTabProps, - state: QueryPreviewTagsState - ): QueryPreviewTagsState | null { - if (tags !== state.data.tags) { - return { - data: { tags }, - }; - } - - return null; - } - - constructor(props: QueryPreviewTabProps) { + constructor(props: QueryPreviewTagsProps) { super(props); this.state = { @@ -32,21 +24,8 @@ export class QueryPreviewTags extends PureComponent< } render(): ReactNode { - const { queryInfo, isWideLayout, base16Theme, styling, invertTheme } = - this.props; + const { isWideLayout, tags } = this.props; - if (!queryInfo) { - return null; - } - - return ( - - ); + return ; } } diff --git a/packages/redux-devtools-rtk-query-monitor/src/components/TreeView.tsx b/packages/redux-devtools-rtk-query-monitor/src/components/TreeView.tsx index 659a38f7..8c8e62a4 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/components/TreeView.tsx +++ b/packages/redux-devtools-rtk-query-monitor/src/components/TreeView.tsx @@ -1,11 +1,16 @@ +import { createSelector } from '@reduxjs/toolkit'; import React, { ComponentProps, ReactNode } from 'react'; import JSONTree from 'react-json-tree'; +import { StylingFunction } from 'react-base16-styling'; import { DATA_TYPE_KEY } from '../monitor-config'; -import { getJsonTreeTheme } from '../styles/createStylingFromTheme'; +import { + getJsonTreeTheme, + StyleUtilsContext, +} from '../styles/createStylingFromTheme'; import { createTreeItemLabelRenderer, getItemString } from '../styles/tree'; -import { StyleUtils } from '../types'; +import { identity } from '../utils/object'; -export interface TreeViewProps extends StyleUtils { +export interface TreeViewProps { data: unknown; isWideLayout: boolean; before?: ReactNode; @@ -15,43 +20,48 @@ export interface TreeViewProps extends StyleUtils { } export class TreeView extends React.PureComponent { - readonly labelRenderer: ReturnType; + readonly selectLabelRenderer = createSelector< + StylingFunction, + StylingFunction, + ReturnType + >(identity, 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; + const { isWideLayout, data, before, after, children, keyPath } = this.props; return ( -
- {before} - - getItemString(styling, type, data, DATA_TYPE_KEY, isWideLayout) - } - hideRoot - /> - {after} - {children} -
+ + {({ styling, invertTheme, base16Theme }) => { + return ( +
+ {before} + + getItemString( + styling, + type, + data, + DATA_TYPE_KEY, + isWideLayout + ) + } + hideRoot + /> + {after} + {children} +
+ ); + }} +
); } } diff --git a/packages/redux-devtools-rtk-query-monitor/src/containers/RtkQueryInspector.tsx b/packages/redux-devtools-rtk-query-monitor/src/containers/RtkQueryInspector.tsx index 15d2b1a5..8651c2fe 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/containers/RtkQueryInspector.tsx +++ b/packages/redux-devtools-rtk-query-monitor/src/containers/RtkQueryInspector.tsx @@ -18,7 +18,6 @@ import { import { QueryList } from '../components/QueryList'; import { QueryForm } from '../components/QueryForm'; import { QueryPreview } from '../components/QueryPreview'; -import { getApiStateOf, getQuerySubscriptionsOf } from '../utils/rtk-query'; type ForwardedMonitorProps> = Pick< LiftedState, @@ -115,26 +114,15 @@ class RtkQueryInspector> extends PureComponent< const { styleUtils: { styling }, } = this.props; - const apiStates = this.selectors.selectApiStates(selectorsSource); const allVisibleQueries = this.selectors.selectAllVisbileQueries(selectorsSource); const currentQueryInfo = this.selectors.selectCurrentQueryInfo(selectorsSource); - const currentRtkApi = - this.selectors.selectApiOfCurrentQuery(selectorsSource); - const currentQuerySubscriptions = getQuerySubscriptionsOf( - currentQueryInfo, - apiStates - ); + const apiStates = this.selectors.selectApiStates(selectorsSource); - const currentTags = this.selectors.selectCurrentQueryTags(selectorsSource); - - const currentApiStats = - this.selectors.selectApiStatsOfCurrentQuery(selectorsSource); - - const hasNoApis = apiStates == null; + const hasNoApi = apiStates == null; return (
> extends PureComponent< selectedQueryKey={selectorsSource.monitorState.selectedQueryKey} />
- + selectorsSource={this.state.selectorsSource} + selectors={this.selectors} queryInfo={currentQueryInfo} selectedTab={selectorsSource.monitorState.selectedPreviewTab} onTabChange={this.handleTabChange} styling={styling} - tags={currentTags} - querySubscriptions={currentQuerySubscriptions} - apiState={currentRtkApi} isWideLayout={isWideLayout} - apiStats={currentApiStats} - hasNoApis={hasNoApis} + hasNoApis={hasNoApi} /> ); diff --git a/packages/redux-devtools-rtk-query-monitor/src/containers/mapProps.tsx b/packages/redux-devtools-rtk-query-monitor/src/containers/mapProps.tsx new file mode 100644 index 00000000..2dc00182 --- /dev/null +++ b/packages/redux-devtools-rtk-query-monitor/src/containers/mapProps.tsx @@ -0,0 +1,29 @@ +import React, { ComponentType, ReactNode, Component } from 'react'; + +interface Mapper { + (inProps: In): Out; +} + +interface MapPropsOutput { + (comp: ComponentType): ComponentType; +} + +export function mapProps( + mapper: Mapper +): MapPropsOutput { + return function mapPropsHoc(Comp) { + class MapPropsHoc extends Component { + render(): ReactNode { + const mappedProps = mapper(this.props); + + return ; + } + + static displayName = `mapProps(${ + Comp.displayName || Comp.name || 'Component' + })`; + } + + return MapPropsHoc; + }; +} diff --git a/packages/redux-devtools-rtk-query-monitor/src/selectors.ts b/packages/redux-devtools-rtk-query-monitor/src/selectors.ts index 069055aa..94dbe503 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/selectors.ts +++ b/packages/redux-devtools-rtk-query-monitor/src/selectors.ts @@ -7,9 +7,11 @@ import { RtkQueryTag, SelectorsSource, RtkQueryProvided, + QueryPreviewTabs, } from './types'; import { Comparator, queryComparators } from './utils/comparators'; import { FilterList, queryListFilters } from './utils/filters'; +import { emptyRecord } from './utils/object'; import { escapeRegExpSpecialCharacter } from './utils/regexp'; import { getApiStatesOf, @@ -63,6 +65,14 @@ export interface InspectorSelectors { S, RtkQueryApiState | null >; + readonly selectTabCounters: InspectorSelector< + S, + Record + >; + readonly selectSubscriptionsOfCurrentQuery: InspectorSelector< + S, + RtkQueryApiState['subscriptions'][string] + >; } export function createInspectorSelectors(): InspectorSelectors { @@ -161,6 +171,17 @@ export function createInspectorSelectors(): InspectorSelectors { return selectApiOfCurrentQuery(selectorsSource)?.provided ?? null; }; + const selectSubscriptionsOfCurrentQuery = createSelector( + [selectApiOfCurrentQuery, selectCurrentQueryInfo], + (apiState, queryInfo) => { + if (!queryInfo || !apiState) { + return emptyRecord; + } + + return apiState.subscriptions[queryInfo.queryKey]; + } + ); + const selectCurrentQueryTags = createSelector( [selectCurrentQueryInfo, selectProvidedOfCurrentQuery], getQueryTagsOf @@ -171,6 +192,21 @@ export function createInspectorSelectors(): InspectorSelectors { generateApiStatsOfCurrentQuery ); + const selectTabCounters = ( + selectorsSource: SelectorsSource + ): Record => { + const subscriptions = selectSubscriptionsOfCurrentQuery(selectorsSource); + const subsLen = Object.keys(subscriptions ?? {}).length; + + return { + [QueryPreviewTabs.queryTags]: + selectCurrentQueryTags(selectorsSource).length, + [QueryPreviewTabs.querySubscriptions]: subsLen, + [QueryPreviewTabs.apiConfig]: 0, + [QueryPreviewTabs.queryinfo]: 0, + }; + }; + return { selectQueryComparator, selectApiStates, @@ -180,6 +216,8 @@ export function createInspectorSelectors(): InspectorSelectors { selectCurrentQueryInfo, selectCurrentQueryTags, selectApiStatsOfCurrentQuery, + selectSubscriptionsOfCurrentQuery, selectApiOfCurrentQuery, + selectTabCounters, }; } diff --git a/packages/redux-devtools-rtk-query-monitor/src/types.ts b/packages/redux-devtools-rtk-query-monitor/src/types.ts index a8f270dc..8dc56880 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/types.ts +++ b/packages/redux-devtools-rtk-query-monitor/src/types.ts @@ -132,24 +132,10 @@ export interface ApiStats { }>; } -export interface QueryPreviewTabProps extends StyleUtils { - queryInfo: QueryInfo | null; - apiState: RtkQueryApiState | null; - querySubscriptions: RTKQuerySubscribers | null; - isWideLayout: boolean; - tags: RtkQueryTag[]; - apiStats: ApiStats | null; -} - export interface TabOption extends SelectOption { component: ComponentType

; } -export type QueryPreviewTabOption = TabOption< - QueryPreviewTabs, - QueryPreviewTabProps ->; - /** * It is Omit & { isFetching: boolean; } */