Skip to content

Commit

Permalink
feat(devtools): createDevtools and state logs
Browse files Browse the repository at this point in the history
  • Loading branch information
artalar committed Dec 17, 2024
1 parent 5eb91bb commit 6315923
Show file tree
Hide file tree
Showing 14 changed files with 473 additions and 184 deletions.
102 changes: 87 additions & 15 deletions docs/src/content/docs/package/devtools.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: Reatom developer tools for states and actions inspecting
<!-- DO NOT EDIT THIS FILE -->
<!-- CHECK "packages/*/README.md" -->

Developer tools for states and actions inspecting
This devtools is NOT coupled to Reatom and may be worked as a general logger for any kind of data. For Reatom users it has some additional features like actions and states cause tracing. But by default it has a huge list of features to log, highlight, filter and inspect any kind of logs.

## Installation

Expand All @@ -17,29 +17,101 @@ npm install @reatom/devtools

**Note** that this package is published with ESM bundle only.

## Usage
## Setup

You typical setup would look like that.
Typical setup in your root file may look like that.

```ts
import { createCtx, connectLogger } from '@reatom/framework'
import { connectDevtools } from '@reatom/devtools'
declare global {
var DEVTOOLS: null | Devtools
}
if (import.meta.env.DEV) {
const { createDevtools } = await import('@reatom/devtools')
globalThis.DEVTOOLS = createDevtools()
} else {
globalThis.DEVTOOLS = null
}
```

const ctx = createCtx()
It is recommended to put the devtools to the global scope to log and inspect needed data easily during development or debugging.

if (import.meta.env.DEV) {
connectLogger(ctx)
connectDevtools(ctx)
**IMPORTANT NOTE** **for Reatom users** - put your `ctx` as an option to the create function (`createDevtools({ ctx })`).

## Devtools API

`createDevtools` returns an object with a few methods:

- `log(name: string, payload?: any)` - log any kind of data
- `state<T>(name: string, initState: T): DevtoolsState<T>` - log and create a state with a lifetime, which you can update directly later and subscribe to changes from the devtools update form
- `show()` - show the devtools (needed for `initVisibility: false` option)
- `hide()` - hide the devtools the opposite to `show` method :)

With `declare global { var DEVTOOLS: undefined | Devtools }` you will be able to use this methods like so:

```ts
function doSome(payload) {
DEVTOOLS?.log('doSome', payload)

// the rest of the code...
}
```

You can log states too (only changes will be logged)

// ...
```ts
const [state, setState] = useState()
DEVTOOLS?.state('my-state', state)
```

