diff --git a/src/components/Dock/Dock.js b/src/components/Dock/Dock.js index 5d96633..1106b74 100644 --- a/src/components/Dock/Dock.js +++ b/src/components/Dock/Dock.js @@ -16,7 +16,7 @@ class Dock extends React.Component { node = null state = { - visible: false, + visible: true, width: 500, isResizing: false } diff --git a/src/components/Effect/Effect.js b/src/components/Effect/Effect.js index a6bb893..6afa5cf 100644 --- a/src/components/Effect/Effect.js +++ b/src/components/Effect/Effect.js @@ -18,21 +18,31 @@ class Effect extends React.Component { effectId = this.props.effect.effectId + highlightFilter = (text) => { + const highlight = this.props.filter + const parts = text.split(new RegExp(`(${highlight})`, 'gi')) + + return {parts.map((part, i) => + + {part} + ) + } + } + renderResult(status, result, error, winner) { return } render() { - const {effect} = this.props + const {effect, filter} = this.props const { status, result, error, winner } = effect - let nodes = [] let data if(effect.root) { nodes = nodes.concat( - renderFuncCall(effect.effect.saga, effect.effect.args), + renderFuncCall(effect.effect.saga, effect.effect.args, this.highlightFilter), this.renderResult(status, result, error) ) } @@ -40,7 +50,11 @@ class Effect extends React.Component { else if((data = asEffect.take(effect.effect))) { nodes = nodes.concat( renderEffectType('take'), - , + , this.renderResult(status, result, error, winner) ) } @@ -48,14 +62,19 @@ class Effect extends React.Component { else if((data = asEffect.put(effect.effect))) { nodes = nodes.concat( renderEffectType('put'), - + ) } else if((data = asEffect.call(effect.effect))) { nodes = nodes.concat( renderEffectType('call'), - renderFuncCall(data.fn, data.args), + renderFuncCall(data.fn, data.args, this.highlightFilter), this.renderResult(status, result, error, winner) ) } @@ -71,7 +90,7 @@ class Effect extends React.Component { else if((data = asEffect.fork(effect.effect))) { nodes = nodes.concat( renderEffectType('fork'), - renderFuncCall(data.fn, data.args), + renderFuncCall(data.fn, data.args, this.highlightFilter), this.renderResult(status, result, error, winner) ) } @@ -79,7 +98,12 @@ class Effect extends React.Component { else if((data = asEffect.join(effect.effect))) { nodes = nodes.concat( renderEffectType('join'), - , + , this.renderResult(status, result, error, winner) ) } @@ -87,7 +111,12 @@ class Effect extends React.Component { else if((data = asEffect.cancel(effect.effect))) { nodes = nodes.concat( renderEffectType('cancel'), - , + , ) } @@ -108,7 +137,7 @@ class Effect extends React.Component { else if((data = asEffect.select(effect.effect))) { nodes = nodes.concat( renderEffectType('select'), - renderFuncCall(data.selector, data.args), + renderFuncCall(data.selector, data.args, this.highlightFilter), this.renderResult(status, result, error, winner) ) } @@ -116,7 +145,11 @@ class Effect extends React.Component { else if((data = asEffect.actionChannel(effect.effect))) { nodes = nodes.concat( renderEffectType('actionChannel'), - , + , this.renderResult(status, result, error, winner) ) } @@ -172,13 +205,13 @@ function renderEffectType(type) { ) } -function renderFuncCall(fn, args) { +function renderFuncCall(fn, args, highlighter = val => val) { if(!args.length) { - return {fn.name}() + return {highlighter(fn.name)}() } return [ - {fn.name}(, + {highlighter(fn.name)}(, ...renderFuncArgs(args), ) ] @@ -197,6 +230,7 @@ function renderFuncArgs(args) { Effect.propTypes = { effect: PropTypes.object.isRequired, + filter: PropTypes.string.isRequired, } export default Effect diff --git a/src/components/JSValue/JSValue.js b/src/components/JSValue/JSValue.js index a5ea889..18328f9 100644 --- a/src/components/JSValue/JSValue.js +++ b/src/components/JSValue/JSValue.js @@ -12,7 +12,7 @@ import { const vnull = const vfuncKeyword = function -function renderValue(value, isIdentifier, label, onlyPrimitive) { +function renderValue(value, isIdentifier, label, onlyPrimitive, highlighter = val => val) { if(value === null || value === undefined) { return vnull @@ -25,9 +25,9 @@ function renderValue(value, isIdentifier, label, onlyPrimitive) { const type = typeof value if(type === 'string') { if(isIdentifier) { - return {value} + return {highlighter(value)} } else { - return '{value}' + return '{highlighter(value)}' } } if( @@ -35,14 +35,14 @@ function renderValue(value, isIdentifier, label, onlyPrimitive) { type === 'number' || type === 'boolean' ) { - return {String(value)} + return {highlighter(String(value))} } else if(type === 'function') { return ( {vfuncKeyword} - {value.name}() + {highlighter(value.name)}() ) } @@ -52,7 +52,7 @@ function renderValue(value, isIdentifier, label, onlyPrimitive) { {label} } return ( - + ) } } @@ -65,17 +65,18 @@ function getObjectSummary(obj) { ) } -function JSValue({value, isIdentifier, label}) { - return renderValue(value, isIdentifier, label, false) +function JSValue({value, isIdentifier, label, highlighter = val => val}) { + return renderValue(value, isIdentifier, label, false, highlighter) } JSValue.propTypes = { value: PropTypes.any, isIdentifier: PropTypes.bool, label: PropTypes.any, + highlighter: PropTypes.func, } -export function JSObject({data, renderLabel, preview, ignoreLabelClick}) { +export function JSObject({data, renderLabel, preview, ignoreLabelClick, highlighter = val => val}) { const keys = Object.keys(data) if(!keys.length) { return renderLabel ? renderLabel() : '{}' @@ -137,6 +138,7 @@ JSObject.propTypes = { renderLabel: PropTypes.func, preview: PropTypes.any, ignoreLabelClick: PropTypes.bool, + highlighter: PropTypes.func, } export default JSValue diff --git a/src/components/SagaValue/SagaValue.js b/src/components/SagaValue/SagaValue.js index a56de61..a5d1de8 100644 --- a/src/components/SagaValue/SagaValue.js +++ b/src/components/SagaValue/SagaValue.js @@ -3,7 +3,7 @@ import { is, CHANNEL_END } from 'redux-saga/utils' import JSValue from '../JSValue' import SagaRef from '../../containers/SagaRef' -export default function SagaValue({value, label, isIdentifier}) { +export default function SagaValue({value, label, isIdentifier, highlighter}) { if(is.channel(value)) { return {label || 'Channel'} } @@ -11,6 +11,6 @@ export default function SagaValue({value, label, isIdentifier}) { return } else { - return + return } } diff --git a/src/containers/EffectEntry/EffectEntry.js b/src/containers/EffectEntry/EffectEntry.js index 14c9126..589ef14 100644 --- a/src/containers/EffectEntry/EffectEntry.js +++ b/src/containers/EffectEntry/EffectEntry.js @@ -48,7 +48,7 @@ class EffectEntry extends React.Component { onUnpin = () => this.props.onUnpin(-1) render() { - const {effect, collapsed, pinned, hasChildren} = this.props + const {effect, collapsed, pinned, hasChildren, passFilter, filter} = this.props let pinNode if(!effect.root) { @@ -60,6 +60,7 @@ class EffectEntry extends React.Component { } return ( + passFilter ? @@ -70,7 +71,7 @@ class EffectEntry extends React.Component { /> - + { @@ -88,6 +89,8 @@ class EffectEntry extends React.Component { ) } + : + null ) } } @@ -99,6 +102,8 @@ EffectEntry.propTypes = { collapsed: PropTypes.bool.isRequired, onCollapse: PropTypes.func.isRequired, onSelect: PropTypes.func.isRequired, + filter: PropTypes.string.isRequired, + passFilter: PropTypes.bool.isRequired, // injected by Redux store effect: PropTypes.object.isRequired, hasChildren: PropTypes.bool.isRequired, @@ -109,7 +114,7 @@ export default connect( const effect = state.effectsById[effectId] return { effect, - hasChildren: state.effectsByParentId[effectId] + hasChildren: !!state.effectsByParentId[effectId] } } )(EffectEntry) diff --git a/src/containers/EffectList/EffectList.js b/src/containers/EffectList/EffectList.js index 4628072..7904705 100644 --- a/src/containers/EffectList/EffectList.js +++ b/src/containers/EffectList/EffectList.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import styled, { css } from 'styled-components' import { connect } from 'react-redux' +import { asEffect } from 'redux-saga/utils' import { matchCurrentAction } from '../../store/selectors' import { KEY_ARROW_DOWN, @@ -25,14 +26,51 @@ const cssMatchAction = css` margin-top: -1px; ` +const EFFECT_TYPES = ['take', 'put', 'fork', 'call', 'cps', 'join', 'cancel'] + class EffectList extends React.Component { state = { collapsedEffects: {} } + componentDidMount() { + this.props.setFilterOptions(EFFECT_TYPES, { allCaps: true }) + } + isCollapsed = effectId => { - return this.state.collapsedEffects[effectId] + return !!this.state.collapsedEffects[effectId] + } + + passFilter = effectId => { + const { filter } = this.props + + if (!filter.word && !filter.type) return true + + let data = {}, description + const effect = this.props.effectsById[effectId] + + if (effect.root) { + data = { type: 'root', name: effect.effect.saga.name } + } + + else { // Currently only filters those with string data.name + EFFECT_TYPES.forEach(type => { + if (description = asEffect[type](effect.effect)) { + data = { + type, + name: description.pattern + || description.channel + || description.action + || (description.fn && description.fn.name) + } + } + }) + } + + return typeof data.name === 'string' + && data.name.toLowerCase().includes(filter.word.toLowerCase()) + && (!filter.type || data.type === filter.type) } collapseEffect = (effectId, collapsed) => { @@ -121,6 +159,8 @@ class EffectList extends React.Component { selected={this.props.selectedEffectId === effectId} pinned={this.props.pinnedEffectId === effectId} collapsed={this.isCollapsed(effectId)} + filter={this.props.filter.word} + passFilter={this.passFilter(effectId)} onCollapse={this.collapseEffect} onPin={this.props.onPin} onUnpin={this.props.onUnpin} @@ -154,6 +194,8 @@ EffectList.propTypes = { selectedEffectId: PropTypes.number, onSelectionChange: PropTypes.func.isRequired, rootEffectIds: PropTypes.array.isRequired, + filter: PropTypes.object.isRequired, + setFilterOptions: PropTypes.func.isRequired, // Injected by redux effectsById: PropTypes.object.isRequired, effectsByParentId: PropTypes.object.isRequired, diff --git a/src/containers/EffectView/EffectView.js b/src/containers/EffectView/EffectView.js index d9ab52b..b4ac40a 100644 --- a/src/containers/EffectView/EffectView.js +++ b/src/containers/EffectView/EffectView.js @@ -64,7 +64,8 @@ class EffectView extends React.Component { render() { - const rootEffectIds = this.props.rootEffectIds + const {rootEffectIds, filter, setFilterOptions} = this.props + const selectedEffectId = this.state.selectedEffectId const pinnedEffectId = this.state.pinnedEffectId @@ -78,6 +79,8 @@ class EffectView extends React.Component { pinnedEffectId={pinnedEffectId} onPin={this.handlePin} onUnpin={this.handleUnpin} + filter={filter} + setFilterOptions={setFilterOptions} /> @@ -94,6 +97,8 @@ class EffectView extends React.Component { EffectView.propTypes = { rootEffectIds: PropTypes.array.isRequired, + filter: PropTypes.object.isRequired, + // setFilterOptions: PropTypes.func.isRequired, // Inject by Redux effectsById: PropTypes.object.isRequired } diff --git a/src/containers/SagaMonitorView/SagaMonitorView.js b/src/containers/SagaMonitorView/SagaMonitorView.js index 6a36f91..ca4ba16 100644 --- a/src/containers/SagaMonitorView/SagaMonitorView.js +++ b/src/containers/SagaMonitorView/SagaMonitorView.js @@ -9,7 +9,10 @@ import { SagaMonitorContainer, SagaMonitorHeader, SagaMonitorOption, - SagaMonitorBody + SagaMonitorBody, + FilterEffect, + FilterDropdown, + FilterOption } from './styles' const EFFECT_VIEW = 'Effects' @@ -22,7 +25,9 @@ class SagaMonitorView extends React.Component { state = { currentView: EFFECT_VIEW, - currentViewIndex: 0 + currentViewIndex: 0, + filter: { word: '', type: undefined }, + filterOptions: [] } viewHandlers = { @@ -30,10 +35,44 @@ class SagaMonitorView extends React.Component { [ACTION_VIEW]: () => this.setState({ currentView: ACTION_VIEW, currentViewIndex: 1 }) } + updateFilter = () => this.setState(({ filter }) => ( + { filter: { word: this.filterWord.value, type: this.filterType.value } } + )) + + setFilterOptions = (items, { allCaps }) => { + const options = items.map(item => { + let label, value + + if(typeof item === 'object') { + label = item.option || item.value + value = item.value + } + else if(typeof item === 'string') { + label = item + value = item + } + + if (allCaps) label = label.toUpperCase() + + return { label, value } + + }) + + this.setState({ filterOptions: options }) + } + + renderFilterOptions = () => this.state.filterOptions.map(option => { + return {option.label} + }) + renderCurrentView() { switch (this.state.currentView) { case EFFECT_VIEW: - return + return case ACTION_VIEW: return default: @@ -62,6 +101,18 @@ class SagaMonitorView extends React.Component { {this.renderViewOption(EFFECT_VIEW)} {this.renderViewOption(ACTION_VIEW)} + this.filterWord = filter} + placeholder="filter..." + /> + this.filterType = filter} + > + none + {this.renderFilterOptions()} +
diff --git a/src/containers/SagaMonitorView/styles.js b/src/containers/SagaMonitorView/styles.js index c391c19..a50b9d1 100644 --- a/src/containers/SagaMonitorView/styles.js +++ b/src/containers/SagaMonitorView/styles.js @@ -49,3 +49,20 @@ export const SagaMonitorBody = styled.section` bottom: 0; width: 100%; ` + +export const FilterEffect = styled.input` + width: 100px; + margin: 4px 10px; + + &::placeholder { + padding-left: 4px; + } +` + +export const FilterDropdown = styled.select` + width: 60px; +` + +export const FilterOption = styled.option` + width: 30px; +`