Skip to content

Commit

Permalink
feat(logger): devtools improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
krulod committed Oct 20, 2023
1 parent 5e3ed21 commit b9a14a5
Show file tree
Hide file tree
Showing 14 changed files with 803 additions and 645 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/logger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@reatom/persist": "^3.3.0",
"@reatom/persist-web-storage": "^3.2.3",
"@reatom/primitives": "^3.2.1",
"@reatom/web": "^3.5.1",
"stylerun": "^1.0.0"
},
"author": "artalar",
Expand Down
15 changes: 15 additions & 0 deletions packages/logger/src/devtools-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import styled from 'stylerun'

export const buttonStyles = styled('')`
border: none;
font: inherit;
font-weight: bolder;
background: #24243a;
color: white;
border-radius: 0.25rem;
border: 1px solid #c5c6de88;
padding: 0.25rem;
width: 100%;
`.styled(':hover')`
background-color: #3B3B4E;
`
122 changes: 122 additions & 0 deletions packages/logger/src/devtools-filter-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { atom, AtomCache, action, AtomMut } from '@reatom/core'
import { parseAtoms } from '@reatom/lens'
import { withLocalStorage } from '@reatom/persist-web-storage'
import { reatomArray } from '@reatom/primitives'

export type FilterColor = (typeof FilterColors)[number]
export const FilterColors = [
'red',
'green',
'blue',
'yellow',
'gray',
'black',
] as const

export type FilterAction = (typeof FilterActions)[number]
export const FilterActions = ['hide', ...FilterColors] as const

export type Filter = {
code: AtomMut<string>
action: AtomMut<FilterAction>
enabled: AtomMut<boolean>
}

const reatomFilter = (config: {
code: string
action: FilterAction
enabled: boolean
}): Filter => ({
code: atom(config.code),
action: atom(config.action),
enabled: atom(config.enabled),
})

export const filters = reatomArray([] as Filter[], 'filters').pipe(
withLocalStorage({
key: '@reatom/logger:filters',
toSnapshot: parseAtoms,
fromSnapshot: (ctx, snapshot = []) =>
(snapshot as any[]).map((config) => reatomFilter(config)),
}),
)

export const filterSplice = action(
(ctx, filter: Filter, ...replaceWith: Filter[]) => {
const index = ctx.get(filters).indexOf(filter)
filters(ctx, filters.toSpliced(ctx, index, 1, ...replaceWith))
},
'filterSplice',
)

export const filtersEnabledGet = atom(
(ctx) => ctx.spy(filters).some((filter) => ctx.spy(filter.enabled)),
'filtersEnabledGet',
)

export const filtersEnabledSet = action((ctx, next: boolean) => {
for (const filter of ctx.get(filters)) filter.enabled(ctx, next)
}, 'filtersEnabledSet')

export const filtersEnabledIndeterminate = atom((ctx) => {
let some = false
let every = true
for (const filter of ctx.spy(filters)) {
if (ctx.spy(filter.enabled)) {
some = true
} else {
every = false
}
}
return some && !every
}, 'filtersEnabledSet')

const filterFunctions = atom((ctx) => {
return ctx.spy(filters).map((filter) => {
// TODO execute in a worker
return new Function(
'log',
`var proto = log.proto; var name = proto.name; return ${ctx.spy(
filter.code,
)}`,
) as (node: AtomCache) => unknown
})
}, 'filterFunctions')

const filterRun = action((ctx, node: AtomCache) => {
const filtersState = ctx.get(filters)
return ctx.get(filterFunctions).map((fn, i) => {
try {
if (!fn(node)) return null
return ctx.get(filtersState[i]!.action)
} catch (error) {
// TODO report error in UI
console.error(error)
return null
}
})
})

export const getNodeHidden = action((ctx, node: AtomCache) => {
return filterRun(ctx, node).some((action) => action === 'hide')
})

export const getNodeColor = action((ctx, node: AtomCache) => {
return filterRun(ctx, node).find((action) => action !== 'hide') as
| FilterColor
| undefined
})
export const draftCode = atom('', 'draftCode')
export const draftAction = atom('hide' as FilterAction, 'draftAction')
export const draftCreate = action((ctx) => {
const filter = reatomFilter({
code: ctx.get(draftCode),
action: ctx.get(draftAction),
enabled: true,
})

filters(ctx, (prev) => [...prev, filter])

draftCode(ctx, '')
draftAction(ctx, 'hide')
}, 'filterDraftCreate')
181 changes: 181 additions & 0 deletions packages/logger/src/devtools-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import {
Action,
Atom,
AtomMaybe,
Ctx,
action,
atom,
isAtom,
} from '@reatom/core'
import { ctx, h } from '@reatom/jsx'
import styled from 'stylerun'
import * as model from './devtools-filter-model'
import { t } from './t'
import { match } from '@reatom/lens'
import { buttonStyles } from './devtools-button'
import { HTMLInputTypeAttribute } from 'react'
import { onConnect } from '@reatom/hooks'

