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 23, 2023
1 parent 3d2bf3e commit 1090ad7
Show file tree
Hide file tree
Showing 19 changed files with 916 additions and 665 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
16 changes: 16 additions & 0 deletions packages/logger/src/devtools-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import styled from 'stylerun'

export const buttonStyles = styled('')`
border: none;
font: inherit;
font-weight: bolder;
font-size: smaller;
background: #24243a;
color: white;
border-radius: 0.25rem;
border: 1px solid #c5c6de55;
padding: 4px;
/* width: 100%; */
`.styled(':hover')`
background-color: #3B3B4E;
`
10 changes: 10 additions & 0 deletions packages/logger/src/devtools-cursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { atom } from '@reatom/core'
import { ctx } from '@reatom/jsx'

export const cursorX = atom(-1, 'cursorX')
export const cursorY = atom(-1, 'cursorY')

globalThis.addEventListener?.('mousemove', (event) => {
cursorX(ctx, event.pageX)
cursorY(ctx, event.pageY)
})
122 changes: 122 additions & 0 deletions packages/logger/src/devtools-filters-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')
179 changes: 179 additions & 0 deletions packages/logger/src/devtools-filters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
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-filters-model'
import { t } from './t'
import { match } from '@reatom/lens'
import { buttonStyles } from './devtools-button'
import { onConnect } from '@reatom/hooks'
import { DevtoolsIconDelete, DevtoolsIconPlus } from './devtools-icon'

export const DevtoolsFilters = () => {
return atom((ctx) =>
t.div({
...filtersStyles({}),
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,
buttonChildren: DevtoolsIconDelete(),
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?',
buttonChildren: DevtoolsIconPlus(),
buttonClicked: model.draftCreate,
})
}

const FilterView = ({
checkedIndeterminate,
checked,
setChecked,
checkedLabel,
action: actionAtom,
setAction,
code,
setCode,
buttonChildren,
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
buttonChildren: Element
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: [buttonChildren],
}),
]),
)

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({}),
title: 'Filter action',
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 filtersStyles = styled('')`
display: flex;
flex-direction: column;
gap: 2px;
`

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

const listItemStyles = styled('')`
display: flex;
gap: 4px;
`

const filterEnabledStyles = styled('')``

const filterCodeStyles = styled('')`
font: var(--mono_fonts);
width: 100%;
`
19 changes: 19 additions & 0 deletions packages/logger/src/devtools-icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { AtomMaybe } from '@reatom/core'
import { t } from './t'

export const DevtoolsIcon = ({ path }: { path: AtomMaybe<string> }) =>
t.svg({
children: [
t.path({
d: path,
}),
],
})

export const DevtoolsIconPlus = () =>
DevtoolsIcon({ path: 'M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z' })

export const DevtoolsIconDelete = () =>
DevtoolsIcon({
path: 'M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19C6,20.1 6.9,21 8,21H16C17.1,21 18,20.1 18,19V7H6V19Z',
})
Loading

0 comments on commit 1090ad7

Please sign in to comment.