├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── main.png ├── package.json ├── rollup.config.js ├── src ├── Explorer.tsx ├── Logo.tsx ├── context │ ├── index.tsx │ └── reducer.ts ├── devtools.tsx ├── index.tsx ├── middleware.ts ├── styledComponents.ts ├── theme.tsx ├── useLocalStorage.ts ├── useMediaQuery.ts └── utils.ts ├── tsconfig.json ├── tsconfig.types.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules 3 | types -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 8, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "impliedStrict": true, 8 | "experimentalObjectRestSpread": true 9 | }, 10 | "allowImportExportEverywhere": true 11 | }, 12 | "overrides": [ 13 | { 14 | "files": ["*.ts", "*.tsx"], 15 | "parserOptions": { 16 | "project": ["./tsconfig.json"] 17 | } 18 | } 19 | ], 20 | "plugins": ["@typescript-eslint", "unused-imports", "react-hooks"], 21 | "extends": [ 22 | "eslint:recommended", 23 | "plugin:react/recommended", 24 | "plugin:@typescript-eslint/recommended", 25 | "prettier" 26 | ], 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | } 31 | }, 32 | "env": { 33 | "es6": true, 34 | "browser": true, 35 | "node": true, 36 | "jest": true 37 | }, 38 | "rules": { 39 | "func-names": [2, "as-needed"], 40 | "no-shadow": 0, 41 | "no-unused-vars": 0, 42 | "unused-imports/no-unused-imports": "error", 43 | "unused-imports/no-unused-vars": [ 44 | "warn", 45 | { 46 | "vars": "all", 47 | "varsIgnorePattern": "^_", 48 | "args": "after-used", 49 | "argsIgnorePattern": "^_" 50 | } 51 | ], 52 | "@typescript-eslint/no-shadow": 2, 53 | "@typescript-eslint/explicit-function-return-type": 0, 54 | "@typescript-eslint/no-use-before-define": 0, 55 | "@typescript-eslint/ban-ts-ignore": 0, 56 | "@typescript-eslint/no-empty-function": 0, 57 | "@typescript-eslint/ban-ts-comment": 0, 58 | "@typescript-eslint/no-var-requires": 0, 59 | "@typescript-eslint/no-explicit-any": 0, 60 | "@typescript-eslint/explicit-module-boundary-types": 0, 61 | "@typescript-eslint/ban-types": 0, 62 | "react-hooks/rules-of-hooks": 2, 63 | "react-hooks/exhaustive-deps": 1, 64 | "react/prop-types": 0, 65 | "react/react-in-jsx-scope": 0, 66 | "react/display-name": 0 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | types 4 | *.log 5 | *.tgz 6 | .env 7 | .next 8 | .idea 9 | .vscode 10 | .eslintcache 11 | package-lock.json 12 | tsconfig.tsbuildinfo 13 | **/*.yalc 14 | yalc.lock 15 | size-plugin.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintcache 2 | node_modules 3 | *.log 4 | *.tgz 5 | .env 6 | .vscode 7 | .eslintcache 8 | package-lock.json 9 | yarn.lock 10 | tsconfig.tsbuildinfo 11 | **/*.yalc 12 | yalc.lock 13 | src 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "trailingComma": "none", 6 | "tabWidth": 2, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for showing interest to contribute 2 | 3 | # Setup the Project 4 | 5 | 1. Fork the repo (click the Fork button at the top right of 6 | [this page](https://github.com/rendinjast/swr-devtools)) 7 | 8 | 2. Clone your fork locally 9 | 10 | ```sh 11 | git clone https://github.com//swr-devtools.git 12 | cd swr-devtools 13 | ``` 14 | 15 | 3. Install dependencies by running `yarn`. 16 | 17 | ### Commands 18 | 19 | **`yarn`**: setup project 20 | 21 | **`yarn build:dev`**: build the project for development then yarn link 22 | 23 | **`yarn build`**: run build production. 24 | 25 | **`yarn test`**: run test for all component packages. 26 | 27 | **`yarn format:test`**:prettier check 28 | 29 | **`yarn format`**:prettier write 30 | 31 | **`yarn lint`**:eslint check files 32 | 33 | **`yarn lint:fix`**:eslint write files(fix) 34 | 35 | ## Found a bug? 36 | 37 | Please conform to the issue template and provide a clear path to reproduction 38 | with a code example. The best way to show a bug is by sending a CodeSandbox 39 | link. 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) ERFAN KHADIVAR 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 4 | associated documentation files (the "Software"), to deal in the Software without restriction, 5 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 6 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 13 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES 15 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

react-query devtools for swr

2 | 3 | ![App screenshot](https://raw.githubusercontent.com/rendinjast/swr-devtools/master/main.png) 4 | 5 | > under development 6 | 7 | ## Live Demo 8 | 9 | [![swr-devtools](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/goofy-microservice-ehhkn) 10 | 11 | ## Installation 12 | 13 | ```bash 14 | yarn add @rendinjast/swr-devtools 15 | # or 16 | npm i @rendinjast/swr-devtools 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```jsx 22 | import SWRDevtools from '@rendinjast/swr-devtools' 23 | 24 | export default function App({ Component, pageProps }) { 25 | return ( 26 | 27 | 28 | 29 | ) 30 | } 31 | ``` 32 | 33 | ## Contributing 34 | 35 | See [CONTRIBUTING.md](./CONTRIBUTING.md) 36 | 37 | ## License 38 | 39 | [MIT](./LICENSE) 40 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const production = process.env.NODE_ENV === 'production' 2 | 3 | module.exports = { 4 | presets: [ 5 | ['@babel/preset-env'], 6 | [ 7 | '@babel/preset-react', 8 | { 9 | runtime: production ? 'automatic' : 'classic' 10 | } 11 | ], 12 | '@babel/preset-typescript' 13 | ], 14 | plugins: ['@babel/plugin-transform-runtime'] 15 | } 16 | -------------------------------------------------------------------------------- /main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendinjast/swr-devtools/2fb092f96c0f8bd5b0cb6006203e6d6c38c84232/main.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rendinjast/swr-devtools", 3 | "description": "react-query devtools for SWR", 4 | "type": "commonjs", 5 | "version": "0.0.4", 6 | "main": "dist/index.cjs.js", 7 | "module": "dist/index.esm.js", 8 | "types": "types/index.d.ts", 9 | "sideEffects": false, 10 | "author": "Erfan Khadivar ", 11 | "repository": "rendinjast/swr-devtools", 12 | "bugs": "https://github.com/rendinjast/swr-devtools/issues", 13 | "homepage": "https://github.com/rendinjast/swr-devtools#readme", 14 | "publishConfig": { 15 | "access": "public" 16 | }, 17 | "license": "MIT", 18 | "scripts": { 19 | "build:dev": "cross-env NODE_ENV=development rollup -c rollup.config.js", 20 | "build:js": "cross-env NODE_ENV=production rollup -c rollup.config.js", 21 | "build:types": "rimraf ./types && tsc --project ./tsconfig.types.json && replace 'import type' 'import' ./types -r --silent && replace 'export type' 'export' ./types -r --silent", 22 | "build": "yarn build:types && yarn build:js", 23 | "format:test": "prettier --check \"{src,src/**}/*.{md,js,jsx,ts,tsx,json}\"", 24 | "format": "prettier --write \"{src,src/**}/*.{md,js,jsx,ts,tsx,json}\"", 25 | "lint": "eslint . ./src/**/*.ts --cache", 26 | "lint:fix": "eslint . ./src/**/*.ts --fix" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.16.7", 30 | "@babel/plugin-transform-runtime": "^7.16.7", 31 | "@babel/preset-env": "^7.16.7", 32 | "@babel/preset-react": "^7.16.7", 33 | "@babel/preset-typescript": "^7.16.7", 34 | "@rollup/plugin-babel": "^5.3.0", 35 | "@rollup/plugin-commonjs": "^21.0.1", 36 | "@rollup/plugin-node-resolve": "^13.1.2", 37 | "@types/node": "^17.0.7", 38 | "@types/react": "^17.0.38", 39 | "@typescript-eslint/eslint-plugin": "^5.9.0", 40 | "@typescript-eslint/parser": "^5.9.0", 41 | "bunchee": "^1.8.0", 42 | "cross-env": "^7.0.3", 43 | "eslint": "^8.6.0", 44 | "eslint-config-prettier": "^8.3.0", 45 | "eslint-plugin-react": "^7.28.0", 46 | "eslint-plugin-react-hooks": "^4.3.0", 47 | "eslint-plugin-unused-imports": "^2.0.0", 48 | "prettier": "^2.5.1", 49 | "react": "^17.0.2", 50 | "replace": "^1.2.1", 51 | "rimraf": "^3.0.2", 52 | "rollup": "^2.63.0", 53 | "rollup-plugin-delete": "^2.0.0", 54 | "rollup-plugin-peer-deps-external": "^2.2.4", 55 | "rollup-plugin-size": "^0.2.2", 56 | "swr": "^1.1.2", 57 | "typescript": "^4.5.4" 58 | }, 59 | "peerDependencies": { 60 | "react": "^16.11.0 || ^17.0.0 || ^18.0.0", 61 | "swr": ">=0.0.1" 62 | }, 63 | "dependencies": { 64 | "@babel/runtime": "^7.16.7", 65 | "match-sorter": "^6.3.1" 66 | }, 67 | "keywords": [ 68 | "swr", 69 | "devtools", 70 | "swr-devtools" 71 | ], 72 | "files": [ 73 | "dist", 74 | "types" 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel' 2 | import { nodeResolve } from '@rollup/plugin-node-resolve' 3 | import commonJS from '@rollup/plugin-commonjs' 4 | import size from 'rollup-plugin-size' 5 | import externalDeps from 'rollup-plugin-peer-deps-external' 6 | import del from 'rollup-plugin-delete' 7 | import pkg from './package.json' 8 | 9 | const extensions = ['.js', '.jsx', '.es6', '.es', '.mjs', '.ts', '.tsx'] 10 | 11 | const globals = { 12 | react: 'React', 13 | 'react-dom': 'ReactDOM' 14 | } 15 | 16 | const input = 'src/index.tsx' 17 | export default { 18 | input, 19 | output: [ 20 | { file: pkg.main, format: 'cjs', exports: 'auto', sourcemap: true }, 21 | { file: pkg.module, format: 'es', exports: 'auto', sourcemap: true } 22 | ], 23 | external: Object.keys(globals), 24 | plugins: [ 25 | del({ targets: 'dist/*' }), 26 | externalDeps(), 27 | nodeResolve({ extensions }), 28 | commonJS(), 29 | babel({ 30 | babelHelpers: 'runtime', 31 | exclude: '**/node_modules/**', 32 | extensions 33 | }), 34 | size() 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/Explorer.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import React from 'react' 4 | 5 | import { styled } from './utils' 6 | 7 | export const Entry = styled('div', { 8 | fontFamily: 'Menlo, monospace', 9 | fontSize: '1em', 10 | lineHeight: '1.7', 11 | outline: 'none', 12 | wordBreak: 'break-word' 13 | }) 14 | 15 | export const Label = styled('span', { 16 | cursor: 'pointer', 17 | color: 'white' 18 | }) 19 | 20 | export const Value = styled('span', (props, theme) => ({ 21 | color: theme.danger 22 | })) 23 | 24 | export const SubEntries = styled('div', { 25 | marginLeft: '.1em', 26 | paddingLeft: '1em', 27 | borderLeft: '2px solid rgba(0,0,0,.15)' 28 | }) 29 | 30 | export const Info = styled('span', { 31 | color: 'grey', 32 | fontSize: '.7em' 33 | }) 34 | 35 | export const Expander = ({ expanded, style = {}, ...rest }) => ( 36 | 44 | ▶ 45 | 46 | ) 47 | 48 | const DefaultRenderer = ({ 49 | handleEntry, 50 | label, 51 | value, 52 | // path, 53 | subEntries, 54 | subEntryPages, 55 | type, 56 | // depth, 57 | expanded, 58 | toggle, 59 | pageSize 60 | }) => { 61 | const [expandedPages, setExpandedPages] = React.useState([]) 62 | 63 | return ( 64 | 65 | {subEntryPages?.length ? ( 66 | <> 67 | 74 | {expanded ? ( 75 | subEntryPages.length === 1 ? ( 76 | 77 | {subEntries.map(entry => handleEntry(entry))} 78 | 79 | ) : ( 80 | 81 | {subEntryPages.map((entries, index) => ( 82 |
83 | 84 | 96 | {expandedPages.includes(index) ? ( 97 | 98 | {entries.map(entry => handleEntry(entry))} 99 | 100 | ) : null} 101 | 102 |
103 | ))} 104 |
105 | ) 106 | ) : null} 107 | 108 | ) : ( 109 | <> 110 | {' '} 111 | 112 | {JSON.stringify(value, Object.getOwnPropertyNames(Object(value)))} 113 | 114 | 115 | )} 116 |
117 | ) 118 | } 119 | 120 | export default function Explorer({ 121 | value, 122 | defaultExpanded, 123 | renderer = DefaultRenderer, 124 | pageSize = 100, 125 | depth = 0, 126 | ...rest 127 | }) { 128 | const [expanded, setExpanded] = React.useState(defaultExpanded) 129 | 130 | const toggle = set => { 131 | setExpanded(old => (typeof set !== 'undefined' ? set : !old)) 132 | } 133 | 134 | const path = [] 135 | let type = typeof value 136 | let subEntries 137 | const subEntryPages = [] 138 | 139 | const makeProperty = sub => { 140 | const newPath = path.concat(sub.label) 141 | const subDefaultExpanded = 142 | defaultExpanded === true 143 | ? { [sub.label]: true } 144 | : defaultExpanded?.[sub.label] 145 | return { 146 | ...sub, 147 | path: newPath, 148 | depth: depth + 1, 149 | defaultExpanded: subDefaultExpanded 150 | } 151 | } 152 | 153 | if (Array.isArray(value)) { 154 | type = 'array' 155 | subEntries = value.map((d, i) => 156 | makeProperty({ 157 | label: i, 158 | value: d 159 | }) 160 | ) 161 | } else if ( 162 | value !== null && 163 | typeof value === 'object' && 164 | typeof value[Symbol.iterator] === 'function' 165 | ) { 166 | type = 'Iterable' 167 | subEntries = Array.from(value, (val, i) => 168 | makeProperty({ 169 | label: i, 170 | value: val 171 | }) 172 | ) 173 | } else if (typeof value === 'object' && value !== null) { 174 | type = 'object' 175 | // eslint-disable-next-line no-shadow 176 | subEntries = Object.entries(value).map(([label, _value]) => 177 | makeProperty({ 178 | label, 179 | value: _value 180 | }) 181 | ) 182 | } 183 | 184 | if (subEntries) { 185 | let i = 0 186 | 187 | while (i < subEntries.length) { 188 | subEntryPages.push(subEntries.slice(i, i + pageSize)) 189 | i = i + pageSize 190 | } 191 | } 192 | 193 | return renderer({ 194 | handleEntry: entry => ( 195 | 196 | ), 197 | type, 198 | subEntries, 199 | subEntryPages, 200 | depth, 201 | value, 202 | path, 203 | expanded, 204 | toggle, 205 | pageSize, 206 | ...rest 207 | }) 208 | } 209 | -------------------------------------------------------------------------------- /src/Logo.tsx: -------------------------------------------------------------------------------- 1 | export default function Logo(props: any) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/context/index.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch, FC, useContext, useReducer } from 'react' 2 | import { Key } from 'swr' 3 | import reducer, { SWRDevToolsActions } from './reducer' 4 | 5 | export interface ISWRItem { 6 | id: string 7 | key: Key 8 | data: any 9 | error: string | null 10 | isLoading: boolean 11 | timestamp: Date 12 | options: any 13 | } 14 | export interface ISWRState { 15 | cache: ISWRItem[] 16 | history: ISWRItem[] 17 | } 18 | interface ISWRContext { 19 | state: ISWRState 20 | dispatch: Dispatch 21 | } 22 | 23 | export const initialState: ISWRContext = { 24 | state: { 25 | cache: [], 26 | history: [] 27 | }, 28 | dispatch: () => {} 29 | } 30 | const context = createContext(initialState) 31 | export const useSWRDevtoolsContext = () => useContext(context) 32 | 33 | const Provider: FC = ({ children }) => { 34 | const [state, dispatch] = useReducer(reducer, initialState.state) 35 | return ( 36 | {children} 37 | ) 38 | } 39 | 40 | export default Provider 41 | -------------------------------------------------------------------------------- /src/context/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Key } from 'swr' 2 | import { initialState, ISWRState } from '.' 3 | export enum SWRActionType { 4 | ITEM_SUCCESS = 'ITEM_SUCCESS', 5 | ITEM_ERROR = 'ITEM_ERROR', 6 | ITEM_LOADING = 'ITEM_LOADING', 7 | ITEM_DELETE = 'ITEM_DELETE' 8 | } 9 | const GenId = () => { 10 | let result = '' 11 | const characters = 12 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 13 | const charactersLength = characters.length 14 | for (let i = 0; i < 10; i++) { 15 | result += characters.charAt(Math.floor(Math.random() * charactersLength)) 16 | } 17 | return result 18 | } 19 | 20 | export type SWRDevToolsActions = 21 | | { 22 | type: SWRActionType.ITEM_SUCCESS 23 | payload: { key: Key; data: any; options: any } 24 | } 25 | | { 26 | type: SWRActionType.ITEM_ERROR 27 | payload: { key: Key; error: string; options: any } 28 | } 29 | | { type: SWRActionType.ITEM_DELETE; payload: { key: Key } } 30 | | { 31 | type: SWRActionType.ITEM_LOADING 32 | payload: { key: Key; options: any } 33 | } 34 | 35 | const reducer = ( 36 | state: ISWRState = initialState.state, 37 | action: SWRDevToolsActions 38 | ): ISWRState => { 39 | switch (action.type) { 40 | case SWRActionType.ITEM_LOADING: { 41 | const find = state.cache.find(x => x.key === action.payload.key) 42 | const OtherItems = state.cache.filter(x => x.key !== action.payload.key) 43 | const item = { 44 | id: GenId(), 45 | key: action.payload.key, 46 | data: find?.data ?? null, 47 | error: find?.error ?? null, 48 | isLoading: true, 49 | timestamp: new Date(), 50 | options: action.payload.options 51 | } 52 | return { 53 | history: [...state.history, item], 54 | cache: [...OtherItems, item] 55 | } 56 | } 57 | case SWRActionType.ITEM_SUCCESS: { 58 | const items = state.cache.filter(x => x.key !== action.payload.key) 59 | const item = { 60 | id: GenId(), 61 | key: action.payload.key, 62 | data: action.payload.data, 63 | error: null, 64 | isLoading: false, 65 | timestamp: new Date(), 66 | options: action.payload.options 67 | } 68 | return { 69 | history: [...state.history, item], 70 | cache: [...items, item] 71 | } 72 | } 73 | case SWRActionType.ITEM_DELETE: { 74 | const items = state.cache.filter(x => x.key !== action.payload.key) 75 | const item = { 76 | id: GenId(), 77 | key: action.payload.key, 78 | data: null, 79 | error: null, 80 | isLoading: false, 81 | timestamp: new Date(), 82 | options: null 83 | } 84 | return { 85 | history: [...state.history, item], 86 | cache: items 87 | } 88 | } 89 | case SWRActionType.ITEM_ERROR: { 90 | const find = state.cache.find(x => x.key === action.payload.key) 91 | const items = state.cache.filter(x => x.key !== action.payload.key) 92 | const item = { 93 | id: GenId(), 94 | key: action.payload.key, 95 | data: find?.data ?? null, 96 | error: action.payload.error, 97 | isLoading: false, 98 | timestamp: new Date(), 99 | options: action.payload.options 100 | } 101 | return { 102 | history: [...state.history, item], 103 | cache: [...items] 104 | } 105 | } 106 | default: 107 | return state 108 | } 109 | } 110 | 111 | export default reducer 112 | -------------------------------------------------------------------------------- /src/devtools.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | 3 | import { matchSorter } from 'match-sorter' 4 | import useLocalStorage from './useLocalStorage' 5 | import { useIsMounted, useSafeState } from './utils' 6 | 7 | import { 8 | Panel, 9 | QueryKeys, 10 | QueryKey, 11 | Button, 12 | Code, 13 | Input, 14 | Select, 15 | ActiveKeyPanel 16 | } from './styledComponents' 17 | import { ThemeProvider, defaultTheme as theme } from './theme' 18 | // import { getQueryStatusLabel, getQueryStatusColor } from './utils'; 19 | import Explorer from './Explorer' 20 | import Logo from './Logo' 21 | import { useSWRConfig } from 'swr' 22 | import { ISWRItem, useSWRDevtoolsContext } from './context' 23 | import { SWRActionType } from './context/reducer' 24 | 25 | interface DevtoolsOptions { 26 | /** 27 | * Set this true if you want the dev tools to default to being open 28 | */ 29 | initialIsOpen?: boolean 30 | /** 31 | * Use this to add props to the panel. For example, you can add className, style (merge and override default style), etc. 32 | */ 33 | panelProps?: React.DetailedHTMLProps< 34 | React.HTMLAttributes, 35 | HTMLDivElement 36 | > 37 | /** 38 | * Use this to add props to the close button. For example, you can add className, style (merge and override default style), onClick (extend default handler), etc. 39 | */ 40 | closeButtonProps?: React.DetailedHTMLProps< 41 | React.ButtonHTMLAttributes, 42 | HTMLButtonElement 43 | > 44 | /** 45 | * Use this to add props to the toggle button. For example, you can add className, style (merge and override default style), onClick (extend default handler), etc. 46 | */ 47 | toggleButtonProps?: React.DetailedHTMLProps< 48 | React.ButtonHTMLAttributes, 49 | HTMLButtonElement 50 | > 51 | /** 52 | * The position of the React Query logo to open and close the devtools panel. 53 | * Defaults to 'bottom-left'. 54 | */ 55 | position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' 56 | /** 57 | * Use this to render the devtools inside a different type of container element for a11y purposes. 58 | * Any string which corresponds to a valid intrinsic JSX element is allowed. 59 | * Defaults to 'aside'. 60 | */ 61 | containerElement?: string | any 62 | } 63 | 64 | interface DevtoolsPanelOptions { 65 | /** 66 | * The standard React style object used to style a component with inline styles 67 | */ 68 | style?: React.CSSProperties 69 | /** 70 | * The standard React className property used to style a component with classes 71 | */ 72 | className?: string 73 | /** 74 | * A boolean variable indicating whether the panel is open or closed 75 | */ 76 | isOpen?: boolean 77 | /** 78 | * A function that toggles the open and close state of the panel 79 | */ 80 | setIsOpen: (isOpen: boolean) => void 81 | /** 82 | * Handles the opening and closing the devtools panel 83 | */ 84 | handleDragStart: (e: React.MouseEvent) => void 85 | } 86 | 87 | const isServer = typeof window === 'undefined' 88 | 89 | export function SWRDevtools({ 90 | initialIsOpen, 91 | panelProps = {}, 92 | closeButtonProps = {}, 93 | toggleButtonProps = {}, 94 | position = 'bottom-right', 95 | containerElement: Container = 'aside' 96 | }: DevtoolsOptions): React.ReactElement | null { 97 | const rootRef = React.useRef(null) 98 | const panelRef = React.useRef(null) 99 | const [isOpen, setIsOpen] = useLocalStorage('SWRDevtoolsOpen', initialIsOpen) 100 | const [devtoolsHeight, setDevtoolsHeight] = useLocalStorage( 101 | 'SWRDevtoolsHeight', 102 | null 103 | ) 104 | const [isResolvedOpen, setIsResolvedOpen] = useSafeState(false) 105 | const [isResizing, setIsResizing] = useSafeState(false) 106 | const isMounted = useIsMounted() 107 | 108 | const handleDragStart = ( 109 | panelElement: HTMLDivElement | null, 110 | startEvent: React.MouseEvent 111 | ) => { 112 | if (startEvent.button !== 0) return // Only allow left click for drag 113 | 114 | setIsResizing(true) 115 | 116 | const dragInfo = { 117 | originalHeight: panelElement?.getBoundingClientRect().height ?? 0, 118 | pageY: startEvent.pageY 119 | } 120 | 121 | const run = (moveEvent: MouseEvent) => { 122 | const delta = dragInfo.pageY - moveEvent.pageY 123 | const newHeight = dragInfo?.originalHeight + delta 124 | 125 | setDevtoolsHeight(newHeight) 126 | 127 | if (newHeight < 70) { 128 | setIsOpen(false) 129 | } else { 130 | setIsOpen(true) 131 | } 132 | } 133 | 134 | const unsub = () => { 135 | setIsResizing(false) 136 | document.removeEventListener('mousemove', run) 137 | document.removeEventListener('mouseUp', unsub) 138 | } 139 | 140 | document.addEventListener('mousemove', run) 141 | document.addEventListener('mouseup', unsub) 142 | } 143 | 144 | React.useEffect(() => { 145 | setIsResolvedOpen(isOpen ?? false) 146 | }, [isOpen, isResolvedOpen, setIsResolvedOpen]) 147 | 148 | // Toggle panel visibility before/after transition (depending on direction). 149 | // Prevents focusing in a closed panel. 150 | React.useEffect(() => { 151 | const ref = panelRef.current 152 | if (ref) { 153 | const handlePanelTransitionStart = () => { 154 | if (ref && isResolvedOpen) { 155 | ref.style.visibility = 'visible' 156 | } 157 | } 158 | 159 | const handlePanelTransitionEnd = () => { 160 | if (ref && !isResolvedOpen) { 161 | ref.style.visibility = 'hidden' 162 | } 163 | } 164 | 165 | ref.addEventListener('transitionstart', handlePanelTransitionStart) 166 | ref.addEventListener('transitionend', handlePanelTransitionEnd) 167 | 168 | return () => { 169 | ref.removeEventListener('transitionstart', handlePanelTransitionStart) 170 | ref.removeEventListener('transitionend', handlePanelTransitionEnd) 171 | } 172 | } 173 | }, [isResolvedOpen]) 174 | 175 | React[isServer ? 'useEffect' : 'useLayoutEffect'](() => { 176 | if (isResolvedOpen) { 177 | const previousValue = rootRef.current?.parentElement?.style.paddingBottom 178 | 179 | const run = () => { 180 | const containerHeight = panelRef.current?.getBoundingClientRect().height 181 | if (rootRef.current?.parentElement) { 182 | rootRef.current.parentElement.style.paddingBottom = `${containerHeight}px` 183 | } 184 | } 185 | 186 | run() 187 | 188 | if (typeof window !== 'undefined') { 189 | window.addEventListener('resize', run) 190 | 191 | return () => { 192 | window.removeEventListener('resize', run) 193 | if ( 194 | rootRef.current?.parentElement && 195 | typeof previousValue === 'string' 196 | ) { 197 | rootRef.current.parentElement.style.paddingBottom = previousValue 198 | } 199 | } 200 | } 201 | } 202 | }, [isResolvedOpen]) 203 | 204 | const { style: panelStyle = {}, ...otherPanelProps } = panelProps 205 | 206 | const { 207 | style: closeButtonStyle = {}, 208 | onClick: onCloseClick, 209 | ...otherCloseButtonProps 210 | } = closeButtonProps 211 | 212 | const { 213 | style: toggleButtonStyle = {}, 214 | onClick: onToggleClick, 215 | ...otherToggleButtonProps 216 | } = toggleButtonProps 217 | 218 | // Do not render on the server 219 | if (!isMounted()) return null 220 | 221 | return ( 222 | 227 | 228 | handleDragStart(panelRef.current, e)} 265 | /> 266 | {isResolvedOpen ? ( 267 | 303 | ) : null} 304 | 305 | {!isResolvedOpen ? ( 306 | 352 | ) : null} 353 | 354 | ) 355 | } 356 | 357 | const sortFns: Record number> = { 358 | key: (a, b) => (a.key! > b.key! ? 1 : -1), 359 | 'Last Updated': (a, b) => (a.timestamp < b.timestamp ? 1 : -1) 360 | } 361 | 362 | export const SWRDevtoolsPanel = React.forwardRef< 363 | HTMLDivElement, 364 | DevtoolsPanelOptions 365 | >(function SWRDevtoolsPanel(props, ref): React.ReactElement { 366 | const { isOpen = true, setIsOpen, handleDragStart, ...panelProps } = props 367 | 368 | const [activeItem, setActiveItem] = useLocalStorage( 369 | 'SWRDevtoolsActiveItem', 370 | undefined 371 | ) 372 | const { state, dispatch } = useSWRDevtoolsContext() 373 | const { cache, mutate } = useSWRConfig() 374 | const [sort, setSort] = useLocalStorage( 375 | 'SWRDevtoolsSortFn', 376 | Object.keys(sortFns)[0] 377 | ) 378 | const [list, setList] = useLocalStorage<'cache' | 'history'>( 379 | 'SWRDevtoolsList', 380 | 'cache' 381 | ) 382 | 383 | // useEffect(() => { 384 | // if (list) { 385 | // const item = state[list!].find((q) => q.id === activeItem); 386 | // setActiveData(item); 387 | // } 388 | // }, [activeItem, state, list]); 389 | 390 | const [filter, setFilter] = useLocalStorage('SWRDevtoolsFilter', '') 391 | 392 | const [sortDesc, setSortDesc] = useLocalStorage('SWRDevtoolsSortDesc', false) 393 | 394 | const sortFn = React.useMemo(() => sortFns[sort as string], [sort]) 395 | 396 | React[isServer ? 'useEffect' : 'useLayoutEffect'](() => { 397 | if (!sortFn) { 398 | setSort(Object.keys(sortFns)[0] as string) 399 | } 400 | }, [setSort, sortFn]) 401 | 402 | const keys = React.useMemo(() => { 403 | if (list) { 404 | const _list = state[list] 405 | // console.log(_list); 406 | // const item = state[list!].find((q) => q.id === activeItem?.id); 407 | // setActiveItem(item); 408 | 409 | const sorted = _list.sort(sortFn) 410 | if (sortDesc) { 411 | sorted.reverse() 412 | } 413 | 414 | if (!filter) { 415 | return sorted 416 | } 417 | 418 | return matchSorter(sorted, filter, { keys: ['key'] }).filter(d => d.key) 419 | } 420 | 421 | return [] 422 | }, [sortDesc, list, sort, state, filter]) 423 | useEffect(() => { 424 | if (list && activeItem) { 425 | // setActiveItem((prv) => { 426 | // return state[list].find((q) => q.id === prv?.id); 427 | // }); 428 | const item = state[list!].find(q => q.key === activeItem?.key) 429 | setActiveItem(item) 430 | } 431 | }, [state]) 432 | 433 | const handleItemClick = (id: string) => { 434 | if (list) { 435 | const item = state[list!].find(q => q.id === id) 436 | setActiveItem(item) 437 | } 438 | } 439 | const handleRefetch = () => { 440 | mutate(activeItem?.key).catch(err => { 441 | console.log(err) 442 | }) 443 | } 444 | const handleDelete = () => { 445 | dispatch({ 446 | type: SWRActionType.ITEM_DELETE, 447 | payload: { key: activeItem?.key } 448 | }) 449 | cache.delete(activeItem?.key) 450 | setActiveItem(undefined) 451 | } 452 | // const handleInvalidate = () => { 453 | // mutate(activeKey).then(() => { 454 | // console.log('revalidated') 455 | // }) 456 | // } 457 | 458 | return ( 459 | 460 | 467 |