feat(rtk-query): add multiple filter options

This commit is contained in:
FaberVitale 2021-06-16 18:13:31 +02:00
parent 825a468ff1
commit 2ce2dc6e5e
8 changed files with 435 additions and 260 deletions

View File

@ -156,7 +156,6 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
data-wide-layout={+this.state.isWideLayout}
>
<QueryForm
dispatch={this.props.dispatch}
values={selectorsSource.monitorState.queryForm.values}
onFormValuesChange={this.handleQueryFormValuesChange}
/>

View File

@ -3,12 +3,11 @@ import { QueryFormValues } from '../types';
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
import { Select } from 'devui';
import { SelectOption } from '../types';
import { AnyAction } from 'redux';
import debounce from 'lodash.debounce';
import { sortQueryOptions, QueryComparators } from '../utils/comparators';
import { QueryFilters, filterQueryOptions } from '../utils/filters';
export interface QueryFormProps {
dispatch: (action: AnyAction) => void;
values: QueryFormValues;
onFormValuesChange: (values: Partial<QueryFormValues>) => void;
}
@ -21,8 +20,8 @@ const ascId = 'rtk-query-rb-asc';
const descId = 'rtk-query-rb-desc';
const selectId = 'rtk-query-comp-select';
const searchId = 'rtk-query-search-query';
const searchPlaceholder = 'filter query...';
const filterSelectId = 'rtk-query-search-query-select';
const searchPlaceholder = 'filter query by...';
export class QueryForm extends React.PureComponent<
QueryFormProps,
@ -57,7 +56,7 @@ export class QueryForm extends React.PureComponent<
}
};
handleSelectComparator = (
handleSelectComparatorChange = (
option: SelectOption<QueryComparators> | undefined | null
): void => {
if (typeof option?.value === 'string') {
@ -65,6 +64,14 @@ export class QueryForm extends React.PureComponent<
}
};
handleSelectFilterChange = (
option: SelectOption<QueryFilters> | undefined | null
): void => {
if (typeof option?.value === 'string') {
this.props.onFormValuesChange({ queryFilter: option.value });
}
};
restoreCaretPosition = (start: number | null, end: number | null): void => {
window.requestAnimationFrame(() => {
if (this.inputSearchRef.current) {
@ -86,9 +93,21 @@ export class QueryForm extends React.PureComponent<
this.invalidateSearchValueFromProps();
};
handleClearSearchClick = (evt: MouseEvent<HTMLButtonElement>): void => {
evt.preventDefault();
if (this.state.searchValue) {
this.setState({ searchValue: '' });
}
};
render(): ReactNode {
const {
values: { isAscendingQueryComparatorOrder: isAsc, queryComparator },
values: {
isAscendingQueryComparatorOrder: isAsc,
queryComparator,
queryFilter,
},
} = this.props;
const isDesc = !isAsc;
@ -106,13 +125,36 @@ export class QueryForm extends React.PureComponent<
<label htmlFor={searchId} {...styling('srOnly')}>
filter query
</label>
<input
ref={this.inputSearchRef}
type="search"
value={this.state.searchValue}
onChange={this.handleSearchChange}
placeholder={searchPlaceholder}
{...styling('querySearch')}
<div {...styling('querySearch')}>
<input
ref={this.inputSearchRef}
type="search"
value={this.state.searchValue}
onChange={this.handleSearchChange}
placeholder={searchPlaceholder}
/>
<button
type="reset"
aria-label="clear search"
data-invisible={
+(this.state.searchValue.length === 0) || undefined
}
onClick={this.handleClearSearchClick}
{...styling('closeButton')}
/>
</div>
<label htmlFor={selectId} {...styling('srOnly')}>
filter by
</label>
<Select<SelectOption<QueryFilters>>
id={filterSelectId}
isSearchable={false}
options={filterQueryOptions}
theme={base16Theme as any}
value={filterQueryOptions.find(
(opt) => opt?.value === queryFilter
)}
onChange={this.handleSelectFilterChange}
/>
</div>
<div {...styling('sortBySection')}>
@ -125,7 +167,7 @@ export class QueryForm extends React.PureComponent<
(opt) => opt?.value === queryComparator
)}
options={sortQueryOptions}
onChange={this.handleSelectComparator}
onChange={this.handleSelectComparatorChange}
/>
<div
tabIndex={0}

View File

@ -8,6 +8,7 @@ import {
QueryPreviewTabs,
} from './types';
import { QueryComparators } from './utils/comparators';
import { QueryFilters } from './utils/filters';
const initialState: RtkQueryInspectorMonitorState = {
queryForm: {
@ -15,6 +16,7 @@ const initialState: RtkQueryInspectorMonitorState = {
queryComparator: QueryComparators.fulfilledTimeStamp,
isAscendingQueryComparatorOrder: false,
searchValue: '',
queryFilter: QueryFilters.queryKey,
},
},
selectedPreviewTab: QueryPreviewTabs.queryinfo,

View File

@ -2,6 +2,7 @@ import { Action, createSelector, Selector } from '@reduxjs/toolkit';
import { RtkQueryInspectorProps } from './RtkQueryInspector';
import { QueryInfo, RtkQueryTag, SelectorsSource } from './types';
import { Comparator, queryComparators } from './utils/comparators';
import { FilterList, queryListFilters } from './utils/filters';
import { escapeRegExpSpecialCharacter } from './utils/regexp';
import {
getApiStatesOf,
@ -58,6 +59,12 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
return queryComparators[monitorState.queryForm.values.queryComparator];
};
const selectQueryListFilter = ({
monitorState,
}: SelectorsSource<S>): FilterList<QueryInfo> => {
return queryListFilters[monitorState.queryForm.values.queryFilter];
};
const selectApiStates = createSelector(
({ userState }: SelectorsSource<S>) => userState,
getApiStatesOf
@ -81,21 +88,23 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
const selectAllVisbileQueries = createSelector(
[
selectQueryComparator,
selectQueryListFilter,
selectAllQueries,
({ monitorState }: SelectorsSource<S>) =>
monitorState.queryForm.values.isAscendingQueryComparatorOrder,
selectSearchQueryRegex,
],
(comparator, queryList, isAscending, searchRegex) => {
const filteredList = searchRegex
? queryList.filter((queryInfo) => searchRegex.test(queryInfo.queryKey))
: queryList.slice();
(comparator, queryListFilter, queryList, isAscending, searchRegex) => {
const filteredList = queryListFilter(
searchRegex,
queryList as QueryInfo[]
);
const computedComparator = isAscending
? comparator
: flipComparator(comparator);
return filteredList.sort(computedComparator);
return filteredList.slice().sort(computedComparator);
}
);

View File

@ -45,259 +45,310 @@ type ColorMap = {
[color in Color]: string;
};
const getSheetFromColorMap = (map: ColorMap) => ({
inspector: {
display: 'flex',
flexFlow: 'column nowrap',
overflow: 'hidden',
width: '100%',
height: '100%',
'font-family': 'monaco, Consolas, "Lucida Console", monospace',
'font-size': '12px',
'font-smoothing': 'antialiased',
'line-height': '1.5em',
const getSheetFromColorMap = (map: ColorMap) => {
const appearanceNone = {
'-webkit-appearance': 'none',
};
'background-color': map.BACKGROUND_COLOR,
color: map.TEXT_COLOR,
'&[data-wide-layout="1"]': {
flexFlow: 'row nowrap',
},
},
querySectionWrapper: {
display: 'flex',
flex: '0 0 auto',
height: '50%',
width: '100%',
borderColor: map.TAB_BORDER_COLOR,
'&[data-wide-layout="0"]': {
borderBottomWidth: 1,
borderStyle: 'solid',
},
'&[data-wide-layout="1"]': {
return {
inspector: {
display: 'flex',
flexFlow: 'column nowrap',
overflow: 'hidden',
width: '100%',
height: '100%',
width: '40%',
borderRightWidth: 1,
borderStyle: 'solid',
},
flexFlow: 'column nowrap',
'& > :first-child': {
flex: '0 0 auto',
'border-bottom-width': '1px',
'border-bottom-style': 'solid',
'border-color': map.LIST_BORDER_COLOR,
},
'& > :nth-child(n + 2)': {
flex: '1 1 auto',
overflowX: 'hidden',
overflowY: 'auto',
maxHeight: 'calc(100% - 70px)',
},
},
'font-family': 'monaco, Consolas, "Lucida Console", monospace',
'font-size': '12px',
'font-smoothing': 'antialiased',
'line-height': '1.5em',
queryList: {
listStyle: 'none',
margin: '0',
padding: '0',
},
'background-color': map.BACKGROUND_COLOR,
color: map.TEXT_COLOR,
queryListItem: {
'border-bottom-width': '1px',
'border-bottom-style': 'solid',
display: 'flex',
'justify-content': 'space-between',
padding: '5px 10px',
cursor: 'pointer',
'user-select': 'none',
'& > :first-child': {
whiteSpace: 'nowrap',
overflowX: 'hidden',
maxWidth: 'calc(100% - 70px)',
textOverflow: 'ellipsis',
},
'&:last-child': {
'border-bottom-width': 0,
'&[data-wide-layout="1"]': {
flexFlow: 'row nowrap',
},
},
'border-bottom-color': map.BORDER_COLOR,
},
queryListHeader: {
display: 'flex',
flex: '0 0 auto',
'align-items': 'center',
'border-bottom-width': '1px',
'border-bottom-style': 'solid',
'border-color': map.LIST_BORDER_COLOR,
},
queryStatus: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
height: 22,
padding: '0 6px',
'border-radius': '3px',
'font-size': '0.7em',
'line-height': '1em',
'flex-shrink': 0,
'background-color': map.ACTION_TIME_BACK_COLOR,
color: map.ACTION_TIME_COLOR,
},
queryListItemSelected: {
'background-color': map.SELECTED_BACKGROUND_COLOR,
},
tabSelector: {
position: 'relative',
'z-index': 1,
display: 'inline-flex',
float: 'right',
},
srOnly: {
position: 'absolute',
width: 1,
height: 1,
padding: 0,
margin: '-1px',
overflow: 'hidden',
clip: 'rect(0,0,0,0)',
border: 0,
},
selectorButton: {
cursor: 'pointer',
position: 'relative',
padding: '6.5px 10px',
color: map.TEXT_COLOR,
'border-style': 'solid',
'border-width': '1px',
'border-left-width': 0,
'&:first-child': {
'border-left-width': '1px',
'border-top-left-radius': '3px',
'border-bottom-left-radius': '3px',
},
'&:last-child': {
'border-top-right-radius': '3px',
'border-bottom-right-radius': '3px',
},
'background-color': map.TAB_BACK_COLOR,
'&:hover': {
'background-color': map.TAB_BACK_HOVER_COLOR,
},
'border-color': map.TAB_BORDER_COLOR,
},
selectorButtonSmall: {
padding: '0px 8px',
'font-size': '0.8em',
},
selectorButtonSelected: {
'background-color': map.TAB_BACK_SELECTED_COLOR,
},
queryForm: {
display: 'flex',
flexFlow: 'column nowrap',
},
sortBySection: {
display: 'flex',
padding: '0.4em',
'& > [role="radiogroup"]': {
flex: '0 0 auto',
padding: '0 0 0 0.4em',
},
'& label': {
querySectionWrapper: {
display: 'flex',
flex: '0 0 auto',
whiteSpace: 'noWrap',
height: '50%',
width: '100%',
borderColor: map.TAB_BORDER_COLOR,
'&[data-wide-layout="0"]': {
borderBottomWidth: 1,
borderStyle: 'solid',
},
'&[data-wide-layout="1"]': {
height: '100%',
width: '40%',
borderRightWidth: 1,
borderStyle: 'solid',
},
flexFlow: 'column nowrap',
'& > :first-child': {
flex: '0 0 auto',
'border-bottom-width': '1px',
'border-bottom-style': 'solid',
'border-color': map.LIST_BORDER_COLOR,
},
'& > :nth-child(n + 2)': {
flex: '1 1 auto',
overflowX: 'hidden',
overflowY: 'auto',
maxHeight: 'calc(100% - 70px)',
},
},
queryList: {
listStyle: 'none',
margin: '0',
padding: '0',
},
queryListItem: {
'border-bottom-width': '1px',
'border-bottom-style': 'solid',
display: 'flex',
'justify-content': 'space-between',
padding: '5px 10px',
cursor: 'pointer',
'user-select': 'none',
'& > :first-child': {
whiteSpace: 'nowrap',
overflowX: 'hidden',
maxWidth: 'calc(100% - 70px)',
textOverflow: 'ellipsis',
},
'&:last-child': {
'border-bottom-width': 0,
},
'border-bottom-color': map.BORDER_COLOR,
},
queryListHeader: {
display: 'flex',
padding: 4,
flex: '0 0 auto',
'align-items': 'center',
'border-bottom-width': '1px',
'border-bottom-style': 'solid',
'border-color': map.LIST_BORDER_COLOR,
},
queryStatus: {
display: 'inline-flex',
alignItems: 'center',
paddingRight: '0.4em',
},
},
querySearch: {
outline: 'none',
border: 'none',
width: '100%',
padding: '5px 10px',
'font-size': '1em',
'font-family': 'monaco, Consolas, "Lucida Console", monospace',
'background-color': map.BACKGROUND_COLOR,
color: map.TEXT_COLOR,
'&::-webkit-input-placeholder': {
color: map.TEXT_PLACEHOLDER_COLOR,
},
'&::-moz-placeholder': {
color: map.TEXT_PLACEHOLDER_COLOR,
},
},
queryPreview: {
flex: '1 1 50%',
overflowX: 'hidden',
oveflowY: 'auto',
display: 'flex',
'flex-direction': 'column',
'overflow-y': 'hidden',
'& pre': {
border: 'inherit',
justifyContent: 'center',
height: 22,
padding: '0 6px',
'border-radius': '3px',
'line-height': 'inherit',
color: 'inherit',
'font-size': '0.7em',
'line-height': '1em',
'flex-shrink': 0,
'background-color': map.ACTION_TIME_BACK_COLOR,
color: map.ACTION_TIME_COLOR,
},
'background-color': map.BACKGROUND_COLOR,
},
previewHeader: {
flex: '0 0 30px',
padding: '5px 10px',
'align-items': 'center',
'border-bottom-width': '1px',
'border-bottom-style': 'solid',
'background-color': map.HEADER_BACKGROUND_COLOR,
'border-bottom-color': map.HEADER_BORDER_COLOR,
},
treeItemPin: {
'font-size': '0.7em',
'padding-left': '5px',
cursor: 'pointer',
'&:hover': {
'text-decoration': 'underline',
queryListItemSelected: {
'background-color': map.SELECTED_BACKGROUND_COLOR,
},
color: map.PIN_COLOR,
},
tabSelector: {
position: 'relative',
'z-index': 1,
display: 'inline-flex',
float: 'right',
},
treeItemKey: {
color: map.TEXT_PLACEHOLDER_COLOR,
},
srOnly: {
position: 'absolute',
width: 1,
height: 1,
padding: 0,
margin: '-1px',
overflow: 'hidden',
clip: 'rect(0,0,0,0)',
border: 0,
},
treeWrapper: {
overflowX: 'auto',
overflowY: 'auto',
padding: '1em',
},
});
selectorButton: {
cursor: 'pointer',
position: 'relative',
padding: '6.5px 10px',
color: map.TEXT_COLOR,
'border-style': 'solid',
'border-width': '1px',
'border-left-width': 0,
'&:first-child': {
'border-left-width': '1px',
'border-top-left-radius': '3px',
'border-bottom-left-radius': '3px',
},
'&:last-child': {
'border-top-right-radius': '3px',
'border-bottom-right-radius': '3px',
},
'background-color': map.TAB_BACK_COLOR,
'&:hover': {
'background-color': map.TAB_BACK_HOVER_COLOR,
},
'border-color': map.TAB_BORDER_COLOR,
},
selectorButtonSmall: {
padding: '0px 8px',
'font-size': '0.8em',
},
selectorButtonSelected: {
'background-color': map.TAB_BACK_SELECTED_COLOR,
},
queryForm: {
display: 'flex',
flexFlow: 'column nowrap',
},
sortBySection: {
display: 'flex',
padding: '0.4em',
'& > [role="radiogroup"]': {
flex: '0 0 auto',
padding: '0 0 0 0.4em',
},
'& label': {
display: 'flex',
flex: '0 0 auto',
whiteSpace: 'noWrap',
alignItems: 'center',
paddingRight: '0.4em',
},
},
querySearch: {
maxWidth: '65%',
'background-color': map.BACKGROUND_COLOR,
display: 'flex',
alignItems: 'center',
flexFlow: 'row nowrap',
flex: '1 1 auto',
'& input': {
outline: 'none',
border: 'none',
width: '100%',
flex: '1 1 auto',
padding: '5px 10px',
'font-size': '1em',
position: 'relative',
fontFamily: 'monaco, Consolas, "Lucida Console", monospace',
'background-color': map.BACKGROUND_COLOR,
color: map.TEXT_COLOR,
'&::-webkit-input-placeholder': {
color: map.TEXT_PLACEHOLDER_COLOR,
},
'&::-moz-placeholder': {
color: map.TEXT_PLACEHOLDER_COLOR,
},
'&::-webkit-search-cancel-button': appearanceNone,
},
},
closeButton: {
...appearanceNone,
border: 'none',
outline: 'none',
boxShadow: 'none',
display: 'block',
cursor: 'pointer',
background: 'transparent',
position: 'relative',
fontSize: 'inherit',
'&[data-invisible="1"]': {
visibility: 'hidden !important',
},
'&::after': {
content: '"\u00d7"',
display: 'block',
padding: 4,
width: '16px',
height: '16px',
fontSize: '16px',
color: map.TEXT_COLOR,
background: 'transparent',
},
},
searchSelectLabel: {
display: 'inline-block',
padding: 4,
borderLeft: `1px solid currentColor`,
},
queryPreview: {
flex: '1 1 50%',
overflowX: 'hidden',
oveflowY: 'auto',
display: 'flex',
'flex-direction': 'column',
'overflow-y': 'hidden',
'& pre': {
border: 'inherit',
'border-radius': '3px',
'line-height': 'inherit',
color: 'inherit',
},
'background-color': map.BACKGROUND_COLOR,
},
previewHeader: {
flex: '0 0 30px',
padding: '5px 10px',
'align-items': 'center',
'border-bottom-width': '1px',
'border-bottom-style': 'solid',
'background-color': map.HEADER_BACKGROUND_COLOR,
'border-bottom-color': map.HEADER_BORDER_COLOR,
},
treeItemPin: {
'font-size': '0.7em',
'padding-left': '5px',
cursor: 'pointer',
'&:hover': {
'text-decoration': 'underline',
},
color: map.PIN_COLOR,
},
treeItemKey: {
color: map.TEXT_PLACEHOLDER_COLOR,
},
treeWrapper: {
overflowX: 'auto',
overflowY: 'auto',
padding: '1em',
},
};
};
let themeSheet: StyleSheet;

View File

@ -5,6 +5,7 @@ import { Base16Theme, StylingFunction } from 'react-base16-styling';
import { Action } from 'redux';
import * as themes from 'redux-devtools-themes';
import { QueryComparators } from './utils/comparators';
import { QueryFilters } from './utils/filters';
export enum QueryPreviewTabs {
queryinfo,
@ -17,6 +18,7 @@ export interface QueryFormValues {
queryComparator: QueryComparators;
isAscendingQueryComparatorOrder: boolean;
searchValue: string;
queryFilter: QueryFilters;
}
export interface RtkQueryInspectorMonitorState {
readonly queryForm: {
@ -69,8 +71,6 @@ export interface ExternalProps<S, A extends Action<unknown>> {
invertTheme: boolean;
}
export type AnyExternalProps = ExternalProps<unknown, any>;
export interface QueryInfo {
query: RtkQueryState;
queryKey: string;

View File

@ -0,0 +1,72 @@
import { QueryInfo, SelectOption } from '../types';
export interface FilterList<T> {
(regex: RegExp | null, list: T[]): T[];
}
export enum QueryFilters {
queryKey = 'query key',
reducerPath = 'reducerPath',
endpointName = 'endpoint',
status = 'status',
}
function filterByQueryKey(
regex: RegExp | null,
list: QueryInfo[]
): QueryInfo[] {
if (!regex) {
return list;
}
return list.filter((queryInfo) => regex.test(queryInfo.queryKey));
}
function filterByReducerPath(
regex: RegExp | null,
list: QueryInfo[]
): QueryInfo[] {
if (!regex) {
return list;
}
return list.filter((queryInfo) => regex.test(queryInfo.reducerPath));
}
function filterByEndpointName(
regex: RegExp | null,
list: QueryInfo[]
): QueryInfo[] {
if (!regex) {
return list;
}
return list.filter((queryInfo) =>
regex.test(queryInfo.query.endpointName ?? 'undefined')
);
}
function filterByStatus(regex: RegExp | null, list: QueryInfo[]): QueryInfo[] {
if (!regex) {
return list;
}
return list.filter((queryInfo) => regex.test(queryInfo.query.status));
}
export const filterQueryOptions: SelectOption<QueryFilters>[] = [
{ label: 'query key', value: QueryFilters.queryKey },
{ label: 'reducerPath', value: QueryFilters.reducerPath },
{ label: 'status', value: QueryFilters.status },
{ label: 'endpoint', value: QueryFilters.endpointName },
];
export const queryListFilters: Readonly<Record<
QueryFilters,
FilterList<QueryInfo>
>> = {
[QueryFilters.queryKey]: filterByQueryKey,
[QueryFilters.endpointName]: filterByEndpointName,
[QueryFilters.reducerPath]: filterByReducerPath,
[QueryFilters.status]: filterByStatus,
};

View File

@ -157,7 +157,7 @@ export function getQueryTagsOf(
const provided = apiStates[queryInfo.reducerPath].provided;
const tagTypes = Object.keys(provided);
console.log({ tagTypes, provided });
if (tagTypes.length < 1) {
return emptyArray;
}