refactor(rtk-query): move Inspector & Monitor to containers clean up typings

Other changes:

* chore: improved type coverage

* chore: do not lint packages/redux-devtools-rtk-query-monitor/demo folder

* refactor: put sort order buttons inside a component

* chore: hopefully resolve mockServiceWorker formatting issues
This commit is contained in:
FaberVitale 2021-06-21 15:34:18 +02:00
parent 9f1d718e80
commit e84c0dcd99
19 changed files with 261 additions and 230 deletions

View File

@ -10,4 +10,4 @@ __snapshots__
dev dev
.yarn/* .yarn/*
**/.yarn/* **/.yarn/*
demo/public/** **/demo/public/**

View File

@ -1,5 +1,2 @@
lib lib
demo/public/** demo/
demo/src/mocks/**
demo/src/build/**
demo/build/**

View File

@ -1,9 +1,9 @@
module.exports = { module.exports = {
extends: '../../../.eslintrc', extends: '../../.eslintrc',
overrides: [ overrides: [
{ {
files: ['*.ts', '*.tsx'], files: ['*.ts', '*.tsx'],
extends: '../../../eslintrc.ts.react.base.json', extends: '../../eslintrc.ts.react.base.json',
parserOptions: { parserOptions: {
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: ['./tsconfig.json'], project: ['./tsconfig.json'],

View File

@ -7,116 +7,116 @@
/* eslint-disable */ /* eslint-disable */
/* tslint:disable */ /* tslint:disable */
const INTEGRITY_CHECKSUM = '82ef9b96d8393b6da34527d1d6e19187'; const INTEGRITY_CHECKSUM = '82ef9b96d8393b6da34527d1d6e19187'
const bypassHeaderName = 'x-msw-bypass'; const bypassHeaderName = 'x-msw-bypass'
const activeClientIds = new Set(); const activeClientIds = new Set()
self.addEventListener('install', function () { self.addEventListener('install', function () {
return self.skipWaiting(); return self.skipWaiting()
}); })
self.addEventListener('activate', async function (event) { self.addEventListener('activate', async function (event) {
return self.clients.claim(); return self.clients.claim()
}); })
self.addEventListener('message', async function (event) { self.addEventListener('message', async function (event) {
const clientId = event.source.id; const clientId = event.source.id
if (!clientId || !self.clients) { if (!clientId || !self.clients) {
return; return
} }
const client = await self.clients.get(clientId); const client = await self.clients.get(clientId)
if (!client) { if (!client) {
return; return
} }
const allClients = await self.clients.matchAll(); const allClients = await self.clients.matchAll()
switch (event.data) { switch (event.data) {
case 'KEEPALIVE_REQUEST': { case 'KEEPALIVE_REQUEST': {
sendToClient(client, { sendToClient(client, {
type: 'KEEPALIVE_RESPONSE', type: 'KEEPALIVE_RESPONSE',
}); })
break; break
} }
case 'INTEGRITY_CHECK_REQUEST': { case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, { sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE', type: 'INTEGRITY_CHECK_RESPONSE',
payload: INTEGRITY_CHECKSUM, payload: INTEGRITY_CHECKSUM,
}); })
break; break
} }
case 'MOCK_ACTIVATE': { case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId); activeClientIds.add(clientId)
sendToClient(client, { sendToClient(client, {
type: 'MOCKING_ENABLED', type: 'MOCKING_ENABLED',
payload: true, payload: true,
}); })
break; break
} }
case 'MOCK_DEACTIVATE': { case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId); activeClientIds.delete(clientId)
break; break
} }
case 'CLIENT_CLOSED': { case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId); activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => { const remainingClients = allClients.filter((client) => {
return client.id !== clientId; return client.id !== clientId
}); })
// Unregister itself when there are no more clients // Unregister itself when there are no more clients
if (remainingClients.length === 0) { if (remainingClients.length === 0) {
self.registration.unregister(); self.registration.unregister()
} }
break; break
} }
} }
}); })
// Resolve the "master" client for the given event. // Resolve the "master" client for the given event.
// Client that issues a request doesn't necessarily equal the client // Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should // that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase. // communicate with during the response resolving phase.
async function resolveMasterClient(event) { async function resolveMasterClient(event) {
const client = await self.clients.get(event.clientId); const client = await self.clients.get(event.clientId)
if (client.frameType === 'top-level') { if (client.frameType === 'top-level') {
return client; return client
} }
const allClients = await self.clients.matchAll(); const allClients = await self.clients.matchAll()
return allClients return allClients
.filter((client) => { .filter((client) => {
// Get only those clients that are currently visible. // Get only those clients that are currently visible.
return client.visibilityState === 'visible'; return client.visibilityState === 'visible'
}) })
.find((client) => { .find((client) => {
// Find the client ID that's recorded in the // Find the client ID that's recorded in the
// set of clients that have registered the worker. // set of clients that have registered the worker.
return activeClientIds.has(client.id); return activeClientIds.has(client.id)
}); })
} }
async function handleRequest(event, requestId) { async function handleRequest(event, requestId) {
const client = await resolveMasterClient(event); const client = await resolveMasterClient(event)
const response = await getResponse(event, client, requestId); const response = await getResponse(event, client, requestId)
// Send back the response clone for the "response:*" life-cycle events. // Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise // Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely. // this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) { if (client && activeClientIds.has(client.id)) {
(async function () { ;(async function () {
const clonedResponse = response.clone(); const clonedResponse = response.clone()
sendToClient(client, { sendToClient(client, {
type: 'RESPONSE', type: 'RESPONSE',
payload: { payload: {
@ -130,21 +130,21 @@ async function handleRequest(event, requestId) {
headers: serializeHeaders(clonedResponse.headers), headers: serializeHeaders(clonedResponse.headers),
redirected: clonedResponse.redirected, redirected: clonedResponse.redirected,
}, },
}); })
})(); })()
} }
return response; return response
} }
async function getResponse(event, client, requestId) { async function getResponse(event, client, requestId) {
const { request } = event; const { request } = event
const requestClone = request.clone(); const requestClone = request.clone()
const getOriginalResponse = () => fetch(requestClone); const getOriginalResponse = () => fetch(requestClone)
// Bypass mocking when the request client is not active. // Bypass mocking when the request client is not active.
if (!client) { if (!client) {
return getOriginalResponse(); return getOriginalResponse()
} }
// Bypass initial page load requests (i.e. static assets). // Bypass initial page load requests (i.e. static assets).
@ -152,26 +152,26 @@ async function getResponse(event, client, requestId) {
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests. // and is not ready to handle requests.
if (!activeClientIds.has(client.id)) { if (!activeClientIds.has(client.id)) {
return await getOriginalResponse(); return await getOriginalResponse()
} }
// Bypass requests with the explicit bypass header // Bypass requests with the explicit bypass header
if (requestClone.headers.get(bypassHeaderName) === 'true') { if (requestClone.headers.get(bypassHeaderName) === 'true') {
const cleanRequestHeaders = serializeHeaders(requestClone.headers); const cleanRequestHeaders = serializeHeaders(requestClone.headers)
// Remove the bypass header to comply with the CORS preflight check. // Remove the bypass header to comply with the CORS preflight check.
delete cleanRequestHeaders[bypassHeaderName]; delete cleanRequestHeaders[bypassHeaderName]
const originalRequest = new Request(requestClone, { const originalRequest = new Request(requestClone, {
headers: new Headers(cleanRequestHeaders), headers: new Headers(cleanRequestHeaders),
}); })
return fetch(originalRequest); return fetch(originalRequest)
} }
// Send the request to the client-side MSW. // Send the request to the client-side MSW.
const reqHeaders = serializeHeaders(request.headers); const reqHeaders = serializeHeaders(request.headers)
const body = await request.text(); const body = await request.text()
const clientMessage = await sendToClient(client, { const clientMessage = await sendToClient(client, {
type: 'REQUEST', type: 'REQUEST',
@ -192,31 +192,31 @@ async function getResponse(event, client, requestId) {
bodyUsed: request.bodyUsed, bodyUsed: request.bodyUsed,
keepalive: request.keepalive, keepalive: request.keepalive,
}, },
}); })
switch (clientMessage.type) { switch (clientMessage.type) {
case 'MOCK_SUCCESS': { case 'MOCK_SUCCESS': {
return delayPromise( return delayPromise(
() => respondWithMock(clientMessage), () => respondWithMock(clientMessage),
clientMessage.payload.delay clientMessage.payload.delay,
); )
} }
case 'MOCK_NOT_FOUND': { case 'MOCK_NOT_FOUND': {
return getOriginalResponse(); return getOriginalResponse()
} }
case 'NETWORK_ERROR': { case 'NETWORK_ERROR': {
const { name, message } = clientMessage.payload; const { name, message } = clientMessage.payload
const networkError = new Error(message); const networkError = new Error(message)
networkError.name = name; networkError.name = name
// Rejecting a request Promise emulates a network error. // Rejecting a request Promise emulates a network error.
throw networkError; throw networkError
} }
case 'INTERNAL_ERROR': { case 'INTERNAL_ERROR': {
const parsedBody = JSON.parse(clientMessage.payload.body); const parsedBody = JSON.parse(clientMessage.payload.body)
console.error( console.error(
`\ `\
@ -229,38 +229,38 @@ This exception has been gracefully handled as a 500 response, however, it's stro
If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\ If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
`, `,
request.method, request.method,
request.url request.url,
); )
return respondWithMock(clientMessage); return respondWithMock(clientMessage)
} }
} }
return getOriginalResponse(); return getOriginalResponse()
} }
self.addEventListener('fetch', function (event) { self.addEventListener('fetch', function (event) {
const { request } = event; const { request } = event
// Bypass navigation requests. // Bypass navigation requests.
if (request.mode === 'navigate') { if (request.mode === 'navigate') {
return; return
} }
// Opening the DevTools triggers the "only-if-cached" request // Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests. // that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return; return
} }
// Bypass all requests when there are no active clients. // Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests // Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload). // after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) { if (activeClientIds.size === 0) {
return; return
} }
const requestId = uuidv4(); const requestId = uuidv4()
return event.respondWith( return event.respondWith(
handleRequest(event, requestId).catch((error) => { handleRequest(event, requestId).catch((error) => {
@ -268,55 +268,55 @@ self.addEventListener('fetch', function (event) {
'[MSW] Failed to mock a "%s" request to "%s": %s', '[MSW] Failed to mock a "%s" request to "%s": %s',
request.method, request.method,
request.url, request.url,
error error,
); )
}),
)
}) })
);
});
function serializeHeaders(headers) { function serializeHeaders(headers) {
const reqHeaders = {}; const reqHeaders = {}
headers.forEach((value, name) => { headers.forEach((value, name) => {
reqHeaders[name] = reqHeaders[name] reqHeaders[name] = reqHeaders[name]
? [].concat(reqHeaders[name]).concat(value) ? [].concat(reqHeaders[name]).concat(value)
: value; : value
}); })
return reqHeaders; return reqHeaders
} }
function sendToClient(client, message) { function sendToClient(client, message) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const channel = new MessageChannel(); const channel = new MessageChannel()
channel.port1.onmessage = (event) => { channel.port1.onmessage = (event) => {
if (event.data && event.data.error) { if (event.data && event.data.error) {
return reject(event.data.error); return reject(event.data.error)
} }
resolve(event.data); resolve(event.data)
}; }
client.postMessage(JSON.stringify(message), [channel.port2]); client.postMessage(JSON.stringify(message), [channel.port2])
}); })
} }
function delayPromise(cb, duration) { function delayPromise(cb, duration) {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => resolve(cb()), duration); setTimeout(() => resolve(cb()), duration)
}); })
} }
function respondWithMock(clientMessage) { function respondWithMock(clientMessage) {
return new Response(clientMessage.payload.body, { return new Response(clientMessage.payload.body, {
...clientMessage.payload, ...clientMessage.payload,
headers: clientMessage.payload.headers, headers: clientMessage.payload.headers,
}); })
} }
function uuidv4() { function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0; const r = (Math.random() * 16) | 0
const v = c == 'x' ? r : (r & 0x3) | 0x8; const v = c == 'x' ? r : (r & 0x3) | 0x8
return v.toString(16); return v.toString(16)
}); })
} }

