├── source ├── tests.js ├── index.js ├── components │ ├── index.js │ ├── ActionFilter.js │ ├── FilterableState.js │ └── FilterHeader.js ├── actions.js ├── reducers.js ├── utils.js └── FilterableLogMonitor.js ├── website ├── favicon.png ├── index.html ├── createAppStore.js ├── DevTools.js ├── index.js ├── Application.css ├── resources.js └── Application.js ├── circle.yml ├── .gitignore ├── webpack.config.umd.js ├── karma.conf.js ├── webpack.config.dev.js ├── webpack.config.demo.js ├── LICENSE ├── .babelrc ├── CHANGELOG.md ├── README.md ├── package.json └── CONTRIBUTING.md /source/tests.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/index.js: -------------------------------------------------------------------------------- 1 | export default from './FilterableLogMonitor' 2 | -------------------------------------------------------------------------------- /source/components/index.js: -------------------------------------------------------------------------------- 1 | export FilterableState from './FilterableState' 2 | -------------------------------------------------------------------------------- /website/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bvaughn/redux-devtools-filterable-log-monitor/HEAD/website/favicon.png -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | general: 2 | branches: 3 | ignore: 4 | - gh-pages 5 | machine: 6 | node: 7 | version: v5.1.0 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | 3 | # Logs 4 | /log/* 5 | !/log/.keep 6 | /tmp 7 | *.log 8 | 9 | # Dependency directory 10 | node_modules 11 | 12 | # Test stuff 13 | coverage 14 | 15 | # OS X 16 | .DS_Store 17 | 18 | # Misc 19 | node_modules 20 | npm-debug.log 21 | build 22 | -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | redux-devtools-filterable-log-monitor 5 | 6 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /website/createAppStore.js: -------------------------------------------------------------------------------- 1 | /** @flow */ 2 | import { compose, createStore } from 'redux' 3 | import { reducer } from './resources' 4 | import DevTools from './DevTools' 5 | 6 | export default function createAppStore (): Object { 7 | const finalCreateStore = compose( 8 | DevTools.instrument() 9 | )(createStore) 10 | 11 | return finalCreateStore(reducer) 12 | } 13 | -------------------------------------------------------------------------------- /website/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createDevTools } from 'redux-devtools' 3 | import DockMonitor from 'redux-devtools-dock-monitor' 4 | import FilterableLogMonitor from '../source' 5 | 6 | const DevTools = createDevTools( 7 | 10 | 11 | 12 | ) 13 | 14 | export default DevTools 15 | -------------------------------------------------------------------------------- /webpack.config.umd.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | devtool: 'source-map', 5 | entry: [ 6 | './source/index.js' 7 | ], 8 | output: { 9 | path: 'dist/umd', 10 | filename: '[name].js', 11 | libraryTarget: 'umd', 12 | library: 'redux-devtools-filterable-log-monitor' 13 | }, 14 | externals: { 15 | react: { 16 | commonjs: 'react', 17 | commonjs2: 'react', 18 | amd: 'react', 19 | root: 'React' 20 | } 21 | }, 22 | plugins: [ 23 | ], 24 | module: { 25 | loaders: [ 26 | { 27 | test: /\.js$/, 28 | loaders: ['babel'], 29 | include: path.join(__dirname, 'source') 30 | } 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /website/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the entray point to the documentation/demo/test harness site for redux-search. 3 | * This target is published to the root of the `gh-pages` branch. 4 | * @flow 5 | */ 6 | import createAppStore from './createAppStore' 7 | import { Provider } from 'react-redux' 8 | import { render } from 'react-dom' 9 | import Application from './Application' 10 | import React from 'react' 11 | import DevTools from './DevTools' 12 | 13 | const store = createAppStore() 14 | 15 | render(( 16 |
17 | 18 |
19 | 20 | 21 |
22 |
23 |
24 | ), 25 | document.getElementById('root') 26 | ) 27 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | browsers: ['PhantomJS'], 4 | frameworks: ['jasmine'], 5 | files: ['source/tests.js'], 6 | preprocessors: { 7 | 'source/tests.js': ['webpack', 'sourcemap'] 8 | }, 9 | junitReporter: { 10 | outputDir: (process.env.CIRCLE_TEST_REPORTS || 'public') + '/karma', 11 | suite: 'karma' 12 | }, 13 | singleRun: true, 14 | plugins: [ 15 | require('karma-jasmine'), 16 | require('karma-webpack'), 17 | require('karma-spec-reporter'), 18 | require('karma-junit-reporter'), 19 | require('karma-sourcemap-loader'), 20 | require('karma-phantomjs-launcher') 21 | ], 22 | webpack: require('./webpack.config.dev') 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | 5 | module.exports = { 6 | devtool: 'eval', 7 | entry: [ 8 | 'babel-polyfill', 9 | './website/index.js' 10 | ], 11 | output: { 12 | path: 'build', 13 | filename: '/static/[name].js' 14 | }, 15 | plugins: [ 16 | new HtmlWebpackPlugin({ 17 | filename: 'index.html', 18 | inject: true, 19 | template: './website/index.html' 20 | }), 21 | new webpack.NoErrorsPlugin() 22 | ], 23 | module: { 24 | loaders: [ 25 | { 26 | test: /\.js$/, 27 | loader: 'babel', 28 | exclude: path.join(__dirname, 'node_modules') 29 | }, 30 | { 31 | test: /\.css$/, 32 | loaders: ['style', 'css?modules&importLoaders=1', 'cssnext'], 33 | exclude: path.join(__dirname, 'node_modules') 34 | } 35 | ] 36 | }, 37 | devServer: { 38 | contentBase: 'build', 39 | port: 3333 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /webpack.config.demo.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | 5 | module.exports = { 6 | devtool: 'source-map', 7 | entry: [ 8 | 'babel-polyfill', 9 | './website/index.js' 10 | ], 11 | output: { 12 | path: 'build', 13 | filename: 'static/[name].js' 14 | }, 15 | plugins: [ 16 | new HtmlWebpackPlugin({ 17 | filename: 'index.html', 18 | inject: true, 19 | template: './website/index.html' 20 | }), 21 | new webpack.DefinePlugin({ 22 | 'process.env': { 23 | 'NODE_ENV': JSON.stringify('production') 24 | } 25 | }) 26 | ], 27 | module: { 28 | loaders: [ 29 | { 30 | test: /\.js$/, 31 | loader: 'babel', 32 | exclude: path.join(__dirname, 'node_modules') 33 | }, 34 | { 35 | test: /\.css$/, 36 | loaders: ['style', 'css?modules&importLoaders=1', 'cssnext'], 37 | exclude: path.join(__dirname, 'node_modules') 38 | } 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /website/Application.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-size: 14px; 4 | line-height: 1.5; 5 | font-weight: 300; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | *, 11 | *:before, 12 | *:after { 13 | box-sizing: border-box; 14 | } 15 | 16 | a { 17 | color: #2196f3; 18 | text-decoration: none; 19 | } 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | .Application { 25 | width: 70%; 26 | } 27 | 28 | .Button { 29 | display: inline-block; 30 | padding: .5em .75em; 31 | border: none; 32 | border-radius: .35em; 33 | font-size: .85em; 34 | font-weight: bold; 35 | background-color: #2A2F3A; 36 | color: white; 37 | margin-right: .5em; 38 | margin-bottom: .5em; 39 | cursor: pointer; 40 | } 41 | .Button:hover { 42 | background-color: #000000; 43 | } 44 | 45 | .Label{ 46 | margin-right: .5em; 47 | } 48 | 49 | .update { 50 | color: #6FB3D2; 51 | margin-right: .5em; 52 | } 53 | .array { 54 | color: #D381C3; 55 | } 56 | .object { 57 | color: #A1C659; 58 | } 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Treasure Data 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": { 4 | "presets": [ 5 | "es2015", 6 | "react", 7 | "stage-0" 8 | ] 9 | }, 10 | "development": { 11 | "plugins": [ 12 | "typecheck", 13 | [ 14 | "react-transform", 15 | { 16 | "transforms": [ 17 | { 18 | "transform": "react-transform-hmr", 19 | "imports": [ 20 | "react" 21 | ], 22 | "locals": [ 23 | "module" 24 | ] 25 | }, 26 | { 27 | "transform": "react-transform-catch-errors", 28 | "imports": [ 29 | "react", 30 | "redbox-react" 31 | ] 32 | } 33 | ] 34 | } 35 | ] 36 | ], 37 | "presets": [ 38 | "es2015", 39 | "react", 40 | "stage-0" 41 | ] 42 | }, 43 | "es": { 44 | "presets": [ 45 | "es2015-rollup", 46 | "react", 47 | "stage-0" 48 | ] 49 | }, 50 | "production": { 51 | "comments": false, 52 | "plugins": [ 53 | "transform-react-inline-elements" 54 | ], 55 | "presets": [ 56 | "es2015", 57 | "react", 58 | "stage-0" 59 | ] 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /source/actions.js: -------------------------------------------------------------------------------- 1 | export const ADD_ACTION_METADATA = '@@redux-devtools-filterable-log-monitor/ADD_ACTION_METADATA' 2 | export const SET_ACTION_FILTER_BY_TEXT = '@@redux-devtools-filterable-log-monitor/SET_ACTION_FILTER_BY_TEXT' 3 | export const SET_FILTER_BY_KEYS = '@@redux-devtools-filterable-log-monitor/SET_FILTER_BY_KEYS' 4 | export const SET_FILTER_BY_VALUES = '@@redux-devtools-filterable-log-monitor/SET_FILTER_BY_VALUES' 5 | export const SET_FILTER_TEXT = '@@redux-devtools-filterable-log-monitor/SET_FILTER_TEXT' 6 | export const TOGGLE_EXPANDED = '@@redux-devtools-filterable-log-monitor/TOGGLE_EXPANDED' 7 | 8 | export function addActionMetadata ({ action, actionId, appState }) { 9 | return { 10 | type: ADD_ACTION_METADATA, 11 | action, 12 | actionId, 13 | appState, 14 | time: new Date() 15 | } 16 | } 17 | 18 | export function setActionFilterText ({ actionFilterText }) { 19 | return { 20 | type: SET_ACTION_FILTER_BY_TEXT, 21 | actionFilterText 22 | } 23 | } 24 | 25 | export function setFilterByKeys ({ actionId, filterByKeys }) { 26 | return { 27 | type: SET_FILTER_BY_KEYS, 28 | actionId, 29 | filterByKeys 30 | } 31 | } 32 | 33 | export function setFilterByValues ({ actionId, filterByValues }) { 34 | return { 35 | type: SET_FILTER_BY_VALUES, 36 | actionId, 37 | filterByValues 38 | } 39 | } 40 | 41 | export function setFilterText ({ actionId, filterText }) { 42 | return { 43 | type: SET_FILTER_TEXT, 44 | actionId, 45 | filterText 46 | } 47 | } 48 | 49 | export function setExpanded ({ actionId, expanded }) { 50 | return { 51 | type: TOGGLE_EXPANDED, 52 | actionId, 53 | expanded 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /source/components/ActionFilter.js: -------------------------------------------------------------------------------- 1 | /** @flow */ 2 | import React, { Component } from 'react' 3 | import PropTypes from 'prop-types' 4 | import debounce from 'lodash.debounce' 5 | import { setActionFilterText } from '../actions' 6 | 7 | const DEBOUNCE_TIME = 250 8 | 9 | export default class ActionFilter extends Component { 10 | static propTypes = { 11 | actionFilterText: PropTypes.string, 12 | dispatch: PropTypes.func.isRequired, 13 | theme: PropTypes.object.isRequired 14 | } 15 | 16 | constructor (props) { 17 | super(props) 18 | 19 | this._debouncedOnActionFilterTextChange = debounce( 20 | this._onActionFilterTextChange.bind(this), 21 | DEBOUNCE_TIME 22 | ) 23 | } 24 | 25 | render () { 26 | const { 27 | actionFilterText, 28 | theme 29 | } = this.props 30 | 31 | return ( 32 |
44 | 60 |
61 | ) 62 | } 63 | 64 | _onActionFilterTextChange () { 65 | const actionFilterText = this.refs.input.value 66 | const { dispatch } = this.props 67 | 68 | dispatch(setActionFilterText({ 69 | actionFilterText 70 | })) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /website/resources.js: -------------------------------------------------------------------------------- 1 | /** @flow */ 2 | import faker from 'faker' 3 | import Immutable from 'immutable' 4 | 5 | export const State = Immutable.Record({ 6 | array: [], 7 | basicTypes: { 8 | array: ['a', 1], 9 | boolean: true, 10 | function: function abc () { 11 | console.log('message') 12 | }, 13 | null: null, 14 | number: 1.2, 15 | string: 'I like strings', 16 | undefined: undefined 17 | }, 18 | list: Immutable.List(), 19 | map: Immutable.Map(), 20 | object: {} 21 | }) 22 | 23 | function createRandomCards () { 24 | const results = [] 25 | for (var i = 0; i < Math.random() * 10; i++) { 26 | results.push({ 27 | ...faker.helpers.createCard(), 28 | uuid: faker.random.uuid() 29 | }) 30 | } 31 | return results 32 | } 33 | 34 | export const UPDATE_ARRAY = 'UPDATE_ARRAY' 35 | export const UPDATE_LIST = 'UPDATE_LIST' 36 | export const UPDATE_MAP = 'UPDATE_MAP' 37 | export const UPDATE_OBJECT = 'UPDATE_OBJECT' 38 | 39 | export const actions = { 40 | udpateArray () { 41 | return { 42 | type: UPDATE_ARRAY, 43 | payload: createRandomCards() 44 | } 45 | }, 46 | udpateList () { 47 | return { 48 | type: UPDATE_LIST, 49 | payload: createRandomCards() 50 | } 51 | }, 52 | udpateMap () { 53 | return { 54 | type: UPDATE_MAP, 55 | payload: createRandomCards() 56 | } 57 | }, 58 | udpateObject () { 59 | return { 60 | type: UPDATE_OBJECT, 61 | payload: createRandomCards() 62 | } 63 | } 64 | } 65 | 66 | export const actionHandlers = { 67 | [UPDATE_ARRAY] (state, { payload }): State { 68 | return state.updateIn(['array'], array => { 69 | Array.prototype.push.apply(array, payload) 70 | return array 71 | }) 72 | }, 73 | [UPDATE_LIST] (state, { payload }): State { 74 | return state.updateIn(['list'], list => list.push(...payload)) 75 | }, 76 | [UPDATE_MAP] (state, { payload }): State { 77 | return state.mergeIn(['map'], { ...payload }) 78 | }, 79 | [UPDATE_OBJECT] (state, { payload }): State { 80 | return state.mergeIn(['object'], { ...payload }) 81 | } 82 | } 83 | 84 | export function reducer (state = new State(), action: Object): State { 85 | const { type } = action 86 | if (type in actionHandlers) { 87 | return actionHandlers[type](state, action) 88 | } else { 89 | return state 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | #### 0.8.1 4 | Bugfix to account for API changes in `react-json-tree` (see [PR #41](https://github.com/bvaughn/redux-devtools-filterable-log-monitor/pull/41)). 5 | 6 | #### 0.8.0 7 | Updated `peerDependencies` and `dependencies` (only where required) to support React 16. 8 | 9 | #### 0.7.0 10 | Added snap-to-botom behavior to log monitor so that the most recent actions remain visible. 11 | Thanks to [@edtoken](https://github.com/edtoken) for this contribution via PR #36. 12 | 13 | #### 0.6.8 14 | Updated `PropTypes` to use new, separate 'prop-type' package. 15 | 16 | #### 0.6.7 17 | Pruned unnecessary files from NPM bundle. 18 | 19 | #### 0.6.6 20 | Bumped react-highlighter and react-json-tree versions to fix React.spread warning. 21 | 22 | #### 0.6.5 23 | Updated `react-json-tree` dependency to fix React version warning. 24 | 25 | #### 0.6.4 26 | Fixed a React 15 `style` warning. 27 | 28 | #### 0.6.3 29 | Updated React dependency to support both `^0.14.0` and `^15.0.0`. 30 | 31 | #### 0.6.2 32 | Upgraded to Babel 6 and split UMD, ES6, and CommonJS builds. 33 | Removed `react-pure-render` and `date-formate-lite` dependencies. 34 | 35 | #### 0.6.1 36 | Fixed null pointer error in debounce input change handler. 37 | 38 | ## 0.6.0 39 | Added (filterable) actions to log monitor in addition to state. 40 | 41 | #### 0.5.2 42 | Updated match highlighting logic to stay in sync with changes in recent react-json-tree 0.5.1 release. 43 | 44 | #### 0.5.1 45 | Updated to `react-highlighter` version 0.2.3 instead of forked build now that custom match style is supported. 46 | 47 | ## 0.5.0 48 | Added visual highlighting for substring matches within keys/values. 49 | Collapse headers by default so that new actions load more efficiently. 50 | Moved styles from CSS files to inline to better support out-of-the-box Browserify usage. 51 | Removed dependency on `Object.values` to simplify external dependencies. 52 | 53 | ## 0.4.0 54 | Added created-at time to action-type header to assist with debugging. 55 | Also added media queries to help with smaller browser sizes. 56 | 57 | ## 0.3.0 58 | Added top-level filter for actions, allowing them to be filtered by type. 59 | 60 | ## 0.2.0 61 | Added action-type to header to each monitor entry. 62 | Filter input is hidden unless at least one filter-by property is selected. 63 | Actions are no longer reverse-sorted in order to be more like the other monitors. 64 | 65 | ## 0.1.0 66 | Initial release. 67 | -------------------------------------------------------------------------------- /source/reducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_ACTION_METADATA, 3 | SET_ACTION_FILTER_BY_TEXT, 4 | SET_FILTER_BY_KEYS, 5 | SET_FILTER_BY_VALUES, 6 | SET_FILTER_TEXT, 7 | TOGGLE_EXPANDED 8 | } from './actions' 9 | import { getFilteredNodes } from './utils' 10 | 11 | const State = () => ({ 12 | actionIdToDatumMap: {} 13 | }) 14 | 15 | const reducers = { 16 | [ADD_ACTION_METADATA] (state, datum) { 17 | const { action, actionId, appState, time } = datum 18 | return updateAction(state, actionId, { action, appState, time }) 19 | }, 20 | 21 | [SET_ACTION_FILTER_BY_TEXT] (state, datum) { 22 | const { actionFilterText } = datum 23 | return { 24 | ...state, 25 | actionFilterText 26 | } 27 | }, 28 | 29 | [SET_FILTER_BY_KEYS] (state, datum) { 30 | const { actionId, filterByKeys } = datum 31 | return updateAction(state, actionId, { filterByKeys }) 32 | }, 33 | 34 | [SET_FILTER_BY_VALUES] (state, datum) { 35 | const { actionId, filterByValues } = datum 36 | return updateAction(state, actionId, { filterByValues }) 37 | }, 38 | 39 | [SET_FILTER_TEXT] (state, datum) { 40 | const { actionId, filterText } = datum 41 | return updateAction(state, actionId, { filterText }) 42 | }, 43 | 44 | [TOGGLE_EXPANDED] (state, datum) { 45 | const { actionId, expanded } = datum 46 | return updateAction(state, actionId, { expanded }, false) 47 | } 48 | } 49 | 50 | export default function reducer (props, state = new State(), datum) { 51 | return datum.type in reducers 52 | ? reducers[datum.type](state, datum, props) 53 | : state 54 | } 55 | 56 | function updateAction (state, actionId, props, updateFilter = true) { 57 | if (!state.actionIdToDatumMap[actionId]) { 58 | state.actionIdToDatumMap[actionId] = { 59 | action: {}, 60 | appState: {}, 61 | expanded: false, 62 | filterByKeys: true, 63 | filterByValues: true, 64 | filteredActions: {}, 65 | filteredState: {}, 66 | filterText: '', 67 | time: null 68 | } 69 | } 70 | 71 | state.actionIdToDatumMap[actionId] = { 72 | ...state.actionIdToDatumMap[actionId], 73 | ...props 74 | } 75 | 76 | const datum = state.actionIdToDatumMap[actionId] 77 | 78 | if (updateFilter) { 79 | datum.filteredActions = getFilteredNodes({ 80 | data: datum.action, 81 | ...datum 82 | }) 83 | datum.filteredState = getFilteredNodes({ 84 | data: datum.appState, 85 | ...datum 86 | }) 87 | } 88 | 89 | return state 90 | } 91 | -------------------------------------------------------------------------------- /source/components/FilterableState.js: -------------------------------------------------------------------------------- 1 | /** @flow */ 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | import JSONTree from 'react-json-tree' 5 | import FilterHeader from './FilterHeader' 6 | import Highlighter from 'react-highlighter' 7 | import { createRegExpFromFilterText } from '../utils' 8 | 9 | function highlightMatches (filterText, value) { 10 | return ( 11 | 17 | {value} 18 | 19 | ) 20 | } 21 | 22 | FilterableState.propTypes = { 23 | action: PropTypes.object.isRequired, 24 | actionId: PropTypes.any.isRequired, 25 | dispatch: PropTypes.func.isRequired, 26 | monitorStateAction: PropTypes.object.isRequired, 27 | theme: PropTypes.object.isRequired 28 | } 29 | 30 | export default function FilterableState ({ 31 | action, 32 | actionId, 33 | dispatch, 34 | monitorStateAction, 35 | theme 36 | }) { 37 | const { 38 | expanded, 39 | filterByKeys, 40 | filterByValues, 41 | filteredActions, 42 | filteredState, 43 | filterText 44 | } = monitorStateAction 45 | 46 | const labelRenderer = filterByKeys && filterText 47 | ? value => highlightMatches(filterText, value[0]) 48 | : value => value[0] 49 | 50 | const valueRenderer = filterByValues && filterText 51 | ? (value, nodeType) => highlightMatches(filterText, value) 52 | : value => value 53 | 54 | const data = [ 55 | { title: 'Action', source: filteredActions }, 56 | { title: 'State', source: filteredState } 57 | ] 58 | 59 | return ( 60 |
65 | 72 | {expanded && data.map((data, index) => ( 73 |
74 |
83 | {data.title} 84 |
85 | 98 |
99 | ))} 100 |
101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | redux devtools filterable log monitor logo 2 | ========================= 3 | 4 | ![NPM version](https://img.shields.io/npm/v/redux-devtools-filterable-log-monitor.svg) 5 | ![NPM license](https://img.shields.io/npm/l/redux-devtools-filterable-log-monitor.svg) 6 | ![NPM total downloads](https://img.shields.io/npm/dt/redux-devtools-filterable-log-monitor.svg) 7 | ![NPM monthly downloads](https://img.shields.io/npm/dm/redux-devtools-filterable-log-monitor.svg) 8 | 9 | Filterable tree view monitor for [Redux DevTools](https://github.com/gaearon/redux-devtools). 10 | 11 | Actions are collapsed by default but they can be expanded by clicking on the action type. Strings and regular expressions can be used to filter actions by type as well as to filter the state tree by nodes or values. 12 | 13 | Check out the [demo application here](https://bvaughn.github.io/redux-devtools-filterable-log-monitor). 14 | 15 | Demo GIF 16 | 17 | Installation 18 | ------------ 19 | 20 | ``` 21 | npm install --save-dev redux-devtools-filterable-log-monitor 22 | ``` 23 | 24 | Usage 25 | ------------ 26 | 27 | The `FilterableLogMonitor` is intended for use within the [`DockMonitor`](https://github.com/gaearon/redux-devtools-dock-monitor). You can configure your app to use these monitors like so: 28 | 29 | ```js 30 | import React from 'react' 31 | import { createDevTools } from 'redux-devtools' 32 | import DockMonitor from 'redux-devtools-dock-monitor' 33 | import FilterableLogMonitor from '../src' 34 | 35 | const DevTools = createDevTools( 36 | 38 | 39 | 40 | ) 41 | 42 | export default DevTools 43 | 44 | ``` 45 | 46 | Then you can render `` to any place inside app or even into a separate popup window. 47 | 48 | [Read how to start using Redux DevTools.](https://github.com/gaearon/redux-devtools) 49 | 50 | Features 51 | ------------ 52 | 53 | Every action is displayed in the log. Use the filter input to quickly and easily locate nodes in your store that are deeply nested. You can search using regular expressions or simple strings. Select whether the search should match keys, values, or either using the checkboxes above the filter input. 54 | 55 | Contributions 56 | ------------ 57 | 58 | Use [GitHub issues](https://github.com/bvaughn/redux-devtools-filterable-log-monitor/issues) for requests. 59 | 60 | I actively welcome pull requests; learn how to [contribute](https://github.com/bvaughn/react-virtualized/blob/master/CONTRIBUTING.md). 61 | 62 | Changelog 63 | --------- 64 | 65 | Changes are tracked in the [changelog](https://github.com/bvaughn/redux-devtools-filterable-log-monitor/blob/master/CHANGELOG.md). 66 | 67 | License 68 | --------- 69 | 70 | *react-virtualized* is available under the MIT License. 71 | -------------------------------------------------------------------------------- /source/utils.js: -------------------------------------------------------------------------------- 1 | function isImmutable (data) { 2 | return data && data.toJS instanceof Function 3 | } 4 | 5 | function searchKeys (key, node, regExp) { 6 | if (key.match(regExp)) { 7 | return true 8 | } else if (node && typeof node === 'object') { 9 | if (isImmutable(node)) { 10 | node = node.toJS() 11 | } 12 | 13 | return Object.keys(node).some(nestedKey => searchKeys(nestedKey, node[nestedKey], regExp)) 14 | } 15 | } 16 | 17 | function searchValues (node, regExp) { 18 | if (typeof node === 'string' && node.match(regExp)) { 19 | return true 20 | } else if (node && typeof node === 'object') { 21 | if (isImmutable(node)) { 22 | node = node.toJS() 23 | } 24 | 25 | for (var key in node) { 26 | if (searchValues(node[key], regExp)) { 27 | return true 28 | } 29 | } 30 | 31 | return false 32 | } 33 | } 34 | 35 | function trimTree (node, filterText, searchFunction) { 36 | let trimmed 37 | 38 | if (isImmutable(node)) { 39 | trimmed = node.toJS() 40 | } else { 41 | trimmed = node instanceof Array 42 | ? [ ...node ] 43 | : { ...node } 44 | } 45 | 46 | if (filterText) { 47 | for (var key in trimmed) { 48 | if (!searchFunction(key, trimmed[key], filterText)) { 49 | delete trimmed[key] 50 | } else if (typeof trimmed[key] === 'object') { 51 | trimmed[key] = trimTree(trimmed[key], filterText, searchFunction) 52 | } 53 | } 54 | } 55 | 56 | return trimmed 57 | } 58 | 59 | export function createRegExpFromFilterText (filterText) { 60 | if (filterText.match(/^\/.+\/[a-z]*$/)) { 61 | const pieces = filterText.split('/') 62 | pieces.shift() // First item is always empty 63 | const flags = pieces.pop() // Last item contains optional) flags 64 | try { 65 | return new RegExp(pieces.join('/'), flags) 66 | } catch (error) { 67 | console.error(error) 68 | return new RegExp() 69 | } 70 | } else { 71 | return new RegExp(filterText) 72 | } 73 | } 74 | 75 | export function getFilteredNodes ({ 76 | data = {}, 77 | filterByKeys, 78 | filterByValues, 79 | filterText = '' 80 | }) { 81 | if (!filterByKeys && !filterByValues || !filterText) { 82 | return data 83 | } 84 | 85 | const regExp = createRegExpFromFilterText(filterText) 86 | const keySearcher = filterByKeys ? searchKeys : () => false 87 | const valueSearcher = filterByValues ? searchValues : () => false 88 | const searchFunction = (key, value, regExp) => keySearcher(key, value, regExp) || valueSearcher(value, regExp) 89 | 90 | return trimTree(data, regExp, searchFunction) 91 | } 92 | 93 | export function formatDateAsTime (date) { 94 | const hours = date.getHours() 95 | const minutes = date.getMinutes() 96 | const seconds = date.getSeconds() 97 | const ampm = hours >= 12 ? 'pm' : 'am' 98 | 99 | const formattedHours = hours % 12 ? hours % 12 : 12 100 | const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes 101 | const formattedSeconds = seconds < 10 ? `0${seconds}` : seconds 102 | 103 | return `${formattedHours}:${formattedMinutes}:${formattedSeconds} ${ampm}` 104 | } 105 | -------------------------------------------------------------------------------- /website/Application.js: -------------------------------------------------------------------------------- 1 | /** @flow */ 2 | import { actions } from './resources' 3 | import { connect } from 'react-redux' 4 | import React, { Component } from 'react' 5 | import PropTypes from 'prop-types' 6 | import styles from './Application.css' 7 | 8 | const timeoutUpdater = function (enable, updateMethodsList) { 9 | if (!enable) { 10 | return clearTimeout(timeoutUpdater.timerId) 11 | } 12 | 13 | const updateMethod = updateMethodsList[Math.floor(Math.random() * updateMethodsList.length)] 14 | updateMethod() 15 | 16 | timeoutUpdater.timerId = setTimeout(() => { 17 | timeoutUpdater(enable, updateMethodsList) 18 | }, 1000) 19 | } 20 | 21 | class Application extends Component { 22 | static propTypes = { 23 | udpateArray: PropTypes.func.isRequired, 24 | udpateList: PropTypes.func.isRequired, 25 | udpateMap: PropTypes.func.isRequired, 26 | udpateObject: PropTypes.func.isRequired 27 | } 28 | 29 | componentDidMount () { 30 | const { udpateMap } = this.props 31 | 32 | udpateMap() 33 | } 34 | 35 | render () { 36 | const { udpateArray, udpateList, udpateMap, udpateObject } = this.props 37 | const autoUpdateMethods = [ udpateArray, udpateList, udpateMap, udpateObject ] 38 | 39 | return ( 40 |
41 | 42 | redux devtools filterable log monitor logo 47 | 48 | 49 |
    50 |
  • Use the log monitor on the side to filter the Redux store.
  • 51 |
  • (You can filter using strings or regular expressions!)
  • 52 |
  • Use the buttons below to trigger a store update (adding more random data).
  • 53 |
54 | 55 |

56 | 61 | 66 | 71 | 76 |

77 | 78 |

79 | 83 |

84 | 85 |

86 | Apologies for the basic demo. 87 | I hope to add more soon! 88 | In the meanwhile check out the documentation to learn more. 89 |

90 |
91 | ) 92 | } 93 | } 94 | 95 | // Import and attach the favicon 96 | document.querySelector('[rel="shortcut icon"]').href = require('file!./favicon.png') 97 | 98 | function UpdateButton ({ 99 | label, 100 | labelClass, 101 | onClick 102 | }) { 103 | return ( 104 | 111 | ) 112 | } 113 | 114 | const selectors = () => ({}) 115 | 116 | export default connect(selectors, actions)(Application) 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-devtools-filterable-log-monitor", 3 | "version": "0.8.1", 4 | "description": "Filterable tree view monitor for Redux DevTools", 5 | "main": "dist/commonjs/index.js", 6 | "jsnext:main": "dist/es/index.js", 7 | "scripts": { 8 | "build": "npm run build:commonjs && npm run build:es && npm run build:demo && npm run build:umd", 9 | "build:demo": "npm run clean:demo && NODE_ENV=production webpack --config webpack.config.demo.js -p --bail", 10 | "build:commonjs": "npm run clean:commonjs && cross-env NODE_ENV=production cross-env BABEL_ENV=commonjs babel source --out-dir dist/commonjs", 11 | "build:es": "npm run clean:es && cross-env NODE_ENV=production cross-env BABEL_ENV=es babel source --out-dir dist/es", 12 | "build:umd": "npm run clean:umd && cross-env NODE_ENV=production webpack --config webpack.config.umd.js --bail", 13 | "clean": "npm run clean:commonjs && npm run clean:demo && npm run clean:es && npm run clean:umd", 14 | "clean:commonjs": "rimraf dist/commonjs", 15 | "clean:demo": "rimraf build", 16 | "clean:es": "rimraf dist/es", 17 | "clean:umd": "rimraf dist/umd", 18 | "deploy": "gh-pages -d build", 19 | "lint": "standard", 20 | "prebuild": "npm run lint", 21 | "postpublish": "npm run deploy", 22 | "prepublish": "npm run build", 23 | "start": "webpack-dev-server --hot --inline --config webpack.config.dev.js", 24 | "test": "npm run lint && npm run test:unit", 25 | "test:unit": "NODE_ENV=test karma start", 26 | "watch": "watch 'clear && npm run lint -s && npm run test -s' source" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/bvaughn/redux-devtools-filterable-log-monitor.git" 31 | }, 32 | "keywords": [ 33 | "redux", 34 | "devtools", 35 | "flux", 36 | "react", 37 | "hot reloading", 38 | "time travel", 39 | "live edit", 40 | "search", 41 | "searchable", 42 | "filter", 43 | "filterable" 44 | ], 45 | "files": [ 46 | "dist" 47 | ], 48 | "standard": { 49 | "parser": "babel-eslint", 50 | "ignore": [ 51 | "build", 52 | "dist", 53 | "node_modules" 54 | ], 55 | "global": [ 56 | "self" 57 | ] 58 | }, 59 | "author": "Brian Vaughn (http://github.com/bvaughn)", 60 | "license": "MIT", 61 | "bugs": { 62 | "url": "https://github.com/bvaughn/redux-devtools-filterable-log-monitor/issues" 63 | }, 64 | "homepage": "https://github.com/bvaughn/redux-devtools-filterable-log-monitor", 65 | "devDependencies": { 66 | "babel-cli": "6.5.1", 67 | "babel-core": "^6.5.1", 68 | "babel-eslint": "^5.0.0", 69 | "babel-loader": "^6.2.3", 70 | "babel-plugin-__coverage__": "^0.1111.1", 71 | "babel-plugin-react-transform": "^2.0.0", 72 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 73 | "babel-plugin-transform-react-inline-elements": "^6.6.5", 74 | "babel-plugin-typecheck": "^3.6.1", 75 | "babel-polyfill": "^6.5.0", 76 | "babel-preset-es2015": "^6.22.0", 77 | "babel-preset-es2015-rollup": "^3.0.0", 78 | "babel-preset-react": "^6.5.0", 79 | "babel-preset-stage-0": "^6.5.0", 80 | "bluebird": "^3.0.5", 81 | "cross-env": "^1.0.7", 82 | "css-loader": "^0.23.0", 83 | "cssnext": "^1.8.4", 84 | "cssnext-loader": "^1.0.1", 85 | "express": "^4.13.3", 86 | "faker": "^3.0.1", 87 | "file-loader": "^0.8.5", 88 | "fs-extra": "^0.26.2", 89 | "gh-pages": "^0.6.0", 90 | "html-webpack-plugin": "^1.7.0", 91 | "immutable": "^3.7.5", 92 | "jasmine": "^2.3.2", 93 | "jasmine-core": "^2.3.4", 94 | "karma": "^0.13.15", 95 | "karma-jasmine": "^0.3.6", 96 | "karma-junit-reporter": "^0.3.8", 97 | "karma-phantomjs-launcher": "^0.2.1", 98 | "karma-sourcemap-loader": "^0.3.6", 99 | "karma-spec-reporter": "0.0.23", 100 | "karma-webpack": "^1.7.0", 101 | "phantomjs": "^1.9.19", 102 | "react": "16", 103 | "react-dom": "16", 104 | "react-redux": "^4.0.2", 105 | "react-transform-catch-errors": "^1.0.0", 106 | "react-transform-hmr": "^1.0.1", 107 | "redbox-react": "^1.0.1", 108 | "redux": "^3.0.5", 109 | "redux-devtools": "^3.0.0", 110 | "redux-devtools-dock-monitor": "^1.1.0", 111 | "reselect": "^2.0.1", 112 | "rimraf": "^2.4.3", 113 | "standard": "^5.4.1", 114 | "style-loader": "^0.13.0", 115 | "watch": "^0.16.0", 116 | "webpack": "^1.9.6", 117 | "webpack-dev-server": "^1.14.0" 118 | }, 119 | "peerDependencies": { 120 | "react": "^0.14.0 || ^15.0.0 || ^16.0.0", 121 | "redux-devtools": "^3.0.0" 122 | }, 123 | "dependencies": { 124 | "lodash.debounce": "^3.1.1", 125 | "prop-types": "^15.6.0", 126 | "react-highlighter": "^0.4.2", 127 | "react-json-tree": "^0.11.0", 128 | "react-responsive": "^4.0.3", 129 | "redux-devtools-themes": "^1.0.0" 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /source/FilterableLogMonitor.js: -------------------------------------------------------------------------------- 1 | /** @flow */ 2 | import React, { Component } from 'react' 3 | import PropTypes from 'prop-types' 4 | import reducer from './reducers' 5 | import * as themes from 'redux-devtools-themes' 6 | import { addActionMetadata } from './actions' 7 | import ActionFilter from './components/ActionFilter' 8 | import FilterableState from './components/FilterableState' 9 | import { createRegExpFromFilterText } from './utils' 10 | 11 | export default class FilterableLogMonitor extends Component { 12 | static update = reducer; 13 | 14 | static propTypes = { 15 | actionsById: PropTypes.object, 16 | computedStates: PropTypes.array, 17 | dispatch: PropTypes.func, 18 | monitorState: PropTypes.shape({ 19 | actionIdToDatumMap: PropTypes.object, 20 | actionFilterText: PropTypes.string 21 | }), 22 | stagedActionIds: PropTypes.array, 23 | theme: PropTypes.oneOfType([ 24 | PropTypes.object, 25 | PropTypes.string 26 | ]) 27 | } 28 | 29 | static defaultProps = { 30 | theme: 'nicinabox' 31 | } 32 | 33 | constructor (props) { 34 | super(props) 35 | 36 | this._isAutoScrollEnabled = true 37 | 38 | this._onScroll = this._onScroll.bind(this) 39 | this._setContainerRef = this._setContainerRef.bind(this) 40 | this._setWrapperRef = this._setWrapperRef.bind(this) 41 | } 42 | 43 | componentWillReceiveProps (nextProps) { 44 | const { actionsById, dispatch, stagedActionIds } = this.props 45 | 46 | if (nextProps.stagedActionIds !== stagedActionIds) { 47 | for (var i = stagedActionIds.length; i < nextProps.stagedActionIds.length; i++) { 48 | let actionId = nextProps.stagedActionIds[i] 49 | let action = actionsById[actionId] 50 | let appState = nextProps.computedStates[i].state 51 | 52 | dispatch(addActionMetadata({ action, actionId, appState })) 53 | } 54 | } 55 | } 56 | 57 | componentDidMount () { 58 | this._autoScroll() 59 | } 60 | 61 | componentDidUpdate () { 62 | this._autoScroll() 63 | } 64 | 65 | render () { 66 | const { 67 | actionsById, 68 | dispatch, 69 | monitorState: { 70 | actionFilterText, 71 | actionIdToDatumMap 72 | }, 73 | stagedActionIds 74 | } = this.props 75 | 76 | const theme = this._getTheme() 77 | const filterableStates = actionIdToDatumMap && stagedActionIds 78 | .filter(actionId => { 79 | const actionMetadata = actionsById[actionId] 80 | const monitorStateAction = actionIdToDatumMap[actionId] 81 | 82 | return ( 83 | monitorStateAction && 84 | ( 85 | !actionFilterText || 86 | actionMetadata.action.type.match( 87 | createRegExpFromFilterText(actionFilterText) 88 | ) 89 | ) 90 | ) 91 | }) 92 | .map(actionId => { 93 | const actionMetadata = actionsById[actionId] 94 | const monitorStateAction = actionIdToDatumMap[actionId] 95 | 96 | return ( 97 | 105 | ) 106 | }) 107 | 108 | return ( 109 |
124 | 129 |
136 |
137 | {filterableStates} 138 |
139 |
140 |
141 | ) 142 | } 143 | 144 | _autoScroll () { 145 | if (this._isAutoScrollEnabled) { 146 | this._wrapperRef.scrollTop = this._containerRef.offsetHeight 147 | } 148 | } 149 | 150 | _getTheme () { 151 | const { theme } = this.props 152 | 153 | if (typeof theme !== 'string') { 154 | return theme 155 | } else if (typeof themes[theme] !== 'undefined') { 156 | return themes[theme] 157 | } else { 158 | console.warn('DevTools theme ' + theme + ' not found, defaulting to nicinabox') 159 | return themes.nicinabox 160 | } 161 | } 162 | 163 | _onScroll () { 164 | this._isAutoScrollEnabled = 165 | this._wrapperRef.scrollTop + this._wrapperRef.offsetHeight >= 166 | this._containerRef.offsetHeight 167 | } 168 | 169 | _setContainerRef (ref) { 170 | this._containerRef = ref 171 | } 172 | 173 | _setWrapperRef (ref) { 174 | this._wrapperRef = ref 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [Bug Reports](#bugs) | [Features Requests](#features) | [Submitting Pull Requests](#pull-requests) | [Running Local Demo](#running-local-demo) | [Running Tests](#running-tests) 2 | 3 | # Contributing to this project 4 | 5 | Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved. 6 | 7 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. 8 | In return, they should reciprocate that respect in addressing your issue or assessing patches and features. 9 | 10 | ## Using the issue tracker 11 | 12 | The issue tracker is the preferred channel for bug reports but please respect the following restrictions: 13 | 14 | * Please **do not** use the issue tracker for personal support requests (use [Gitter](https://gitter.im/bvaughn/redux-devtools-filterable-log-monitor)). 15 | * Please **do not** derail or troll issues. Keep the discussion on topic and respect the opinions of others. 16 | 17 | 18 | ## Bug reports 19 | 20 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 21 | Good bug reports are extremely helpful - thank you! 22 | 23 | Guidelines for bug reports: 24 | 25 | 1. **Use the GitHub issue search** — check if the issue has already been reported. 26 | 2. **Check if the issue has been fixed** — try to reproduce it using the latest `master` or development branch in the repository. 27 | 3. **Isolate the problem** — create a [reduced test case](http://css-tricks.com/reduced-test-cases/) and a live example (using a site like [Plunker](http://plnkr.co/)). 28 | 29 | A good bug report shouldn't leave others needing to chase you up for more information. 30 | Please try to be as detailed as possible in your report. 31 | Which versions of formFor and Angular are you using? 32 | What steps will reproduce the issue? What browser(s) and OS experience the problem? 33 | What would you expect to be the outcome? 34 | All these details will help people to fix any potential bugs. 35 | 36 | Example: 37 | 38 | > Short and descriptive example bug report title 39 | > 40 | > A summary of the issue and the browser/OS environment in which it occurs. 41 | > If suitable, include the steps required to reproduce the bug. 42 | > 43 | > 1. This is the first step 44 | > 2. This is the second step 45 | > 3. Further steps, etc. 46 | > 47 | > `` - a link to the reduced test case 48 | > 49 | > Any other information you want to share that is relevant to the issue being reported. 50 | > This might include the lines of code that you have identified as causing the bug, 51 | > and potential solutions (and your opinions on their merits). 52 | 53 | 54 | 55 | ## Feature requests 56 | 57 | Feature requests are welcome. 58 | But take a moment to find out whether your idea fits with the scope and aims of the project. 59 | It's up to *you* to make a strong case to convince the project's developers of the merits of this feature. 60 | Please provide as much detail and context as possible. 61 | 62 | 63 | 64 | ## Pull requests 65 | 66 | Good pull requests - patches, improvements, new features - are a fantastic help. 67 | They should remain focused in scope and avoid containing unrelated commits. 68 | 69 | **Please ask first** before embarking on any significant pull request (e.g. implementing features, refactoring code, porting to a different language), 70 | otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project. 71 | 72 | Please adhere to the coding conventions used throughout a project (indentation, accurate comments, etc.) and any other requirements (such as test coverage). 73 | 74 | Follow this process if you'd like your work considered for inclusion in the project: 75 | 76 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, and configure the remotes: 77 | 78 | ```bash 79 | # Clone your fork of the repo into the current directory 80 | git clone https://github.com//redux-devtools-filterable-log-monitor 81 | # Navigate to the newly cloned directory 82 | cd redux-devtools-filterable-log-monitor 83 | # Assign the original repo to a remote called "upstream" 84 | git remote add upstream https://github.com/bvaughn/redux-devtools-filterable-log-monitor 85 | ``` 86 | 87 | 2. If you cloned a while ago, get the latest changes from upstream: 88 | 89 | ```bash 90 | git checkout master 91 | git pull upstream master 92 | ``` 93 | 94 | 3. Create a new topic branch (off the main project development branch) to 95 | contain your feature, change, or fix: 96 | 97 | ```bash 98 | git checkout -b 99 | ``` 100 | 101 | 4. Commit your changes in logical chunks. 102 | Please adhere to these [git commit message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 103 | or your code is unlikely be merged into the main project. 104 | Use Git's [interactive rebase](https://help.github.com/articles/interactive-rebase) 105 | feature to tidy up your commits before making them public. 106 | 107 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 108 | 109 | ```bash 110 | git pull [--rebase] upstream master 111 | ``` 112 | 113 | 6. Push your topic branch up to your fork: 114 | 115 | ```bash 116 | git push origin 117 | ``` 118 | 119 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 120 | with a clear title and description. 121 | 122 | **IMPORTANT**: By submitting a patch, you agree to allow the project owner to license your work under the same license as that used by this project (MIT). 123 | 124 | 125 | ## Running Local Demo 126 | 127 | You can run the local demo with NPM like so: 128 | 129 | ```bash 130 | cd 131 | npm start 132 | ``` 133 | 134 | The local app will then be available at http://localhost:3001 135 | 136 | 137 | ## Running Tests 138 | 139 | All unit tests must pass before a pull request will be approved. 140 | You can run unit tests with NPM like so: 141 | 142 | ```bash 143 | cd 144 | npm test 145 | ``` 146 | -------------------------------------------------------------------------------- /source/components/FilterHeader.js: -------------------------------------------------------------------------------- 1 | /** @flow */ 2 | import React, { Component } from 'react' 3 | import PropTypes from 'prop-types' 4 | import { 5 | setFilterByKeys, 6 | setFilterByValues, 7 | setFilterText, 8 | setExpanded 9 | } from '../actions' 10 | import { formatDateAsTime } from '../utils' 11 | import debounce from 'lodash.debounce' 12 | import MediaQuery from 'react-responsive' 13 | 14 | const DEBOUNCE_TIME = 250 15 | 16 | export default class FilterHeader extends Component { 17 | static propTypes = { 18 | action: PropTypes.object.isRequired, 19 | actionId: PropTypes.any.isRequired, 20 | dispatch: PropTypes.func.isRequired, 21 | monitorStateAction: PropTypes.object.isRequired, 22 | theme: PropTypes.object.isRequired 23 | } 24 | 25 | constructor (props) { 26 | super(props) 27 | 28 | this._onFilterByKeysChange = this._onFilterByKeysChange.bind(this) 29 | this._onFilterByValuesChange = this._onFilterByValuesChange.bind(this) 30 | this._onFilterTextChange = this._onFilterTextChange.bind(this) 31 | this._toggleExpanded = this._toggleExpanded.bind(this) 32 | 33 | this._debouncedOnFilterTextChange = debounce( 34 | this._onFilterTextChange, 35 | DEBOUNCE_TIME 36 | ) 37 | } 38 | 39 | render () { 40 | const { 41 | action, 42 | monitorStateAction, 43 | theme 44 | } = this.props 45 | 46 | const { 47 | expanded, 48 | filterByKeys, 49 | filterByValues, 50 | filterText, 51 | time 52 | } = monitorStateAction 53 | 54 | return ( 55 |
61 |
70 |
85 | 86 |
97 | {action.type} 98 | 99 | 100 | 107 | {`(${formatDateAsTime(time)})`} 108 | 109 | 110 |
111 | 112 |
121 | 122 |
127 | Filter by 128 |
129 |
130 | 145 | 160 |
161 |
162 | 163 | {expanded && 164 |
178 | 194 |
195 | } 196 |
197 | ) 198 | } 199 | 200 | _onFilterByKeysChange (event) { 201 | const filterByKeys = event.target.checked 202 | const { 203 | actionId, 204 | dispatch 205 | } = this.props 206 | 207 | dispatch(setFilterByKeys({ 208 | actionId, 209 | filterByKeys 210 | })) 211 | } 212 | 213 | _onFilterByValuesChange (event) { 214 | const filterByValues = event.target.checked 215 | const { 216 | actionId, 217 | dispatch 218 | } = this.props 219 | 220 | dispatch(setFilterByValues({ 221 | actionId, 222 | filterByValues 223 | })) 224 | } 225 | 226 | _onFilterTextChange (event) { 227 | const filterText = this.refs.input.value 228 | const { 229 | actionId, 230 | dispatch 231 | } = this.props 232 | 233 | dispatch(setFilterText({ 234 | actionId, 235 | filterText 236 | })) 237 | } 238 | 239 | _toggleExpanded () { 240 | const { 241 | actionId, 242 | dispatch, 243 | monitorStateAction 244 | } = this.props 245 | 246 | const { expanded } = monitorStateAction 247 | 248 | dispatch(setExpanded({ 249 | actionId, 250 | expanded: !expanded 251 | })) 252 | } 253 | } 254 | --------------------------------------------------------------------------------