Skip to content

Instantly share code, notes, and snippets.

@sergeysova
Forked from kobzarvs/effector-addon.css
Created June 27, 2019 18:13
Show Gist options
  • Select an option

  • Save sergeysova/bad861533ec0f34b3abd9f1f7f33ecaa to your computer and use it in GitHub Desktop.

Select an option

Save sergeysova/bad861533ec0f34b3abd9f1f7f33ecaa to your computer and use it in GitHub Desktop.

Revisions

  1. @kobzarvs kobzarvs created this gist Jun 27, 2019.
    11 changes: 11 additions & 0 deletions effector-addon.css
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,11 @@
    .button {
    border: 1px solid #555;
    text-align: center;
    padding: 0 12px;
    color: #ddd;
    cursor: pointer;
    }

    .button:hover {
    background: rgba(255, 255, 255, .25)
    }
    537 changes: 537 additions & 0 deletions effector-addon.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,537 @@
    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)
    }