feat(search): add search logic and refactor monitor state shape

This commit is contained in:
FaberVitale 2021-06-12 19:30:01 +02:00
parent 143a01edba
commit ad5b52a4e7
11 changed files with 165 additions and 78 deletions

View File

@ -18,7 +18,8 @@
"react-scripts": "4.0.2", "react-scripts": "4.0.2",
"redux": "^4.0.5", "redux": "^4.0.5",
"redux-devtools-themes": "^1.0.0", "redux-devtools-themes": "^1.0.0",
"react-json-tree": "^0.15.0" "react-json-tree": "^0.15.0",
"lodash.debounce": "^4.0.8"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "17.0.0", "@types/react": "17.0.0",

View File

@ -46,7 +46,8 @@
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"redux-devtools-themes": "^1.0.0", "redux-devtools-themes": "^1.0.0",
"devui": "^1.0.0-8", "devui": "^1.0.0-8",
"react-json-tree": "^0.15.0" "react-json-tree": "^0.15.0",
"lodash.debounce": "^4.0.8"
}, },
"devDependencies": { "devDependencies": {
"@redux-devtools/core": "^3.9.0", "@redux-devtools/core": "^3.9.0",

View File

@ -3,16 +3,20 @@ import { AnyAction, Dispatch, Action } from 'redux';
import { LiftedAction, LiftedState } from '@redux-devtools/core'; import { LiftedAction, LiftedState } from '@redux-devtools/core';
import * as themes from 'redux-devtools-themes'; import * as themes from 'redux-devtools-themes';
import { Base16Theme } from 'react-base16-styling'; import { Base16Theme } from 'react-base16-styling';
import { QueryInfo, RtkQueryInspectorMonitorState } from './types'; import {
QueryFormValues,
QueryInfo,
RtkQueryInspectorMonitorState,
} from './types';
import { createInspectorSelectors, computeSelectorSource } from './selectors'; import { createInspectorSelectors, computeSelectorSource } from './selectors';
import { selectQueryKey } from './reducers'; import { changeQueryFormValues, selectQueryKey } from './reducers';
import { QueryList } from './components/QueryList'; import { QueryList } from './components/QueryList';
import { StyleUtils } from './styles/createStylingFromTheme'; 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';
type SelectorsSource<S> = { type SelectorsSource<S> = {
currentState: S | null; userState: S | null;
monitorState: RtkQueryInspectorMonitorState; monitorState: RtkQueryInspectorMonitorState;
}; };
@ -47,7 +51,7 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
}; };
} }
static wideLayout = 500; static wideLayout = 650;
static getDerivedStateFromProps( static getDerivedStateFromProps(
props: RtkQueryInspectorProps<unknown, Action<unknown>>, props: RtkQueryInspectorProps<unknown, Action<unknown>>,
@ -80,18 +84,22 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
} }
}; };
componentDidMount() { componentDidMount(): void {
this.updateSizeMode(); this.updateSizeMode();
this.isWideIntervalRef = setInterval(this.updateSizeMode, 200); this.isWideIntervalRef = setInterval(this.updateSizeMode, 200);
} }
componentWillUnmount() { componentWillUnmount(): void {
if (this.isWideIntervalRef) { if (this.isWideIntervalRef) {
clearTimeout(this.isWideIntervalRef as any); clearTimeout(this.isWideIntervalRef as any);
} }
} }
handleQueryFormValuesChange = (values: Partial<QueryFormValues>): void => {
this.props.dispatch(changeQueryFormValues(values) as AnyAction);
};
handleSelectQuery = (queryInfo: QueryInfo): void => { handleSelectQuery = (queryInfo: QueryInfo): void => {
this.props.dispatch(selectQueryKey(queryInfo) as AnyAction); this.props.dispatch(selectQueryKey(queryInfo) as AnyAction);
}; };
@ -102,7 +110,7 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
styleUtils: { styling }, styleUtils: { styling },
} = this.props; } = this.props;
const apiStates = this.selectors.selectApiStates(selectorsSource); const apiStates = this.selectors.selectApiStates(selectorsSource);
const allSortedQueries = this.selectors.selectAllSortedQueries( const allVisibleQueries = this.selectors.selectAllVisbileQueries(
selectorsSource selectorsSource
); );
@ -112,7 +120,7 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
console.log('inspector', { console.log('inspector', {
apiStates, apiStates,
allSortedQueries, allVisibleQueries,
selectorsSource, selectorsSource,
currentQueryInfo, currentQueryInfo,
}); });
@ -129,14 +137,12 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
> >
<QueryForm <QueryForm
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
queryComparator={selectorsSource.monitorState.queryComparator} values={selectorsSource.monitorState.queryForm.values}
isAscendingQueryComparatorOrder={ onFormValuesChange={this.handleQueryFormValuesChange}
selectorsSource.monitorState.isAscendingQueryComparatorOrder
}
/> />
<QueryList <QueryList
onSelectQuery={this.handleSelectQuery} onSelectQuery={this.handleSelectQuery}
queryInfos={allSortedQueries} queryInfos={allVisibleQueries}
selectedQueryKey={selectorsSource.monitorState.selectedQueryKey} selectedQueryKey={selectorsSource.monitorState.selectedQueryKey}
/> />
</div> </div>

