Skip to content

Instantly share code, notes, and snippets.

@kobzarvs
Created June 27, 2019 18:06
Show Gist options
  • Save kobzarvs/3bb5ba2e6fb5f5c8472bdd8e73447369 to your computer and use it in GitHub Desktop.
Save kobzarvs/3bb5ba2e6fb5f5c8472bdd8e73447369 to your computer and use it in GitHub Desktop.
import { useStore } from 'effector-react'
import * as effector from 'effector'
import { combine } from 'effector'
import { pathOr } from 'ramda'
import React, { useLayoutEffect, useRef } from 'react'
import ReactDOM from 'react-dom'
import { chromeDark, ObjectInspector, ObjectLabel } from 'react-inspector'
import { throttle } from 'lodash'
import './effector-addon.css'
const trackCreateStore = effector.createEvent('trackCreateStore')
const trackCreateEvent = effector.createEvent('trackCreateEvent')
const trackCreateEffect = effector.createEvent('trackCreateEffect')
export function createStore(...params) {
const store = effector.createStore(...params)
process.env.NODE_ENV === 'development' && trackCreateStore({
store,
params,
})
return store
}
export function createEvent(...params) {
// console.log('createEvent', params)
const event = effector.createEvent(...params)
const name = params[0]
const file = params[1].loc.file
const module = file.split('/').slice(-1)[0].split('.')[0]
process.env.NODE_ENV === 'development' && trackCreateEvent({
event,
name,
module,
})
return event
}
export function createEffect(...params) {
// console.log('createEffect', params)
const effect = effector.createEffect(...params)
process.env.NODE_ENV === 'development' && trackCreateEffect({
effect,
params,
})
return effect
}
const toggleVisibility = effector.createEvent('toggleVisibility')
const visibility = effector.createStore(true)
.on(toggleVisibility, state => !state)
const $winParams = effector.createStoreObject({
visibility,
})
const getNewPromise = () => new Promise((resolve) => resolver = resolve)
let resolver = null
let sync = Promise.resolve()
const $eventMap = effector.createStore({})
.on(trackCreateEvent, (state, { event, name, module }) => {
event.watch(async () => {
await sync
sync = getNewPromise()
debouncedTimeSlice()
addEvent({ type: 'event', module, name, fullname: `${module}.${name}` })
resolver && resolver()
})
return {
...state,
[module]: [
...pathOr([], [module], state),
`${module}.${name}`,
],
}
})
const $storeMap = effector.createStore({})
.on(trackCreateStore, (state, { store, params }) => {
store.updates.watch(async () => {
await sync
sync = getNewPromise()
debouncedTimeSlice()
addEvent({ type: 'store', name: store.shortName })
resolver && resolver()
})
return {
...state,
[store.shortName]: store,
}
})
const addEvent = effector.createEvent()
const eventCall = addEvent.filter({
fn: (params) => params.type === 'event',
})
const timeSlice = effector.createEvent()
const debouncedTimeSlice = throttle(timeSlice, 1000, { leading: true, trailing: false })
let eventCounter = 0
const clearStack = effector.createEvent()
const $eventCallStack = effector.createStore([])
.reset(clearStack)
.on(addEvent, (state, event) => {
if (event.type === 'store') {
const lastEvent = state.slice(-1)[0]
if (lastEvent) {
return [...state.slice(0, -1), { ...lastEvent, store: event.name }]
}
}
return [
...state.slice(-100),
{ ...event, index: event.type === 'event' ? ++eventCounter : 0 },
]
})
.on(timeSlice, (state) => [
...state,
{ type: 'time', time: Date.now() },
])
const $eventCalls = effector.createStore({})
.reset(clearStack)
.on(eventCall, (state, { module, name }) => {
return ({
...state,
[`${module}.${name}`]: pathOr(0, [`${module}.${name}`], state) + 1,
})
})
const toggleEventFilter = effector.createEvent()
const toggleEventGroupFilter = effector.createEvent()
const $eventFilter = effector.createStore({})
.reset(clearStack)
.on(trackCreateEvent, (state, { event, name, module }) => ({
...state,
[module]: {
checked: pathOr(true, [module, 'checked'], state),
data: {
...pathOr({}, [module, 'data'], state),
[`${module}.${name}`]: true,
},
},
}))
.on(toggleEventFilter, (state, name) => {
const module = name.split('.')[0]
const checked = !pathOr(true, [module, 'data', name], state)
const newState = {
...state,
[module]: {
// checked: moduleChecked,
data: {
...pathOr({}, [module, 'data'], state),
[name]: checked,
},
},
}
const moduleChecked = checked
? Object.values(pathOr({}, [module, 'data'], newState)).every(item => item)
: false
newState[module].checked = moduleChecked
return newState
})
.on(toggleEventGroupFilter, (state, module) => {
const checked = !pathOr(true, [module, 'checked'], state)
return ({
...state,
[module]: {
checked,
data: Object.keys(pathOr({}, [module, 'data'], state))
.reduce((acc, item) => Object.assign(acc, { [item]: checked }), {}),
},
})
})
const $filteredCallStack = combine($eventCallStack, $eventFilter, (eventCallStack, eventFilter) => {
return eventCallStack.filter(event => {
if (event.type !== 'event') return event
return eventFilter[event.module].data[event.fullname]
})
})
function formatDate(date) {
let diff = new Date() - date // the difference in milliseconds
if (diff < 1000) { // less than 1 second
return 'right now'
}
let sec = Math.floor(diff / 1000) // convert diff to seconds
if (sec < 60) {
return sec + ' sec. ago'
}
let min = Math.floor(diff / 60000) // convert diff to minutes
if (min < 60) {
return min + ' min. ago'
}
// format the date
// add leading zeroes to single-digit day/month/hours/minutes
let d = date
d = [
'0' + d.getDate(),
'0' + (d.getMonth() + 1),
'' + d.getFullYear(),
'0' + d.getHours(),
'0' + d.getMinutes(),
].map(component => component.slice(-2)) // take last 2 digits of every component
// join the components into date
return d.slice(0, 3).join('.') + ' ' + d.slice(3).join(':')
}
const storeNodeRenderer = ({ depth, name, data, isNonenumerable, expanded }) => {
switch (depth) {
case 0:
return <span style={{ color: 'rgb(232, 234, 246)' }}>Store list</span>
case 1:
return typeof data === 'object' || Array.isArray(data)
? <span style={{ color: '#9ecbe0' }}>{name}</span>
: <ObjectLabel
name={name}
data={data}
isNonenumerable={isNonenumerable}
/>
default:
return <ObjectLabel name={name} data={data} isNonenumerable={isNonenumerable} />
}
}
const eventNodeRenderer = (eventCalls, eventFilter) => ({ depth, name, data, isNonenumerable, expanded }) => {
switch (depth) {
case 0:
return <label style={{ color: 'rgb(232, 234, 246)' }}>
{name}
</label>
case 1: {
return (
<span style={{ color: '#9ecbe0', lineHeight: 1.2 }}>
<input
type="checkbox"
checked={pathOr(true, [name, 'checked'], eventFilter)}
onClick={e => {
e.preventDefault()
e.stopPropagation()
}}
onChange={() => toggleEventGroupFilter(name)}
style={{
height: 12,
width: 12,
marginLeft: 2,
marginRight: 5,
// float: 'left',
}}
/>
{name}
</span>
)
}
case 2: {
const splitPath = data.split('.')
const module = splitPath[0]
const eventName = splitPath.slice(-1)
return (
<label style={{ color: 'rgb(161, 198, 89)', lineHeight: 1.2 }}>
<input
type="checkbox"
checked={pathOr(true, [module, 'data', data], eventFilter)}
onChange={() => toggleEventFilter(data)}
style={{
height: 12,
width: 12,
marginRight: 5,
// float: 'left',
}}
/>
<span style={{ cursor: 'pointer', color: pathOr(0, [data], eventCalls) ? '#baf742' : 'inherit' }}>
{eventName}
</span>
{eventCalls[data] > 0 && (
<span style={{ color: 'rgb(232, 234, 246)', paddingLeft: 6 }}>
[
<span style={{ color: pathOr(0, [data], eventCalls) ? '#baf742' : 'inherit' }}>
{eventCalls[data]}
</span>
]
</span>
)}
</label>
)
}
default:
return null
}
}
const EventStackItemRenderer = ({ eventCallStack, item, index }) => {
switch (item.type) {
case 'store':
return (
<div style={{
borderTop: pathOr('', [index - 1, 'type'], eventCallStack) === 'event' ? '1px dotted #7777' : 'none',
textAlign: 'right',
color: '#e0e042',
padding: '0 10px',
}}
>
{item.name || '<NONAME>'}
</div>
)
case 'event':
return (
<div style={{
color: 'lightgray',
display: 'flex',
justifyContent: 'space-between',
marginTop: pathOr('', [index - 1, 'type'], eventCallStack) === 'store' ? 10 : 0,
padding: '0 5px 0 2px',
}}
>
<div>
<span style={{ color: '#789', marginRight: 5 }}>{item.index}.</span>
<span style={{ color: '#bbb' }}>{item.name || '<NONAME>'}</span>
</div>
<div style={{
flex: '1 0',
margin: '0 0 12px 10px',
borderBottom: item.store ? '1px dotted #7777' : 'none',
}}
/>
{item.store && (
<div style={{ marginRight: 10, marginLeft: -1, color: '#777' }}>
</div>
)}
<div style={{
textAlign: 'right',
color: '#e0e042',
}}
>
{item.store}
</div>
</div>
)
case 'time':
return null
return (
<div style={{ textAlign: 'center', color: '#9ecbe0', marginTop: 10, borderBottom: '1px dotted #7771' }}>
{formatDate(item.time)}
</div>
)
default:
return (
<div style={{ color: 'red', marginTop: 5 }}>
Unknown event
</div>
)
}
}
// const tick = effector.createEvent()
// const $refresher = effector.createStore(Date.now())
// .on(tick, state => Date.now())
//
// setInterval(tick, 5000)
export const DevToolWindow = () => {
const winParams = useStore($winParams)
const eventCallStack = useStore($filteredCallStack)
const eventCalls = useStore($eventCalls)
const storeMap = useStore($storeMap)
const eventMap = useStore($eventMap)
const storeList = Object.keys(storeMap)
const eventFilter = useStore($eventFilter)
// const refresher = useStore($refresher)
const ref = useRef(null)
useLayoutEffect(() => {
if (ref.current) {
ref.current.scrollTop = ref.current.scrollHeight
}
}, [eventCallStack[eventCallStack.length - 1]])
// if (!winParams.visibility) return null
const storeObject = storeList.reduce((acc, storeName) =>
Object.assign(acc, { [storeName]: storeMap[storeName].getState() }), {},
)
return (
<div
style={{
fontSize: 16,
position: 'absolute',
width: winParams.visibility ? '50%' : 0,
transition: 'width 50ms',
top: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgb(53, 59, 70)',
zIndex: 9999999,
boxShadow: '-4px 0 10px rgba(0, 0, 0, .35)',
overflow: 'auto',
color: 'rgb(232, 234, 246)',
}}
onMouseDown={e => e.stopPropagation()}
onMouseUp={e => e.stopPropagation()}
onClick={e => e.stopPropagation()}
>
<div style={{
padding: '5px 10px',
backgroundColor: 'rgba(0, 0, 0, .25)',
width: '100%',
fontSize: 20,
borderBottom: '1px solid rgba(255, 255, 255, .25)',
}}
>
Effector Inspector
</div>
<div style={{
display: 'flex',
padding: 5,
height: 'calc(100% - 42px)',
}}
>
<div style={{
flex: '0 0 40%',
display: 'flex',
flexDirection: 'column',
overflow: 'auto',
}}
>
<div style={{
height: 290,
marginBottom: 10,
overflow: 'auto',
border: '1px solid #555',
backgroundColor: '#2A2F3A',
userSelect: 'none',
}}
>
<ObjectInspector
nodeRenderer={eventNodeRenderer(eventCalls, eventFilter)}
expandLevel={1}
data={eventMap}
theme={{
...chromeDark,
BASE_FONT_SIZE: '14px',
TREENODE_FONT_SIZE: '14px',
OBJECT_NAME_COLOR: '#9ecbe0',
OBJECT_VALUE_STRING_COLOR: 'rgb(161, 198, 89)',
OBJECT_VALUE_BOOLEAN_COLOR: 'rgb(252, 109, 36)',
BASE_BACKGROUND_COLOR: '#2A2F3A',
}}
name='Event list'
/>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 5 }}>
<div>Events stack:</div>
<div className="button" onClick={clearStack}>
Clear
</div>
</div>
<div
ref={ref}
style={{
flex: '1 0 60%',
overflow: 'auto',
border: '1px solid #555',
backgroundColor: '#2A2F3A',
}}
>
{eventCallStack.map((item, index) => (
<EventStackItemRenderer key={index} index={index} item={item} eventCallStack={eventCallStack} />
))}
</div>
</div>
<div style={{
flex: '1 0',
overflow: 'auto',
marginLeft: 5,
border: '1px solid #555',
backgroundColor: '#2A2F3A',
padding: 5,
}}
>
<ObjectInspector
style={{ border: '5px solid red' }}
nodeRenderer={storeNodeRenderer}
expandLevel={1}
data={storeObject}
theme={{
...chromeDark,
BASE_FONT_SIZE: '14px',
TREENODE_FONT_SIZE: '14px',
OBJECT_NAME_COLOR: '#9ecbe0',
OBJECT_VALUE_STRING_COLOR: 'rgb(161, 198, 89)',
OBJECT_VALUE_BOOLEAN_COLOR: 'rgb(252, 109, 36)',
BASE_BACKGROUND_COLOR: '#2A2F3A',
}}
/>
</div>
</div>
</div>
)
}
export function bindEffectorInspectorHotKey({ alt = false, ctrl = false, shift = false, key = '`' } = {}) {
window.addEventListener('keydown', (e) => {
e.key === key && e.altKey === alt && e.ctrlKey === ctrl && e.shiftKey === shift && toggleVisibility()
})
}
export const effectorInspector = (hotKeyOptions) => {
bindEffectorInspectorHotKey(hotKeyOptions)
const devtool = document.createElement('div')
ReactDOM.render(<DevToolWindow />, devtool)
document.body.appendChild(devtool)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment