Skip to content

Commit

Permalink
feat(cozy-devtools): Add first version to test providers
Browse files Browse the repository at this point in the history
  • Loading branch information
JF-Cozy committed Nov 6, 2024
1 parent 102cfa3 commit 5689640
Show file tree
Hide file tree
Showing 16 changed files with 1,241 additions and 0 deletions.
Empty file.
90 changes: 90 additions & 0 deletions packages/cozy-devtools/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
## Cozy-Devtools

Cozy-Devtools exposes a devtool that can be injected in an app for debug
and better developer experience. It is inspired by the [awesome devtools
for react-query][react-query devtools].

To activate it, just run in your browser console:

```js
flag('debug', true)
```

### Usage

Before using the devtools, you need to install cozy-ui and react-inspector.

```bash
yarn add cozy-ui # >= 48.0.0
yarn add react-inspector # >= 5.1.0
```

Next, you need to add it to your app, inside a CozyProvider.

```jsx
import CozyClient, { CozyProvider } from 'cozy-client'
import CozyDevtools from 'cozy-client/dist/devtools'

const App = () => {
return <CozyProvider client={client}>
/* Your app is here */
{ process.env.NODE_ENV !== 'production' ? <CozyDevtools /> : null }
</CozyProvider>
}
```

### Panels

- The devtools is made of several "panels".
- There are default panels and the app can also inject its own adhoc panels.

#### Queries

Shows current queries inside cozy-client cache. Allows to see the data of
the query. The execution time is also shown, and is very valuable to track
down performance issues. It uses the execution statistics collected
from CouchDB.

#### Flags

Shows all the current flags and allow to modify them.

#### Libraries

Show library versions based on the global **VERSIONS** variable that
should be injected by the app. If it is defined, the panel will be blank.

#### PouchLink

If you use the PouchLink to synchronize your data to PouchDB, you can use
the optional devtool PouchLink devtool panel. Since PouchDB is optional,
it is not available by default and you need to explicitly tell the Devtools
to display it.

```jsx
import PouchDevtools from 'cozy-client/dist/devtools/Pouch'

() => <CozyDevTools panels={{ id: 'pouch', Component: PouchDevtools}} />
```

### Ideas for next features

- Performance tips in query panels

- Show index related tips
- Show slow queries
- Show repeating queries
- Show queries downloading too much data

- Actions on queries

- Reset data inside query
- Refetch
- Set to error
- Delete from store

If you have any other idea, please [open an issue][open-issue] 👍

[react-query devtools]: https://github.com/tannerlinsley/react-query-devtools

[open-issue]: https://github.com/cozy/cozy-libs/issues/new
12 changes: 12 additions & 0 deletions packages/cozy-devtools/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = {
presets: ['cozy-app'],
env: {
transpilation: {
ignore: ['**/*.spec.jsx', '**/*.spec.js', '**/*.spec.tsx', '**/*.spec.ts']
},
test: {
presets: [['cozy-app', { transformRuntime: { helpers: true } }]]
}
},
ignore: ['examples/**/*', '**/*.md', '**/*.snap']
}
19 changes: 19 additions & 0 deletions packages/cozy-devtools/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
testPathIgnorePatterns: ['node_modules', 'dist'],
testEnvironment: 'jest-environment-jsdom-sixteen',
testURL: 'http://localhost/',
moduleFileExtensions: ['js', 'jsx', 'json', 'ts', 'tsx'],
moduleDirectories: ['src', 'node_modules'],
moduleNameMapper: {
'^cozy-client$': 'cozy-client/dist/index'
},
transformIgnorePatterns: ['node_modules/(?!(cozy-ui)'],
transform: {
'^.+\\.(ts|tsx|js|jsx)?$': 'babel-jest'
},
globals: {
__ALLOW_HTTP__: false,
cozy: {}
},
setupFilesAfterEnv: ['jest-canvas-mock']
}
47 changes: 47 additions & 0 deletions packages/cozy-devtools/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "cozy-devtools",
"version": "0.0.1",
"description": "Cozy-Devtools exposes a devtool that can be injected in an app for debug and better developer experience.",
"main": "dist/index.js",
"license": "MIT",
"homepage": "https://github.com/cozy/cozy-libs/blob/master/packages/cozy-devtools/README.md",
"repository": {
"type": "git",
"url": "git+https://github.com/cozy/cozy-libs.git"
},
"bugs": {
"url": "https://github.com/cozy/cozy-libs/issues"
},
"scripts": {
"build": "rm -rf ./dist && env BABEL_ENV=transpilation babel --extensions .js,.jsx,.md,.styl,.json,.snap ./src -d ./dist --copy-files --no-copy-ignored --verbose",
"start": "yarn build --watch",
"prepublishOnly": "yarn build",
"test": "env NODE_ENV=test jest --passWithNoTests",
"lint": "cd .. && yarn eslint --ext js,jsx packages/cozy-devtools"
},
"devDependencies": {
"babel-preset-cozy-app": "^2.5.0",
"cozy-client": "^50.0.0",
"cozy-flags": "^4.3.0",
"cozy-intent": "^2.26.0",
"cozy-pouch-link": "^50.0.0",
"cozy-ui": "^111.19.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"stylus": "0.64.0"
},
"dependencies": {
"date-fns": "2.29.3",
"lodash": "4.17.13",
"react-inspector": "5.1.1"
},
"peerDependencies": {
"cozy-client": ">=50.0.0",
"cozy-flags": ">=4.3.0",
"cozy-intent": ">=2.26.0",
"cozy-pouch-link": ">=50.0.0",
"cozy-ui": ">=111.19.0",
"react": ">=16.12.0",
"react-dom": ">=16.12.0"
}
}
81 changes: 81 additions & 0 deletions packages/cozy-devtools/src/Flags/FlagEdit.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { useEffect, useState } from 'react'