export const FilterMenu = () => {
return atom((ctx) =>
t.div({
...menuStyles({}),
children: [
FilterMenuDraft(),
match((ctx) => ctx.spy(model.filters).length).truthy(
t.div(draftHrStyles({})),
),
...ctx.spy(model.filters).map((filter) => {
return FilterView({
checked: filter.enabled,
setChecked: filter.enabled,
checkedLabel: 'Filter enabled?',
action: filter.action,
setAction: filter.action,
code: filter.code,
setCode: filter.code,
buttonLabel: 'remove',
buttonClicked: action((ctx) => model.filterSplice(ctx, filter)),
})
}),
],
}),
)
}

const FilterMenuDraft = () => {
return FilterView({
checkedIndeterminate: model.filtersEnabledIndeterminate,
action: model.draftAction,
setAction: model.draftAction,
code: model.draftCode,
setCode: model.draftCode,
checked: model.filtersEnabledGet,
setChecked: model.filtersEnabledSet,
checkedLabel: 'Filters enabled?',
buttonLabel: 'create',
buttonClicked: model.draftCreate,
})
}

const FilterView = ({
checkedIndeterminate,
checked,
setChecked,
checkedLabel,
action: actionAtom,
setAction,
code,
setCode,
buttonLabel,
buttonClicked,
}: {
checkedIndeterminate?: Atom<boolean>
checked: AtomMaybe<boolean>
setChecked: (ctx: Ctx, next: boolean) => void
checkedLabel: string
action: Atom<model.FilterAction>
setAction: (ctx: Ctx, next: model.FilterAction) => void
code: AtomMaybe<string>
setCode: (ctx: Ctx, next: string) => void
buttonLabel: string
buttonClicked: (ctx: Ctx) => void
}) => {
const input = t.input({
...filterEnabledStyles({}),
type: 'checkbox',
checked: checked,
title: checkedLabel,
oninput: action((ctx, event) =>
setChecked(ctx, event.target.checked),
) as any,
}) as HTMLInputElement

const viewAtom = atom((ctx) =>
h('div', listItemStyles({}), [
input,
FilterActionView({
value: actionAtom,
setValue: action((ctx, action) => setAction(ctx, action)),
}),
t.input({
...filterCodeStyles({}),
type: 'string',
placeholder: 'filter code',
value: code,
min: 1,
minLength: 1,
size: 50,
// FIXME types
oninput: action((ctx, event) =>
setCode(ctx, event.target.value),
) as any,
}),
t.button({
...buttonStyles({}),
onclick: action((ctx) => buttonClicked(ctx)),
children: [buttonLabel],
}),
]),
)

if (checkedIndeterminate) {
onConnect(viewAtom, (ctx) => {
return ctx.subscribe(checkedIndeterminate, (state) => {
input.indeterminate = state
})
})
}

return viewAtom
}

const FilterActionView = ({
value,
setValue,
}: {
value: Atom<model.FilterAction>
setValue: (ctx: Ctx, action: model.FilterAction) => void
}) => {
return t.select({
...buttonStyles({}),
value,
onchange: action((ctx, event) =>
setValue(ctx, event.target.value as model.FilterAction),
) as any,
children: model.FilterActions.map((action) =>
t.option({
value: action,
selected: atom((ctx) => ctx.spy(value) === action),
children: [action],
}),
),
})
}

const menuStyles = styled('')`
display: flex;
flex-direction: column;
gap: calc(var(--rem) / 4);
`

const draftHrStyles = styled('')`
width: 100%;
border-bottom: 1px solid #c5c6de88;
margin: 0.25rem 0;
`

const listItemStyles = styled('')`
display: flex;
gap: 0.25rem;
`

const filterEnabledStyles = styled('')``

const filterCodeStyles = styled('')`
font: var(--mono_fonts);
`

const filterActionStyles = styled('')``

const filterButtonStyles = styled('')``
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { AtomCache, action, atom } from '@reatom/core'
import { caches } from './rld-model'
import { caches } from './devtools-model'
import {
FilterAction,
FilterColor,
filterColor,
filterHide,
getNodeColor,
getNodeHidden,
filters,
} from './rld-filter-model'
} from './devtools-filter-model'
import { parseAtoms } from '@reatom/lens'

export type Log = { cache: AtomCache; color?: FilterColor }
Expand All @@ -16,22 +15,20 @@ export const logSelected = atom(null as Log | null, 'logSelected')
export const logSelect = action((ctx, log: Log) => {
if (ctx.get(logSelected) === log) {
logSelected(ctx, null)
return
}
logSelected(ctx, log)
} else logSelected(ctx, log)
}, 'logSelect')

export const logGroups = atom((ctx) => {
const logs = ctx
.spy(caches)
.map((cache) => ({ cache, color: filterColor(ctx, cache) }))
.map((cache) => ({ cache, color: getNodeColor(ctx, cache) }))
const groups: LogGroup[] = []

// otherwise we lose reactivity because filters are used in actions
parseAtoms(ctx, filters)

for (const log of logs) {
if (filterHide(ctx, log.cache)) {
if (getNodeHidden(ctx, log.cache)) {
let group = groups.at(-1)
if (!group || !('hide' in group)) {
group = { hide: [] }
Expand Down
Loading

0 comments on commit b9a14a5

Please sign in to comment.