`connectDevtools` has an optional second argument where you can specify how to process an atoms names.
With "state" log you can reinit your form from the devtools with the edit tool (see [UI features](#ui-features) below).

```ts
type Options = {
separator?: string | RegExp | ((name: string) => Array<string>) // default is /\.|#/
privatePrefix?: string // default is '_'
}
const { values, init } = useForm()
React.useEffect(() => DEVTOOLS?.state('my-form', values).subscribe(init), [state])
```

## createDevtools Options

The create function accepts the following options:

- `initSize?: number` - initial value of the size of collected logs
- `initVisibility?: boolean` - the predicate to mount the devtools to the DOM, devtools will collect all logs nevertheless of the state, it affects only the UI
- `getColor?: typeof getColor` - the function to get the color of the log, the default function available by an import with the same name
- `separator?: string | RegExp | ((name: string) => Array<string>)` - the separator to split the name of the log for "state view", `.` by default
- `privatePrefix?: string` - the prefix which mark the log as private, `_` by default

## UI features

### Logs view

![Devtools logs view](devtools1.png)

1. **Filter form** - input RegExp will apply to the name of the log immediately, you can choose the type of the match mechanic: equal, not equal, highlight (color picker), off. By pressing "save" button the filter options will be saved in the filters group below.
2. **Filters group** - the list of saved filters, which you can change, also additional match mechanic "garbage" can be added, which will throw input logs forever, which is useful for better performance of many logs.
> "private" unremovable filter applies to private logs, which defines by `privatePrefix` option.
3. **Values search** allows you to filter the logs by the stringified payload matching.
4. **Actions** allows you to modify the behavior of the devtools:
- "clear lines" clears causes chain of transactional logs
- "clear logs" removes all collected logs
- "size" sets the amount of logs to be collected
- "inline preview" toggles the preview under log name
- "hover preview" toggles the preview by document icon before a log name hover
- "timestamps" toggles the timestamps lines of transactions (you need to restart the page to see the changes)
5. **Log name** row starts with chain highlight button, then payload preview button (could be hovered with the options or clicked to hold it), then log name.
6. **Log payload** row includes the payload of the log, if the relative option is enabled.
7. **Payload actions** (see description below)

### Payload inspector view

![Devtools payload inspector view](devtools2.png)

In the screenshot above we turn off the inline preview, clicked at the chain button of "issues.dataAtom" log, which paint the cause chain to other relative logs, and clicked on the document button of "issues.dataAtom" log to see the payload inspector view.

1. Name of the log (atom) and its payload (state).
2. List of the changes of this log.
3. **Payload action** buttons:
- **Convert to plain JSON** (useful for [atomized](/recipes/atomization/) state inspection)
- **Log** to the browser console
- **Copy** JSON to the clipboard
- **Open edit form** (will create additional log if will be applied)
- **Close** the inspector
Binary file added docs/src/content/docs/package/devtools1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/src/content/docs/package/devtools2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 11 additions & 4 deletions examples/react-search/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import ReactDOM from 'react-dom/client'
import { createCtx, connectLogger } from '@reatom/framework'
import { connectDevtools } from '@reatom/devtools'
import { createCtx } from '@reatom/framework'
import { type Devtools } from '@reatom/devtools'
import { reatomContext } from '@reatom/npm-react'
import { App } from './app'

const ctx = createCtx()

connectLogger(ctx)
connectDevtools(ctx)
declare global {
var DEVTOOLS: undefined | Devtools
}
if (import.meta.env.DEV) {
const { createDevtools } = await import('@reatom/devtools')
globalThis.DEVTOOLS = createDevtools({ ctx, initVisibility: true })
} else {
globalThis.DEVTOOLS = undefined
}

const root = ReactDOM.createRoot(document.getElementById('root')!)
root.render(
Expand Down
102 changes: 87 additions & 15 deletions packages/devtools/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Developer tools for states and actions inspecting
This devtools is NOT coupled to Reatom and may be worked as a general logger for any kind of data. For Reatom users it has some additional features like actions and states cause tracing. But by default it has a huge list of features to log, highlight, filter and inspect any kind of logs.

## Installation

Expand All @@ -8,29 +8,101 @@ npm install @reatom/devtools

**Note** that this package is published with ESM bundle only.

## Usage
## Setup

You typical setup would look like that.
Typical setup in your root file may look like that.

```ts
import { createCtx, connectLogger } from '@reatom/framework'
import { connectDevtools } from '@reatom/devtools'
declare global {
var DEVTOOLS: null | Devtools
}
if (import.meta.env.DEV) {
const { createDevtools } = await import('@reatom/devtools')
globalThis.DEVTOOLS = createDevtools()
} else {
globalThis.DEVTOOLS = null
}
```

const ctx = createCtx()
It is recommended to put the devtools to the global scope to log and inspect needed data easily during development or debugging.

if (import.meta.env.DEV) {
connectLogger(ctx)
connectDevtools(ctx)
**IMPORTANT NOTE** **for Reatom users** - put your `ctx` as an option to the create function (`createDevtools({ ctx })`).

## Devtools API

`createDevtools` returns an object with a few methods:

- `log(name: string, payload?: any)` - log any kind of data
- `state<T>(name: string, initState: T): DevtoolsState<T>` - log and create a state with a lifetime, which you can update directly later and subscribe to changes from the devtools update form
- `show()` - show the devtools (needed for `initVisibility: false` option)
- `hide()` - hide the devtools the opposite to `show` method :)

With `declare global { var DEVTOOLS: undefined | Devtools }` you will be able to use this methods like so:

```ts
function doSome(payload) {
DEVTOOLS?.log('doSome', payload)

// the rest of the code...
}
```

You can log states too (only changes will be logged)

// ...
```ts
const [state, setState] = useState()
DEVTOOLS?.state('my-state', state)
```

`connectDevtools` has an optional second argument where you can specify how to process an atoms names.
With "state" log you can reinit your form from the devtools with the edit tool (see [UI features](#ui-features) below).

```ts
type Options = {
separator?: string | RegExp | ((name: string) => Array<string>) // default is /\.|#/
privatePrefix?: string // default is '_'
}
const { values, init } = useForm()
React.useEffect(() => DEVTOOLS?.state('my-form', values).subscribe(init), [state])
```

## createDevtools Options

The create function accepts the following options:

- `initSize?: number` - initial value of the size of collected logs
- `initVisibility?: boolean` - the predicate to mount the devtools to the DOM, devtools will collect all logs nevertheless of the state, it affects only the UI
- `getColor?: typeof getColor` - the function to get the color of the log, the default function available by an import with the same name
- `separator?: string | RegExp | ((name: string) => Array<string>)` - the separator to split the name of the log for "state view", `.` by default
- `privatePrefix?: string` - the prefix which mark the log as private, `_` by default

## UI features

### Logs view

![Devtools logs view](devtools1.png)

1. **Filter form** - input RegExp will apply to the name of the log immediately, you can choose the type of the match mechanic: equal, not equal, highlight (color picker), off. By pressing "save" button the filter options will be saved in the filters group below.
2. **Filters group** - the list of saved filters, which you can change, also additional match mechanic "garbage" can be added, which will throw input logs forever, which is useful for better performance of many logs.
> "private" unremovable filter applies to private logs, which defines by `privatePrefix` option.
3. **Values search** allows you to filter the logs by the stringified payload matching.
4. **Actions** allows you to modify the behavior of the devtools:
- "clear lines" clears causes chain of transactional logs
- "clear logs" removes all collected logs
- "size" sets the amount of logs to be collected
- "inline preview" toggles the preview under log name
- "hover preview" toggles the preview by document icon before a log name hover
- "timestamps" toggles the timestamps lines of transactions (you need to restart the page to see the changes)
5. **Log name** row starts with chain highlight button, then payload preview button (could be hovered with the options or clicked to hold it), then log name.
6. **Log payload** row includes the payload of the log, if the relative option is enabled.
7. **Payload actions** (see description below)

### Payload inspector view

![Devtools payload inspector view](devtools2.png)

In the screenshot above we turn off the inline preview, clicked at the chain button of "issues.dataAtom" log, which paint the cause chain to other relative logs, and clicked on the document button of "issues.dataAtom" log to see the payload inspector view.

1. Name of the log (atom) and its payload (state).
2. List of the changes of this log.
3. **Payload action** buttons:
- **Convert to plain JSON** (useful for [atomized](https://www.reatom.dev/recipes/atomization/) state inspection)
- **Log** to the browser console
- **Copy** JSON to the clipboard
- **Open edit form** (will create additional log if will be applied)
- **Close** the inspector
Binary file added packages/devtools/devtools1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/devtools/devtools2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion packages/devtools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
],
"files": [
"/build",
"/package.json"
"/package.json",
"/devtools1.png",
"/devtools2.png"
],
"devDependencies": {
"@observablehq/inspector": "^5.0.0",
Expand Down
11 changes: 7 additions & 4 deletions packages/devtools/src/Graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ type Props = {
getColor: typeof getColor
width: Atom<string>
height: Atom<string>
initSize: number
}

export const Graph = ({ clientCtx, getColor, width, height }: Props) => {
export const Graph = ({ clientCtx, getColor, width, height, initSize }: Props) => {
const name = '_ReatomDevtools.Graph'

const list = reatomLinkedList(
Expand Down Expand Up @@ -211,7 +212,7 @@ export const Graph = ({ clientCtx, getColor, width, height }: Props) => {
const redrawLines = action((ctx) => lines.redraw(ctx, svg), `${name}.redrawLines`)

const filters = reatomFilters(
{ list: list as unknown as LinkedListAtom, clearLines: lines.clear, redrawLines },
{ list: list as unknown as LinkedListAtom, clearLines: lines.clear, redrawLines, initSize },
`${name}.filters`,
)
const valuesSearch = atom((ctx) => {
Expand All @@ -228,10 +229,12 @@ export const Graph = ({ clientCtx, getColor, width, height }: Props) => {
ctx.spy(height)
parseAtoms(ctx, filters)
await ctx.schedule(() => new Promise((r) => requestAnimationFrame(r)))
// TODO: the second one is required in Firefox
await ctx.schedule(() => new Promise((r) => requestAnimationFrame(r)))
return `${listEl.getBoundingClientRect().height}px`
}, `${name}.listHeight`).pipe(withDataAtom('0px')).dataAtom

const subscribe = () =>
// const subscribe = () =>
clientCtx.subscribe(async (logs) => {
// sort causes and insert only from this transaction
const insertTargets = new Set<AtomCache>()
Expand Down Expand Up @@ -322,7 +325,7 @@ export const Graph = ({ clientCtx, getColor, width, height }: Props) => {

const listEl = (
<ul
ref={subscribe}
// ref={subscribe}
css={`
padding: 0;
content-visibility: auto;
Expand Down
19 changes: 14 additions & 5 deletions packages/devtools/src/Graph/reatomFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ const initState: FiltersJSON = {
size: 1000,
list: [{ name: 'private', search: `(^_)|(\._)`, type: 'mismatch', color: DEFAULT_COLOR, default: true }],
}
const initSnapshot = JSON.stringify(initState)
const version = 'v24'

const FilterView = ({ id, filter, remove }: { id: string; filter: Filter; remove: Fn<[Ctx]> }) => (
Expand Down Expand Up @@ -167,17 +166,27 @@ export const reatomFilters = (
list,
clearLines,
redrawLines,
}: { list: LinkedListAtom; clearLines: Action<[], void>; redrawLines: Action<[], void> },
initSize,
}: {
list: LinkedListAtom
clearLines: Action<[], void>
redrawLines: Action<[], void>
initSize: number
},
name: string,
) => {
const KEY = name + version

try {
var snapshot: undefined | FiltersJSON = Filters.parse(JSON.parse(localStorage.getItem(KEY) || initSnapshot))
} catch {}
const snapshotString = localStorage.getItem(KEY)
const snapshotObject = snapshotString && JSON.parse(snapshotString)
var snapshot: undefined | FiltersJSON = Filters.parse(snapshotObject || { ...initState, size: initSize })
} catch {
snapshot = { ...initState, size: initSize }
}

const filters = reatomZod(Filters, {
initState: snapshot || initState,
initState: snapshot,
sync: (ctx) => {
redrawLines(ctx)
ctx.schedule(() => {
Expand Down
Loading

0 comments on commit 6315923

Please sign in to comment.