import flag from 'cozy-flags'
import Button from 'cozy-ui/transpiled/react/Buttons'
import TextField from 'cozy-ui/transpiled/react/TextField'

import { isJSONString, makeHumanValue } from './helpers'

const FlagEdit = ({ flag: editedFlag }) => {
const [formData, setFormData] = useState({
key: '',
name: '',
value: '',
humanValue: ''
})

useEffect(() => {
if (editedFlag) setFormData(editedFlag)
}, [editedFlag])

const handleSubmit = e => {
e.preventDefault()
if (!formData.name || !formData.value) return

/** @type {any} */
let value = formData.value
if (isJSONString(value)) {
value = JSON.parse(value)
} else if (value === 'true' || value === 'false') {
value = Boolean(value)
}

flag(formData.name, value)
location.reload()
}

const handleFlagNameChange = e => {
setFormData({
...formData,
key: `flag__${e.target.value}`,
name: e.target.value
})
}

const handleFlagValueChange = e => {
let value = e.target.value
if (Number.isInteger(value)) {
value = parseInt(value)
}
setFormData({
...formData,
value,
humanValue: makeHumanValue(value)
})
}

return (
<form onSubmit={handleSubmit} className="u-mt-1 u-flex-items-center u-flex">
<TextField
label="Name"
name="name"
onChange={handleFlagNameChange}
value={formData.name}
size="small"
variant="outlined"
/>
<TextField
label="Value"
name="value"
onChange={handleFlagValueChange}
value={formData.humanValue}
size="small"
variant="outlined"
className="u-ml-1"
/>
<Button type="submit" label="Edit" className="u-ml-1" />
</form>
)
}

export { FlagEdit }
54 changes: 54 additions & 0 deletions packages/cozy-devtools/src/Flags/FlagItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react'
import { ObjectInspector } from 'react-inspector'

import flagUtils from 'cozy-flags'
import Checkbox from 'cozy-ui/transpiled/react/Checkbox'
import Icon from 'cozy-ui/transpiled/react/Icon'
import IconButton from 'cozy-ui/transpiled/react/IconButton'
import PenIcon from 'cozy-ui/transpiled/react/Icons/Pen'
import TrashIcon from 'cozy-ui/transpiled/react/Icons/Trash'
import ListItem from 'cozy-ui/transpiled/react/ListItem'
import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon'
import ListItemSecondaryAction from 'cozy-ui/transpiled/react/ListItemSecondaryAction'
import ListItemText from 'cozy-ui/transpiled/react/ListItemText'

const FlagItem = ({ flag, onEdit, onTrash }) => {
const handleCheckboxChange = e => {
flagUtils(flag.name, e.target.checked)
location.reload()
}

return (
<ListItem size="small">
<ListItemIcon>
{flag.type === 'boolean' ? (
<Checkbox
size="small"
checked={flag.value}
onChange={handleCheckboxChange}
/>
) : null}
</ListItemIcon>
<ListItemText
primary={flag.humanName}
secondary={
flag.type === 'object' ? (
<ObjectInspector data={flag.value} />
) : flag.type !== 'boolean' ? (
flag.humanValue
) : null
}
/>
<ListItemSecondaryAction>
<IconButton onClick={() => onEdit(flag)}>
<Icon icon={PenIcon} />
</IconButton>
<IconButton onClick={() => onTrash(flag)}>
<Icon icon={TrashIcon} />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
)
}

export default FlagItem
45 changes: 45 additions & 0 deletions packages/cozy-devtools/src/Flags/Flags.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, { useState } from 'react'

import List from 'cozy-ui/transpiled/react/List'
import Typography from 'cozy-ui/transpiled/react/Typography'

import { FlagEdit } from './FlagEdit'
import FlagItem from './FlagItem'
import { computeFlags } from './helpers'
import PanelContent from '../PanelContent'

const Flags = () => {
const [edited, setEdited] = useState(null)

const flags = computeFlags()

const handleEdit = flag => {
setEdited(flag)
}

const handleTrash = flag => {
if (localStorage.getItem(flag.key)) {
localStorage.removeItem(flag.key)
location.reload()
}
}

return (
<PanelContent>
<Typography variant="subtitle1">Flags</Typography>
<List dense className="u-maw-6">
{flags.map(flag => (
<FlagItem
key={flag.key}
flag={flag}
onEdit={handleEdit}
onTrash={handleTrash}
/>
))}
</List>
<FlagEdit flag={edited} />
</PanelContent>
)
}

export default Flags
32 changes: 32 additions & 0 deletions packages/cozy-devtools/src/Flags/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import flag from 'cozy-flags'

const human = name => {
return name.replace(/[a-z][A-Z]/g, str => str[0] + ' ' + str[1].toLowerCase())
}

export const makeHumanValue = value => {
return typeof value === 'object' ? JSON.stringify(value) : value.toString()
}

export const computeFlags = () => {
return flag.list().map(name => {
const value = flag(name)
return {
key: `flag__${name}`,
name,
type: typeof value,
humanName: human(name),
value,
humanValue: makeHumanValue(value)
}
})
}

export const isJSONString = str => {
try {
JSON.parse(str)
return true
} catch (e) {
return false
}
}
Loading

0 comments on commit 5689640

Please sign in to comment.