View File

@ -1,19 +1,20 @@
import React, { ReactNode, FormEvent, MouseEvent } from 'react'; import React, { ReactNode, FormEvent, MouseEvent, ChangeEvent } from 'react';
import { RtkQueryInspectorMonitorState } from '../types'; import { QueryFormValues } from '../types';
import { StyleUtilsContext } from '../styles/createStylingFromTheme'; import { StyleUtilsContext } from '../styles/createStylingFromTheme';
import { Select } from 'devui'; import { Select } from 'devui';
import { SelectOption } from '../types';
import { AnyAction } from 'redux'; import { AnyAction } from 'redux';
import debounce from 'lodash.debounce';
import { sortQueryOptions, QueryComparators } from '../utils/comparators'; import { sortQueryOptions, QueryComparators } from '../utils/comparators';
import {
changeIsAscendingQueryComparatorOrder, export interface QueryFormProps {
changeQueryComparator,
} from '../reducers';
export interface QueryFormProps
extends Pick<
RtkQueryInspectorMonitorState,
'isAscendingQueryComparatorOrder' | 'queryComparator'
> {
dispatch: (action: AnyAction) => void; dispatch: (action: AnyAction) => void;
values: QueryFormValues;
onFormValuesChange: (values: Partial<QueryFormValues>) => void;
}
interface QueryFormState {
searchValue: string;
} }
const ascId = 'rtk-query-rb-asc'; const ascId = 'rtk-query-rb-asc';
@ -23,37 +24,71 @@ const searchId = 'rtk-query-search-query';
const searchPlaceholder = 'filter query...'; const searchPlaceholder = 'filter query...';
export class QueryForm extends React.PureComponent<QueryFormProps> { export class QueryForm extends React.PureComponent<
QueryFormProps,
QueryFormState
> {
constructor(props: QueryFormProps) {
super(props);
this.state = {
searchValue: props.values.searchValue,
};
}
inputSearchRef = React.createRef<HTMLInputElement>();
handleSubmit = (evt: FormEvent<HTMLFormElement>): void => { handleSubmit = (evt: FormEvent<HTMLFormElement>): void => {
evt.preventDefault(); evt.preventDefault();
}; };
handleButtonGroupClick = ({ target }: MouseEvent<HTMLElement>): void => { handleButtonGroupClick = ({ target }: MouseEvent<HTMLElement>): void => {
const { isAscendingQueryComparatorOrder: isAsc, dispatch } = this.props; const {
values: { isAscendingQueryComparatorOrder: isAsc },
onFormValuesChange,
} = this.props;
const targetId = (target as HTMLElement)?.id ?? null; const targetId = (target as HTMLElement)?.id ?? null;
if (targetId === ascId && !isAsc) { if (targetId === ascId && !isAsc) {
dispatch(changeIsAscendingQueryComparatorOrder(true)); onFormValuesChange({ isAscendingQueryComparatorOrder: true });
} else if (targetId === descId && isAsc) { } else if (targetId === descId && isAsc) {
this.props.dispatch(changeIsAscendingQueryComparatorOrder(false)); onFormValuesChange({ isAscendingQueryComparatorOrder: false });
} }
}; };
handleSelectComparator = (option: { value: string }): void => { handleSelectComparator = (
const { dispatch } = this.props; option: SelectOption<QueryComparators> | undefined | null
): void => {
if (typeof option?.value === 'string') { if (typeof option?.value === 'string') {
dispatch(changeQueryComparator(option.value as QueryComparators)); this.props.onFormValuesChange({ queryComparator: option.value });
} }
}; };
getSelectedOption = (option: { value: string }): string => option?.value; restoreCaretPosition = (start: number | null, end: number | null): void => {
window.requestAnimationFrame(() => {
if (this.inputSearchRef.current) {
this.inputSearchRef.current.selectionStart = start;
this.inputSearchRef.current.selectionEnd = end;
}
});
};
invalidateSearchValueFromProps = debounce(() => {
this.props.onFormValuesChange({
searchValue: this.state.searchValue,
});
}, 150);
handleSearchChange = (evt: ChangeEvent<HTMLInputElement>): void => {
const searchValue = evt.target.value.trim();
this.setState({ searchValue });
this.invalidateSearchValueFromProps();
};
render(): ReactNode { render(): ReactNode {
const { const {
isAscendingQueryComparatorOrder: isAsc, values: { isAscendingQueryComparatorOrder: isAsc, queryComparator },
queryComparator,
} = this.props; } = this.props;
const isDesc = !isAsc; const isDesc = !isAsc;
@ -72,24 +107,25 @@ export class QueryForm extends React.PureComponent<QueryFormProps> {
filter query filter query
</label> </label>
<input <input
ref={this.inputSearchRef}
type="search" type="search"
value={this.state.searchValue}
onChange={this.handleSearchChange}
placeholder={searchPlaceholder} placeholder={searchPlaceholder}
{...styling('querySearch')} {...styling('querySearch')}
/> />
</div> </div>
<div {...styling('sortBySection')}> <div {...styling('sortBySection')}>
<label htmlFor={selectId}>Sort by</label> <label htmlFor={selectId}>Sort by</label>
<Select <Select<SelectOption<QueryComparators>>
id={selectId} id={selectId}
isSearchable={false} isSearchable={false}
openOuterUp theme={base16Theme as any}
theme={base16Theme}
value={sortQueryOptions.find( value={sortQueryOptions.find(
(opt) => opt?.value === queryComparator (opt) => opt?.value === queryComparator
)} )}
options={sortQueryOptions} options={sortQueryOptions}
onChange={this.handleSelectComparator} onChange={this.handleSelectComparator}
selectOption={this.getSelectedOption}
/> />
<div <div
tabIndex={0} tabIndex={0}

View File

@ -68,7 +68,7 @@ export class QueryPreview extends React.PureComponent<QueryPreviewProps> {
<li>{`loaded at: ${latestFetch}`}</li> <li>{`loaded at: ${latestFetch}`}</li>
<li>{`requested at: ${startedAt}`}</li> <li>{`requested at: ${startedAt}`}</li>
</ul> </ul>
<div style={{ padding: '1em' }}> <div {...styling('treeWrapper')}>
<JSONTree <JSONTree
data={data} data={data}
labelRenderer={this.labelRenderer} labelRenderer={this.labelRenderer}

View File

@ -1,12 +1,21 @@
import { Action, AnyAction } from 'redux'; import { Action, AnyAction } from 'redux';
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RtkQueryInspectorProps } from './RtkQueryInspector'; import { RtkQueryInspectorProps } from './RtkQueryInspector';
import { QueryInfo, RtkQueryInspectorMonitorState } from './types'; import {
QueryInfo,
RtkQueryInspectorMonitorState,
QueryFormValues,
} from './types';
import { QueryComparators } from './utils/comparators'; import { QueryComparators } from './utils/comparators';
const initialState: RtkQueryInspectorMonitorState = { const initialState: RtkQueryInspectorMonitorState = {
queryForm: {
values: {
queryComparator: QueryComparators.fulfilledTimeStamp, queryComparator: QueryComparators.fulfilledTimeStamp,
isAscendingQueryComparatorOrder: false, isAscendingQueryComparatorOrder: false,
searchValue: '',
},
},
selectedQueryKey: null, selectedQueryKey: null,
}; };
@ -14,14 +23,11 @@ const monitorSlice = createSlice({
name: 'rtk-query-monitor', name: 'rtk-query-monitor',
initialState, initialState,
reducers: { reducers: {
changeQueryComparator(state, action: PayloadAction<QueryComparators>) { changeQueryFormValues(
state.queryComparator = action.payload;
},
changeIsAscendingQueryComparatorOrder(
state, state,
action: PayloadAction<boolean> action: PayloadAction<Partial<QueryFormValues>>
) { ) {
state.isAscendingQueryComparatorOrder = !!action.payload; state.queryForm.values = { ...state.queryForm.values, ...action.payload };
}, },
selectQueryKey( selectQueryKey(
state, state,
@ -45,8 +51,4 @@ export default function reducer<S, A extends Action<unknown>>(
return output; return output;
} }
export const { export const { selectQueryKey, changeQueryFormValues } = monitorSlice.actions;
changeIsAscendingQueryComparatorOrder,
changeQueryComparator,
selectQueryKey,
} = monitorSlice.actions;

View File

@ -1,18 +1,14 @@
import { Action, createSelector, Selector } from '@reduxjs/toolkit'; import { Action, createSelector, Selector } from '@reduxjs/toolkit';
import { RtkQueryInspectorProps } from './RtkQueryInspector'; import { RtkQueryInspectorProps } from './RtkQueryInspector';
import { QueryInfo, RtkQueryInspectorMonitorState } from './types'; import { QueryInfo, SelectorsSource } from './types';
import { Comparator, queryComparators } from './utils/comparators'; import { Comparator, queryComparators } from './utils/comparators';
import { escapeRegExpSpecialCharacter } from './utils/regexp';
import { import {
getApiStatesOf, getApiStatesOf,
extractAllApiQueries, extractAllApiQueries,
flipComparator, flipComparator,
} from './utils/rtk-query'; } from './utils/rtk-query';
type SelectorsSource<S> = {
currentState: S | null;
monitorState: RtkQueryInspectorMonitorState;
};
type InspectorSelector<S, Output> = Selector<SelectorsSource<S>, Output>; type InspectorSelector<S, Output> = Selector<SelectorsSource<S>, Output>;
export function computeSelectorSource<S, A extends Action<unknown>>( export function computeSelectorSource<S, A extends Action<unknown>>(
@ -21,16 +17,16 @@ export function computeSelectorSource<S, A extends Action<unknown>>(
): SelectorsSource<S> { ): SelectorsSource<S> {
const { computedStates, currentStateIndex, monitorState } = props; const { computedStates, currentStateIndex, monitorState } = props;
const currentState = const userState =
computedStates.length > 0 ? computedStates[currentStateIndex].state : null; computedStates.length > 0 ? computedStates[currentStateIndex].state : null;
if ( if (
!previous || !previous ||
previous.currentState !== currentState || previous.userState !== userState ||
previous.monitorState !== monitorState previous.monitorState !== monitorState
) { ) {
return { return {
currentState, userState,
monitorState, monitorState,
}; };
} }
@ -48,19 +44,20 @@ export interface InspectorSelectors<S> {
S, S,
ReturnType<typeof extractAllApiQueries> ReturnType<typeof extractAllApiQueries>
>; >;
readonly selectAllSortedQueries: 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>;
} }
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<QueryInfo> => {
return queryComparators[monitorState.queryComparator]; return queryComparators[monitorState.queryForm.values.queryComparator];
}; };
const selectApiStates = createSelector( const selectApiStates = createSelector(
({ currentState }: SelectorsSource<S>) => currentState, ({ userState }: SelectorsSource<S>) => userState,
getApiStatesOf getApiStatesOf
); );
const selectAllQueries = createSelector( const selectAllQueries = createSelector(
@ -68,21 +65,35 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
extractAllApiQueries extractAllApiQueries
); );
const selectAllSortedQueries = createSelector( const selectSearchQueryRegex = createSelector(
({ monitorState }: SelectorsSource<S>) =>
monitorState.queryForm.values.searchValue,
(searchValue) => {
if (searchValue.length >= 3) {
return new RegExp(escapeRegExpSpecialCharacter(searchValue), 'i');
}
return null;
}
);
const selectAllVisbileQueries = createSelector(
[ [
selectQueryComparator, selectQueryComparator,
selectAllQueries, selectAllQueries,
({ monitorState }: SelectorsSource<S>) => ({ monitorState }: SelectorsSource<S>) =>
monitorState.isAscendingQueryComparatorOrder, monitorState.queryForm.values.isAscendingQueryComparatorOrder,
selectSearchQueryRegex,
], ],
(comparator, queryList, isAscending) => { (comparator, queryList, isAscending, searchRegex) => {
console.log({ comparator, queryList, isAscending }); const filteredList = searchRegex
? queryList.filter((queryInfo) => searchRegex.test(queryInfo.queryKey))
: queryList.slice();
const computedComparator = isAscending const computedComparator = isAscending
? comparator ? comparator
: flipComparator(comparator); : flipComparator(comparator);
return queryList.slice().sort(computedComparator); return filteredList.sort(computedComparator);
} }
); );
@ -109,7 +120,8 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
selectQueryComparator, selectQueryComparator,
selectApiStates, selectApiStates,
selectAllQueries, selectAllQueries,
selectAllSortedQueries, selectAllVisbileQueries,
selectSearchQueryRegex,
selectorCurrentQueryInfo, selectorCurrentQueryInfo,
}; };
} }

View File

@ -52,6 +52,7 @@ const getSheetFromColorMap = (map: ColorMap) => ({
inspector: { inspector: {
display: 'flex', display: 'flex',
flexFlow: 'column nowrap', flexFlow: 'column nowrap',
overflow: 'hidden',
width: '100%', width: '100%',
height: '100%', height: '100%',
'font-family': 'monaco, Consolas, "Lucida Console", monospace', 'font-family': 'monaco, Consolas, "Lucida Console", monospace',
@ -253,6 +254,8 @@ const getSheetFromColorMap = (map: ColorMap) => ({
queryPreview: { queryPreview: {
flex: '1 1 50%', flex: '1 1 50%',
overflowX: 'hidden',
oveflowY: 'auto',
display: 'flex', display: 'flex',
'flex-direction': 'column', 'flex-direction': 'column',
'overflow-y': 'hidden', 'overflow-y': 'hidden',
@ -291,6 +294,12 @@ const getSheetFromColorMap = (map: ColorMap) => ({
treeItemKey: { treeItemKey: {
color: map.TEXT_PLACEHOLDER_COLOR, color: map.TEXT_PLACEHOLDER_COLOR,
}, },
treeWrapper: {
overflowX: 'auto',
overflowY: 'auto',
padding: '1em',
},
}); });
let themeSheet: StyleSheet; let themeSheet: StyleSheet;

View File

@ -6,10 +6,16 @@ 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 interface RtkQueryInspectorMonitorState { export interface QueryFormValues {
queryComparator: QueryComparators; queryComparator: QueryComparators;
isAscendingQueryComparatorOrder: boolean; isAscendingQueryComparatorOrder: boolean;
selectedQueryKey: Pick<QueryInfo, 'reducerPath' | 'queryKey'> | null; searchValue: string;
}
export interface RtkQueryInspectorMonitorState {
readonly queryForm: {
values: QueryFormValues;
};
readonly selectedQueryKey: Pick<QueryInfo, 'reducerPath' | 'queryKey'> | null;
} }
export interface RtkQueryInspectorMonitorProps<S, A extends Action<unknown>> export interface RtkQueryInspectorMonitorProps<S, A extends Action<unknown>>
@ -63,3 +69,13 @@ export interface ApiInfo {
reducerPath: string; reducerPath: string;
apiState: RtkQueryApiState; apiState: RtkQueryApiState;
} }
export interface SelectOption<T = string> {
label: string;
value: T;
}
export interface SelectorsSource<S> {
userState: S | null;
monitorState: RtkQueryInspectorMonitorState;
}

View File

@ -1,5 +1,5 @@
import { QueryStatus } from '@reduxjs/toolkit/dist/query'; import { QueryStatus } from '@reduxjs/toolkit/dist/query';
import { QueryInfo } from '../types'; import { QueryInfo, SelectOption } from '../types';
export interface Comparator<T> { export interface Comparator<T> {
(a: T, b: T): number; (a: T, b: T): number;
@ -12,7 +12,7 @@ export enum QueryComparators {
endpointName = 'endpointName', endpointName = 'endpointName',
} }
export const sortQueryOptions: { label: string; value: string }[] = [ export const sortQueryOptions: SelectOption<QueryComparators>[] = [
{ label: 'fulfilledTimeStamp', value: QueryComparators.fulfilledTimeStamp }, { label: 'fulfilledTimeStamp', value: QueryComparators.fulfilledTimeStamp },
{ label: 'query key', value: QueryComparators.queryKey }, { label: 'query key', value: QueryComparators.queryKey },
{ label: 'status ', value: QueryComparators.status }, { label: 'status ', value: QueryComparators.status },

View File

@ -0,0 +1,4 @@
// https://stackoverflow.com/a/9310752
export function escapeRegExpSpecialCharacter(text: string): string {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}