View File

@ -73,7 +73,7 @@ const PostJsonDetail = ({ id }: { id: string }) => {
export const PostDetail = () => { export const PostDetail = () => {
const { id } = useParams<{ id: any }>(); const { id } = useParams<{ id: any }>();
const { push } = useHistory(); const history = useHistory();
const toast = useToast(); const toast = useToast();
@ -137,7 +137,9 @@ export const PostDetail = () => {
{isUpdating ? 'Updating...' : 'Edit'} {isUpdating ? 'Updating...' : 'Edit'}
</Button> </Button>
<Button <Button
onClick={() => deletePost(id).then(() => push('/posts'))} onClick={() =>
deletePost(id).then(() => history.push('/posts'))
}
disabled={isDeleting} disabled={isDeleting}
colorScheme="red" colorScheme="red"
> >

View File

@ -82,7 +82,7 @@ const AddPost = () => {
const PostList = () => { const PostList = () => {
const { data: posts, isLoading } = useGetPostsQuery(); const { data: posts, isLoading } = useGetPostsQuery();
const { push } = useHistory(); const history = useHistory();
if (isLoading) { if (isLoading) {
return <div>Loading</div>; return <div>Loading</div>;
@ -95,7 +95,7 @@ const PostList = () => {
return ( return (
<List spacing={3}> <List spacing={3}>
{posts.map(({ id, name }) => ( {posts.map(({ id, name }) => (
<ListItem key={id} onClick={() => push(`/posts/${id}`)}> <ListItem key={id} onClick={() => history.push(`/posts/${id}`)}>
<ListIcon as={MdBook} color="green.500" /> {name} <ListIcon as={MdBook} color="green.500" /> {name}
</ListItem> </ListItem>
))} ))}

View File

@ -1,13 +1,14 @@
import React from 'react'; import React from 'react';
import { ReactNode } from 'react';
import { StyleUtilsContext } from '../styles/createStylingFromTheme'; import { StyleUtilsContext } from '../styles/createStylingFromTheme';
export function NoRtkQueryApi(): ReactNode { export function NoRtkQueryApi(): JSX.Element {
return ( return (
<StyleUtilsContext.Consumer> <StyleUtilsContext.Consumer>
{({ styling }) => ( {({ styling }) => (
<div {...styling('noApiFound')}> <div {...styling('noApiFound')}>
No rtk-query api found.<br/>Make sure to follow{' '} No rtk-query api found.
<br />
Make sure to follow{' '}
<a <a
href="https://redux-toolkit.js.org/rtk-query/overview#basic-usage" href="https://redux-toolkit.js.org/rtk-query/overview#basic-usage"
target="_blank" target="_blank"

View File

@ -6,6 +6,7 @@ import { SelectOption } from '../types';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import { sortQueryOptions, QueryComparators } from '../utils/comparators'; import { sortQueryOptions, QueryComparators } from '../utils/comparators';
import { QueryFilters, filterQueryOptions } from '../utils/filters'; import { QueryFilters, filterQueryOptions } from '../utils/filters';
import { SortOrderButtons } from './SortOrderButtons';
export interface QueryFormProps { export interface QueryFormProps {
values: QueryFormValues; values: QueryFormValues;
@ -16,8 +17,6 @@ interface QueryFormState {
searchValue: string; searchValue: string;
} }
const ascId = 'rtk-query-rb-asc';
const descId = 'rtk-query-rb-desc';
const selectId = 'rtk-query-comp-select'; const selectId = 'rtk-query-comp-select';
const searchId = 'rtk-query-search-query'; const searchId = 'rtk-query-search-query';
const filterSelectId = 'rtk-query-search-query-select'; const filterSelectId = 'rtk-query-search-query-select';
@ -41,19 +40,8 @@ export class QueryForm extends React.PureComponent<
evt.preventDefault(); evt.preventDefault();
}; };
handleButtonGroupClick = ({ target }: MouseEvent<HTMLElement>): void => { handleButtonGroupClick = (isAsc: boolean): void => {
const { this.props.onFormValuesChange({ isAscendingQueryComparatorOrder: isAsc });
values: { isAscendingQueryComparatorOrder: isAsc },
onFormValuesChange,
} = this.props;
const targetId = (target as HTMLElement)?.id ?? null;
if (targetId === ascId && !isAsc) {
onFormValuesChange({ isAscendingQueryComparatorOrder: true });
} else if (targetId === descId && isAsc) {
onFormValuesChange({ isAscendingQueryComparatorOrder: false });
}
}; };
handleSelectComparatorChange = ( handleSelectComparatorChange = (
@ -111,8 +99,6 @@ export class QueryForm extends React.PureComponent<
}, },
} = this.props; } = this.props;
const isDesc = !isAsc;
return ( return (
<StyleUtilsContext.Consumer> <StyleUtilsContext.Consumer>
{({ styling, base16Theme }) => { {({ styling, base16Theme }) => {
@ -170,37 +156,10 @@ export class QueryForm extends React.PureComponent<
options={sortQueryOptions} options={sortQueryOptions}
onChange={this.handleSelectComparatorChange} onChange={this.handleSelectComparatorChange}
/> />
<div <SortOrderButtons
tabIndex={0} isAsc={isAsc}
role="radiogroup" onChange={this.handleButtonGroupClick}
aria-activedescendant={isAsc ? ascId : descId} />
onClick={this.handleButtonGroupClick}
>
<button
role="radio"
type="button"
id={ascId}
aria-checked={isAsc}
{...styling(
['selectorButton', isAsc && 'selectorButtonSelected'],
isAsc
)}
>
asc
</button>
<button
id={descId}
role="radio"
type="button"
aria-checked={isDesc}
{...styling(
['selectorButton', isDesc && 'selectorButtonSelected'],
isDesc
)}
>
desc
</button>
</div>
</div> </div>
</form> </form>
); );

View File

@ -0,0 +1,67 @@
import React, { MouseEvent } from 'react';
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
export const ascId = 'rtk-query-rb-asc';
export const descId = 'rtk-query-rb-desc';
export interface SortOrderButtonsProps {
readonly isAsc?: boolean;
readonly onChange: (isAsc: boolean) => void;
}
export function SortOrderButtons({
isAsc,
onChange,
}: SortOrderButtonsProps): JSX.Element {
const handleButtonGroupClick = ({
target,
}: MouseEvent<HTMLElement>): void => {
const targetId = (target as HTMLElement)?.id ?? null;
if (targetId === ascId && !isAsc) {
onChange(true);
} else if (targetId === descId && isAsc) {
onChange(false);
}
};
const isDesc = !isAsc;
return (
<StyleUtilsContext.Consumer>
{({ styling }) => (
<div
tabIndex={0}
role="radiogroup"
aria-activedescendant={isAsc ? ascId : descId}
onClick={handleButtonGroupClick}
>
<button
role="radio"
type="button"
id={ascId}
aria-checked={isAsc}
{...styling(
['selectorButton', isAsc && 'selectorButtonSelected'],
isAsc
)}
>
asc
</button>
<button
id={descId}
role="radio"
type="button"
aria-checked={isDesc}
{...styling(
['selectorButton', isDesc && 'selectorButtonSelected'],
isDesc
)}
>
desc
</button>
</div>
)}
</StyleUtilsContext.Consumer>
);
}

View File

@ -1,37 +1,33 @@
import React, { Component, createRef, ReactNode } from 'react'; import React, { PureComponent, createRef, ReactNode } from 'react';
import { AnyAction, Dispatch, Action } from 'redux'; import type { AnyAction, Dispatch, Action } from '@reduxjs/toolkit';
import { LiftedAction, LiftedState } from '@redux-devtools/core'; import type { LiftedAction, LiftedState } from '@redux-devtools/core';
import * as themes from 'redux-devtools-themes';
import { Base16Theme } from 'react-base16-styling';
import { import {
QueryFormValues, QueryFormValues,
QueryInfo, QueryInfo,
QueryPreviewTabs, QueryPreviewTabs,
RtkQueryMonitorState, RtkQueryMonitorState,
StyleUtils, StyleUtils,
} from './types'; SelectorsSource,
import { createInspectorSelectors, computeSelectorSource } from './selectors'; } from '../types';
import { createInspectorSelectors, computeSelectorSource } from '../selectors';
import { import {
changeQueryFormValues, changeQueryFormValues,
selectedPreviewTab, selectedPreviewTab,
selectQueryKey, selectQueryKey,
} from './reducers'; } from '../reducers';
import { QueryList } from './components/QueryList'; import { QueryList } from '../components/QueryList';
import { QueryForm } from './components/QueryForm'; import { QueryForm } from '../components/QueryForm';
import { QueryPreview } from './components/QueryPreview'; import { QueryPreview } from '../components/QueryPreview';
import { getApiStateOf, getQuerySubscriptionsOf } from './utils/rtk-query'; import { getApiStateOf, getQuerySubscriptionsOf } from '../utils/rtk-query';
type SelectorsSource<S> = { type ForwardedMonitorProps<S, A extends Action<unknown>> = Pick<
userState: S | null; LiftedState<S, A, RtkQueryMonitorState>,
monitorState: RtkQueryMonitorState; 'monitorState' | 'currentStateIndex' | 'computedStates'
}; >;
export interface RtkQueryInspectorProps<S, A extends Action<unknown>> export interface RtkQueryInspectorProps<S, A extends Action<unknown>>
extends LiftedState<S, A, RtkQueryMonitorState> { extends ForwardedMonitorProps<S, A> {
dispatch: Dispatch<LiftedAction<S, A, RtkQueryMonitorState>>; dispatch: Dispatch<LiftedAction<S, A, RtkQueryMonitorState>>;
theme: keyof typeof themes | Base16Theme;
invertTheme: boolean;
state: S | null;
styleUtils: StyleUtils; styleUtils: StyleUtils;
} }
@ -40,13 +36,13 @@ type RtkQueryInspectorState<S> = {
isWideLayout: boolean; isWideLayout: boolean;
}; };
class RtkQueryInspector<S, A extends Action<unknown>> extends Component< class RtkQueryInspector<S, A extends Action<unknown>> extends PureComponent<
RtkQueryInspectorProps<S, A>, RtkQueryInspectorProps<S, A>,
RtkQueryInspectorState<S> RtkQueryInspectorState<S>
> { > {
inspectorRef = createRef<HTMLDivElement>(); inspectorRef = createRef<HTMLDivElement>();
isWideIntervalRef: number | NodeJS.Timeout | null = null; isWideIntervalRef: ReturnType<typeof setInterval> | null = null;
constructor(props: RtkQueryInspectorProps<S, A>) { constructor(props: RtkQueryInspectorProps<S, A>) {
super(props); super(props);
@ -98,7 +94,7 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
componentWillUnmount(): void { componentWillUnmount(): void {
if (this.isWideIntervalRef) { if (this.isWideIntervalRef) {
clearTimeout(this.isWideIntervalRef as any); clearTimeout(this.isWideIntervalRef);
} }
} }

View File

@ -1,18 +1,18 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Action } from 'redux'; import { Action, AnyAction } from 'redux';
import RtkQueryInspector from './RtkQueryInspector'; import RtkQueryInspector from './RtkQueryInspector';
import { reducer } from './reducers'; import { reducer } from '../reducers';
import { import {
ExternalProps, ExternalProps,
RtkQueryMonitorProps, RtkQueryMonitorProps,
RtkQueryMonitorState, RtkQueryMonitorState,
StyleUtils, StyleUtils,
} from './types'; } from '../types';
import { import {
createThemeState, createThemeState,
StyleUtilsContext, StyleUtilsContext,
} from './styles/createStylingFromTheme'; } from '../styles/createStylingFromTheme';
interface DefaultProps { interface DefaultProps {
theme: string; theme: string;
@ -41,7 +41,7 @@ class RtkQueryMonitor<S, A extends Action<unknown>> extends Component<
invertTheme: PropTypes.bool, invertTheme: PropTypes.bool,
}; };
static defaultProps = { static defaultProps: DefaultProps = {
theme: 'nicinabox', theme: 'nicinabox',
invertTheme: false, invertTheme: false,
}; };
@ -55,17 +55,16 @@ class RtkQueryMonitor<S, A extends Action<unknown>> extends Component<
} }
render() { render() {
const { const { currentStateIndex, computedStates, monitorState, dispatch } =
styleUtils: { base16Theme }, this.props;
} = this.state;
const RtkQueryInspectorAsAny = RtkQueryInspector as any;
return ( return (
<StyleUtilsContext.Provider value={this.state.styleUtils}> <StyleUtilsContext.Provider value={this.state.styleUtils}>
<RtkQueryInspectorAsAny <RtkQueryInspector<S, AnyAction>
{...this.props} computedStates={computedStates}
theme={base16Theme} currentStateIndex={currentStateIndex}
monitorState={monitorState}
dispatch={dispatch}
styleUtils={this.state.styleUtils} styleUtils={this.state.styleUtils}
/> />
</StyleUtilsContext.Provider> </StyleUtilsContext.Provider>

View File

@ -1,2 +1,2 @@
export { default } from './RtkQueryMonitor'; export { default } from './containers/RtkQueryMonitor';
export { ExternalProps } from './types'; export { ExternalProps } from './types';

View File

@ -1,10 +1,10 @@
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 { import {
QueryInfo, QueryInfo,
RtkQueryMonitorState, RtkQueryMonitorState,
QueryFormValues, QueryFormValues,
RtkQueryMonitorProps,
QueryPreviewTabs, QueryPreviewTabs,
} from './types'; } from './types';
import { QueryComparators } from './utils/comparators'; import { QueryComparators } from './utils/comparators';
@ -53,7 +53,7 @@ const monitorSlice = createSlice({
}); });
export function reducer<S, A extends Action<unknown>>( export function reducer<S, A extends Action<unknown>>(
props: RtkQueryInspectorProps<S, A>, props: RtkQueryMonitorProps<S, A>,
state: RtkQueryMonitorState | undefined, state: RtkQueryMonitorState | undefined,
action: AnyAction action: AnyAction
): RtkQueryMonitorState { ): RtkQueryMonitorState {

View File

@ -1,5 +1,5 @@
import { Action, createSelector, Selector } from '@reduxjs/toolkit'; import { Action, createSelector, Selector } from '@reduxjs/toolkit';
import { RtkQueryInspectorProps } from './RtkQueryInspector'; import { RtkQueryInspectorProps } from './containers/RtkQueryInspector';
import { ApiStats, QueryInfo, RtkQueryTag, SelectorsSource } from './types'; import { ApiStats, QueryInfo, RtkQueryTag, SelectorsSource } 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';

View File

@ -14,7 +14,8 @@ import { createContext } from 'react';
jss.setup(preset()); jss.setup(preset());
export const colorMap = (theme: reduxThemes.Base16Theme) => ({ export const colorMap = (theme: reduxThemes.Base16Theme) =>
({
TEXT_COLOR: theme.base06, TEXT_COLOR: theme.base06,
TEXT_PLACEHOLDER_COLOR: rgba(theme.base06, 60), TEXT_PLACEHOLDER_COLOR: rgba(theme.base06, 60),
BACKGROUND_COLOR: theme.base00, BACKGROUND_COLOR: theme.base00,
@ -38,7 +39,7 @@ export const colorMap = (theme: reduxThemes.Base16Theme) => ({
LINK_COLOR: rgba(theme.base0E, 90), LINK_COLOR: rgba(theme.base0E, 90),
LINK_HOVER_COLOR: theme.base0E, LINK_HOVER_COLOR: theme.base0E,
ERROR_COLOR: theme.base08, ERROR_COLOR: theme.base08,
}); } as const);
type Color = keyof ReturnType<typeof colorMap>; type Color = keyof ReturnType<typeof colorMap>;
type ColorMap = { type ColorMap = {

View File

@ -5,11 +5,11 @@ import isIterable from '../utils/isIterable';
const IS_IMMUTABLE_KEY = '@@__IS_IMMUTABLE__@@'; const IS_IMMUTABLE_KEY = '@@__IS_IMMUTABLE__@@';
function isImmutable(value: any) { function isImmutable(value: unknown) {
return isKeyed(value) || isIndexed(value) || isCollection(value); return isKeyed(value) || isIndexed(value) || isCollection(value);
} }
function getShortTypeString(val: any, diff: boolean | undefined) { function getShortTypeString(val: unknown, diff: boolean | undefined) {
if (diff && Array.isArray(val)) { if (diff && Array.isArray(val)) {
val = val[val.length === 2 ? 1 : 0]; val = val[val.length === 2 ? 1 : 0];
} }
@ -23,7 +23,9 @@ function getShortTypeString(val: any, diff: boolean | undefined) {
} else if (val === undefined) { } else if (val === undefined) {
return 'undef'; return 'undef';
} else if (typeof val === 'object') { } else if (typeof val === 'object') {
return Object.keys(val).length > 0 ? '{…}' : '{}'; return Object.keys(val as Record<string, unknown>).length > 0
? '{…}'
: '{}';
} else if (typeof val === 'function') { } else if (typeof val === 'function') {
return 'fn'; return 'fn';
} else if (typeof val === 'string') { } else if (typeof val === 'string') {

View File

@ -1,11 +1,11 @@
import { LiftedAction, LiftedState } from '@redux-devtools/instrument'; import type { LiftedAction, LiftedState } from '@redux-devtools/instrument';
import type { createApi, QueryStatus } from '@reduxjs/toolkit/query'; import type { createApi, QueryStatus } from '@reduxjs/toolkit/query';
import { ComponentType, Dispatch } from 'react'; import type { Action, Dispatch } from '@reduxjs/toolkit';
import { Base16Theme, StylingFunction } from 'react-base16-styling'; import type { ComponentType } from 'react';
import { Action } from 'redux'; import type { Base16Theme, StylingFunction } from 'react-base16-styling';
import * as themes from 'redux-devtools-themes'; import type * as themes from 'redux-devtools-themes';
import { QueryComparators } from './utils/comparators'; import type { QueryComparators } from './utils/comparators';
import { QueryFilters } from './utils/filters'; import type { QueryFilters } from './utils/filters';
export enum QueryPreviewTabs { export enum QueryPreviewTabs {
queryinfo, queryinfo,

View File

@ -3,6 +3,8 @@ export default function isIterable(obj: unknown): boolean {
obj !== null && obj !== null &&
typeof obj === 'object' && typeof obj === 'object' &&
!Array.isArray(obj) && !Array.isArray(obj) &&
typeof (obj as any)[window.Symbol.iterator] === 'function' typeof (obj as Record<string | typeof Symbol.iterator, unknown>)[
window.Symbol.iterator
] === 'function'
); );
} }

View File

@ -25,6 +25,11 @@ const rtkqueryApiStateKeys: ReadonlyArray<keyof RtkQueryApiState> = [
'subscriptions', 'subscriptions',
]; ];
/**
* Type guard used to select apis from the user store state.
* @param val
* @returns {boolean}
*/
export function isApiSlice(val: unknown): val is RtkQueryApiState { export function isApiSlice(val: unknown): val is RtkQueryApiState {
if (!isPlainObject(val)) { if (!isPlainObject(val)) {
return false; return false;
@ -282,7 +287,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.includes(queryInfo.queryKey as any)) { if ((queryKeys as unknown[]).includes(queryInfo.queryKey)) {
const tag: RtkQueryTag = { type }; const tag: RtkQueryTag = { type };
if (id !== missingTagId) { if (id !== missingTagId) {