Add redux-slider-monitor (#434)

* Add redux-slider-monitor

* Fix example configuration of redux-slider-monitor

* Fix lint errors

* CI: Run build:all before lint
This commit is contained in:
Jhen-Jie Hong 2018-12-22 08:20:04 -06:00 committed by Mihail Diordiev
parent e6fdfb9c9e
commit 476e37a875
35 changed files with 2383 additions and 55 deletions

View File

@ -8,6 +8,6 @@ cache:
directories: directories:
- "node_modules" - "node_modules"
script: script:
- npm run lint
- npm run build:all - npm run build:all
- npm run lint
- npm test - npm test

View File

@ -0,0 +1,3 @@
{
"presets": ["es2015", "stage-0", "react"]
}

View File

@ -0,0 +1,3 @@
lib
**/node_modules
examples/**/dist

View File

@ -0,0 +1,22 @@
{
"extends": "eslint-config-airbnb",
"env": {
"browser": true,
"mocha": true,
"node": true
},
"parser": "babel-eslint",
"rules": {
"comma-dangle": [2, "never"],
"jsx-quotes": [2, "prefer-single"],
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/react-in-jsx-scope": 2,
"react/sort-comp": 0,
"react/forbid-prop-types": 0,
"import/no-extraneous-dependencies": 0,
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"jsx-a11y/no-static-element-interactions": 0
},
"plugins": ["react"]
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Cale Newman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,61 @@
## Redux Slider Monitor
[![npm version](https://img.shields.io/npm/v/redux-slider-monitor.svg?style=flat-square)](https://www.npmjs.com/package/redux-slider-monitor)
A custom monitor for use with [Redux DevTools](https://github.com/gaearon/redux-devtools).
It uses a slider based on [react-slider](https://github.com/mpowaga/react-slider) to slide between different recorded actions. It also features play/pause/step-through, which is inspired by some very cool [Elm](http://elm-lang.org/) [examples](http://elm-lang.org/blog/time-travel-made-easy).
[Try out the demo!](https://calesce.github.io/redux-slider-monitor/?debug_session=123)
<image src="https://s3.amazonaws.com/f.cl.ly/items/1I3P222C3N2R1M2y1K3b/Screen%20Recording%202015-12-22%20at%2007.20%20PM.gif?v=1b6267e7" width='800'>
### Installation
```npm install redux-slider-monitor```
### Recommended Usage
Use with [`DockMonitor`](https://github.com/gaearon/redux-devtools-dock-monitor)
```javascript
<DockMonitor toggleVisibilityKey='ctrl-h'
changePositionKey='ctrl-q'
defaultPosition='bottom'
defaultSize={0.15}>
<SliderMonitor keyboardEnabled />
</DockMonitor>
```
Dispatch some Redux actions. Use the slider to navigate between the state changes.
Click the play/pause buttons to watch the state changes over time, or step backward or forward in state time with the left/right arrow buttons. Change replay speeds with the ```1x``` button, and "Live" will replay actions with the same time intervals in which they originally were dispatched.
## Keyboard shortcuts
Pass the ```keyboardEnabled``` prop to use these shortcuts
```ctrl+j```: play/pause
```ctrl+[```: step backward
```ctrl+]```: step forward
### Running Examples
You can do this:
```
git clone https://github.com/calesce/redux-slider-monitor.git
cd redux-slider-monitor
npm install
cd examples/todomvc
npm install
npm start
open http://localhost:3000
```
### License
MIT

View File

@ -0,0 +1,10 @@
{
"presets": [
["es2015", { "modules": false }],
"stage-0",
"react"
],
"plugins": [
"react-hot-loader/babel"
]
}

View File

@ -0,0 +1,6 @@
# Redux DevTools TodoMVC example
## Getting Started
1. Install dependencies: `npm i`
2. Start the development server: `npm start`

View File

@ -0,0 +1,42 @@
import * as types from '../constants/ActionTypes';
export function addTodo(text) {
return {
type: types.ADD_TODO,
text
};
}
export function deleteTodo(id) {
return {
type: types.DELETE_TODO,
id
};
}
export function editTodo(id, text) {
return {
type: types.EDIT_TODO,
id,
text
};
}
export function markTodo(id) {
return {
type: types.MARK_TODO,
id
};
}
export function markAll() {
return {
type: types.MARK_ALL
};
}
export function clearMarked() {
return {
type: types.CLEAR_MARKED
};
}

View File

@ -0,0 +1,72 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { SHOW_ALL, SHOW_MARKED, SHOW_UNMARKED } from '../constants/TodoFilters';
const FILTER_TITLES = {
[SHOW_ALL]: 'All',
[SHOW_UNMARKED]: 'Active',
[SHOW_MARKED]: 'Completed'
};
export default class Footer extends Component {
static propTypes = {
markedCount: PropTypes.number.isRequired,
unmarkedCount: PropTypes.number.isRequired,
filter: PropTypes.string.isRequired,
onClearMarked: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired
};
render() {
return (
<footer className='footer'>
{this.renderTodoCount()}
<ul className='filters'>
{[SHOW_ALL, SHOW_UNMARKED, SHOW_MARKED].map(filter => (
<li key={filter}>{this.renderFilterLink(filter)}</li>
))}
</ul>
{this.renderClearButton()}
</footer>
);
}
renderTodoCount() {
const { unmarkedCount } = this.props;
const itemWord = unmarkedCount === 1 ? 'item' : 'items';
return (
<span className='todo-count'>
<strong>{unmarkedCount || 'No'}</strong> {itemWord} left
</span>
);
}
renderFilterLink(filter) {
const title = FILTER_TITLES[filter];
const { filter: selectedFilter, onShow } = this.props;
return (
<a
className={classnames({ selected: filter === selectedFilter })}
style={{ cursor: 'hand' }}
onClick={() => onShow(filter)}
>
{title}
</a>
);
}
renderClearButton() {
const { markedCount, onClearMarked } = this.props;
if (markedCount > 0) {
return (
<button className='clear-completed' onClick={onClearMarked}>
Clear completed
</button>
);
}
return null;
}
}

View File

@ -0,0 +1,28 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import TodoTextInput from './TodoTextInput';
export default class Header extends Component {
static propTypes = {
addTodo: PropTypes.func.isRequired
};
handleSave = (text) => {
if (text.length !== 0) {
this.props.addTodo(text);
}
}
render() {
return (
<header className='header'>
<h1>todos</h1>
<TodoTextInput
newTodo
onSave={this.handleSave}
placeholder='What needs to be done?'
/>
</header>
);
}
}

View File

@ -0,0 +1,90 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import TodoItem from './TodoItem';
import Footer from './Footer';
import { SHOW_ALL, SHOW_MARKED, SHOW_UNMARKED } from '../constants/TodoFilters';
const TODO_FILTERS = {
[SHOW_ALL]: () => true,
[SHOW_UNMARKED]: todo => !todo.marked,
[SHOW_MARKED]: todo => todo.marked
};
export default class MainSection extends Component {
static propTypes = {
todos: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired
};
constructor(props, context) {
super(props, context);
this.handleClearMarked = this.handleClearMarked.bind(this);
this.handleShow = this.handleShow.bind(this);
this.state = { filter: SHOW_ALL };
}
handleClearMarked() {
const atLeastOneMarked = this.props.todos.some(todo => todo.marked);
if (atLeastOneMarked) {
this.props.actions.clearMarked();
}
}
handleShow(filter) {
this.setState({ filter });
}
render() {
const { todos, actions } = this.props;
const { filter } = this.state;
const filteredTodos = todos.filter(TODO_FILTERS[filter]);
const markedCount = todos.reduce((count, todo) => (todo.marked ? count + 1 : count), 0);
return (
<section className='main'>
{this.renderToggleAll(markedCount)}
<ul className='todo-list'>
{filteredTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} {...actions} />
))}
</ul>
{this.renderFooter(markedCount)}
</section>
);
}
renderToggleAll(markedCount) {
const { todos, actions } = this.props;
if (todos.length > 0) {
return (
<input
className='toggle-all'
type='checkbox'
checked={markedCount === todos.length}
onChange={actions.markAll}
/>
);
}
return null;
}
renderFooter(markedCount) {
const { todos } = this.props;
const { filter } = this.state;
const unmarkedCount = todos.length - markedCount;
if (todos.length) {
return (
<Footer
markedCount={markedCount}
unmarkedCount={unmarkedCount}
filter={filter}
onClearMarked={this.handleClearMarked}
onShow={this.handleShow}
/>
);
}
return null;
}
}

View File

@ -0,0 +1,77 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import TodoTextInput from './TodoTextInput';
export default class TodoItem extends Component {
static propTypes = {
todo: PropTypes.object.isRequired,
editTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired,
markTodo: PropTypes.func.isRequired
};
constructor(props, context) {
super(props, context);
this.state = {
editing: false
};
}
handleDoubleClick = () => {
this.setState({ editing: true });
}
handleSave = (id, text) => {
if (text.length === 0) {
this.props.deleteTodo(id);
} else {
this.props.editTodo(id, text);
}
this.setState({ editing: false });
}
render() {
const { todo, markTodo, deleteTodo } = this.props;
let element;
if (this.state.editing) {
element = (
<TodoTextInput
text={todo.text}
editing={this.state.editing}
onSave={text => this.handleSave(todo.id, text)}
/>
);
} else {
element = (
<div className='view'>
<input
className='toggle'
type='checkbox'
checked={todo.marked}
onChange={() => markTodo(todo.id)}
/>
<label htmlFor='text' onDoubleClick={this.handleDoubleClick}>
{todo.text}
</label>
<button
className='destroy'
onClick={() => deleteTodo(todo.id)}
/>
</div>
);
}
return (
<li
className={classnames({
completed: todo.marked,
editing: this.state.editing
})}
>
{element}
</li>
);
}
}

View File

@ -0,0 +1,62 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
export default class TodoTextInput extends Component {
static propTypes = {
onSave: PropTypes.func.isRequired,
text: PropTypes.string,
placeholder: PropTypes.string,
editing: PropTypes.bool,
newTodo: PropTypes.bool
};
static defaultProps = {
text: '',
placeholder: '',
editing: false,
newTodo: false
};
constructor(props, context) {
super(props, context);
this.state = {
text: this.props.text || ''
};
}
handleSubmit = (e) => {
const text = e.target.value.trim();
if (e.which === 13) {
this.props.onSave(text);
if (this.props.newTodo) {
this.setState({ text: '' });
}
}
}
handleChange = (e) => {
this.setState({ text: e.target.value });
}
handleBlur = (e) => {
if (!this.props.newTodo) {
this.props.onSave(e.target.value);
}
}
render() {
return (
<input
className={classnames({ edit: this.props.editing, 'new-todo': this.props.newTodo })}
type='text'
placeholder={this.props.placeholder}
autoFocus='true'
value={this.state.text}
onBlur={this.handleBlur}
onChange={this.handleChange}
onKeyDown={this.handleSubmit}
/>
);
}
}

View File

@ -0,0 +1,6 @@
export const ADD_TODO = 'ADD_TODO';
export const DELETE_TODO = 'DELETE_TODO';
export const EDIT_TODO = 'EDIT_TODO';
export const MARK_TODO = 'MARK_TODO';
export const MARK_ALL = 'MARK_ALL';
export const CLEAR_MARKED = 'CLEAR_MARKED';

View File

@ -0,0 +1,3 @@
export const SHOW_ALL = 'show_all';
export const SHOW_MARKED = 'show_marked';
export const SHOW_UNMARKED = 'show_unmarked';

View File

@ -0,0 +1,15 @@
import React from 'react';
import { createDevTools } from 'redux-devtools';
import DockMonitor from 'redux-devtools-dock-monitor';
import SliderMonitor from 'redux-slider-monitor'; // eslint-disable-line
export default createDevTools(
<DockMonitor
toggleVisibilityKey='ctrl-h'
changePositionKey='ctrl-q'
defaultPosition='bottom'
defaultSize={0.15}
>
<SliderMonitor keyboardEnabled />
</DockMonitor>
);

View File

@ -0,0 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import TodoApp from './TodoApp';
import DevTools from './DevTools';
const Root = ({ store }) => (
<Provider store={store}>
<div>
<TodoApp />
<DevTools />
</div>
</Provider>
);
Root.propTypes = {
store: PropTypes.object.isRequired
};
export default Root;

View File

@ -0,0 +1,6 @@
/* eslint-disable global-require */
if (process.env.NODE_ENV === 'production') {
module.exports = require('./Root.prod');
} else {
module.exports = require('./Root.dev');
}

View File

@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import TodoApp from './TodoApp';
const Root = ({ store }) => (
<Provider store={store}>
<div>
<TodoApp />
</div>
</Provider>
);
Root.propTypes = {
store: PropTypes.object.isRequired
};
export default Root;

View File

@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Header from '../components/Header';
import MainSection from '../components/MainSection';
import * as TodoActions from '../actions/TodoActions';
const TodoApp = ({ todos, actions }) => (
<div>
<Header addTodo={actions.addTodo} />
<MainSection todos={todos} actions={actions} />
</div>
);
TodoApp.propTypes = {
todos: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired
};
function mapState(state) {
return {
todos: state.todos
};
}
function mapDispatch(dispatch) {
return {
actions: bindActionCreators(TodoActions, dispatch)
};
}
export default connect(mapState, mapDispatch)(TodoApp);

View File

@ -0,0 +1,23 @@
import 'todomvc-app-css/index.css';
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import configureStore from './store/configureStore';
import Root from './containers/Root';
const store = configureStore();
const rootEl = document.getElementById('root');
const render = () => {
ReactDOM.render(
<AppContainer>
<Root store={store} />
</AppContainer>,
rootEl
);
};
render(Root);
if (module.hot) {
module.hot.accept('./containers/Root', render);
}

View File

@ -0,0 +1,15 @@
{
"name": "todomvc",
"version": "0.0.0",
"description": "TodoMVC example for redux",
"main": "server.js",
"scripts": {
"start": "../../node_modules/.bin/webpack-dev-server",
"build": "../../node_modules/.bin/webpack --config webpack.config.prod.js"
},
"repository": {
"type": "git",
"url": "https://github.com/calesce/redux-slider-monitor.git"
},
"license": "MIT"
}

View File

@ -0,0 +1,8 @@
import { combineReducers } from 'redux';
import todos from './todos';
const rootReducer = combineReducers({
todos
});
export default rootReducer;

View File

@ -0,0 +1,50 @@
import { ADD_TODO, DELETE_TODO, EDIT_TODO, MARK_TODO, MARK_ALL, CLEAR_MARKED } from '../constants/ActionTypes';
const initialState = [{
text: 'Use Redux',
marked: false,
id: 0
}];
export default function todos(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return [{
id: (state.length === 0) ? 0 : state[0].id + 1,
marked: false,
text: action.text
}, ...state];
case DELETE_TODO:
return state.filter(todo =>
todo.id !== action.id
);
case EDIT_TODO:
return state.map(todo => (
todo.id === action.id ?
{ ...todo, text: action.text } :
todo
));
case MARK_TODO:
return state.map(todo => (
todo.id === action.id ?
{ ...todo, marked: !todo.marked } :
todo
));
case MARK_ALL: {
const areAllMarked = state.every(todo => todo.marked);
return state.map(todo => ({
...todo,
marked: !areAllMarked
}));
}
case CLEAR_MARKED:
return state.filter(todo => todo.marked === false);
default:
return state;
}
}

View File

@ -0,0 +1,23 @@
import { createStore, compose } from 'redux';
import { persistState } from 'redux-devtools';
import rootReducer from '../reducers';
import DevTools from '../containers/DevTools';
const finalCreateStore = compose(
DevTools.instrument(),
persistState(
window.location.href.match(
/[?&]debug_session=([^&]+)\b/
)
)
)(createStore);
export default function configureStore(initialState) {
const store = finalCreateStore(rootReducer, initialState);
if (module.hot) {
module.hot.accept('../reducers', () => store.replaceReducer(rootReducer));
}
return store;
}

View File

@ -0,0 +1,6 @@
/* eslint-disable global-require */
if (process.env.NODE_ENV === 'production') {
module.exports = require('./configureStore.prod');
} else {
module.exports = require('./configureStore.dev');
}

View File

@ -0,0 +1,6 @@
import { createStore } from 'redux';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
return createStore(rootReducer, initialState);
}

View File

@ -0,0 +1,48 @@
const path = require('path');
const webpack = require('webpack');
module.exports = {
devtool: 'eval-cheap-module-source-map',
devServer: {
contentBase: path.join(__dirname, 'dist'),
host: 'localhost',
port: process.env.PORT || 3000,
historyApiFallback: true,
hot: true
},
entry: [
'react-hot-loader/patch',
'webpack-dev-server/client?http://localhost:3000',
'webpack/hot/only-dev-server',
'./index'
],
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
},
plugins: [new webpack.HotModuleReplacementPlugin(), new webpack.NoEmitOnErrorsPlugin()],
resolve: {
alias: {
'redux-slider-monitor': path.join(__dirname, '..', '..', 'src/SliderMonitor')
},
extensions: ['.js']
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
exclude: /node_modules/,
include: [__dirname, path.join(__dirname, '../../src')]
},
{
test: /\.css?$/,
use: ['style-loader', 'raw-loader'],
include: [
__dirname,
path.join(__dirname, '../../../../node_modules/todomvc-app-css')
]
}
]
}
};

View File

@ -0,0 +1,10 @@
// NOTE: This config is used for deploy to gh-pages
const webpack = require('webpack');
const devConfig = require('./webpack.config');
devConfig.entry = './index';
devConfig.plugins = [
new webpack.NoEmitOnErrorsPlugin()
];
module.exports = devConfig;

View File

@ -0,0 +1,60 @@
{
"name": "redux-slider-monitor",
"version": "2.0.0-2",
"description": "A custom monitor for replaying Redux actions that works similarly to a video player",
"main": "lib/SliderMonitor.js",
"scripts": {
"clean": "rimraf lib",
"build": "babel src --out-dir lib",
"lint": "eslint src examples",
"prepublish": "npm run lint && npm run clean && npm run build"
},
"repository": {
"url": "https://github.com/reduxjs/redux-devtools"
},
"author": "Cale Newman <newman.cale@gmail.com> (http://github.com/calesce)",
"license": "MIT",
"bugs": {
"url": "https://github.com/reduxjs/redux-devtools/issues"
},
"homepage": "https://github.com/reduxjs/redux-devtools",
"devDependencies": {
"babel-cli": "^6.24.1",
"babel-core": "^6.24.1",
"babel-eslint": "^7.2.2",
"babel-loader": "^6.4.1",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"classnames": "^2.1.2",
"eslint": "^5.0.0",
"eslint-config-airbnb": "^14.1.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jsx-a11y": "^4.0.0",
"eslint-plugin-react": "^7.4.0",
"raw-loader": "^0.5.1",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-redux": "^6.0.0",
"react-hot-loader": "^3.0.0-beta.6",
"redux": "^4.0.0",
"redux-devtools-dock-monitor": "^1.0.1",
"redux-devtools-log-monitor": "^1.0.1",
"redux-devtools": "^3.5.0",
"rimraf": "^2.3.4",
"style-loader": "^0.16.1",
"todomvc-app-css": "^2.0.1",
"webpack": "^2.2.1",
"webpack-dev-server": "^2.4.1"
},
"peerDependencies": {
"react": "^0.14.0 || ^15.0.0 || ^16.0.0-0",
"react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0-0",
"redux-devtools": "^3.0.0"
},
"dependencies": {
"devui": "^1.0.0-3",
"prop-types": "^15.5.8",
"redux-devtools-themes": "^1.0.0"
}
}

View File

@ -0,0 +1,93 @@
import React, { Component, PureComponent } from 'react';
import PropTypes from 'prop-types';
import Button from 'devui/lib/Button';
export default class SliderButton extends (PureComponent || Component) {
static propTypes = {
theme: PropTypes.object,
type: PropTypes.string,
disabled: PropTypes.bool,
onClick: PropTypes.func
};
iconStyle() {
return {
cursor: 'hand',
fill: this.props.theme.base06,
width: '1.8rem',
height: '1.8rem'
};
}
renderPlayButton() {
return (
<Button
onClick={this.props.onClick}
title='Play'
size='small'
disabled={this.props.disabled}
theme={this.props.theme}
>
<svg viewBox='0 0 24 24' preserveAspectRatio='xMidYMid meet' style={this.iconStyle()}>
<g>
<path d='M8 5v14l11-7z' />
</g>
</svg>
</Button>
);
}
renderPauseButton = () => (
<Button
onClick={this.props.onClick}
title='Pause'
size='small'
disabled={this.props.disabled}
theme={this.props.theme}
>
<svg viewBox='0 0 24 24' preserveAspectRatio='xMidYMid meet' style={this.iconStyle()}>
<g>
<path d='M6 19h4V5H6v14zm8-14v14h4V5h-4z' />
</g>
</svg>
</Button>
);
renderStepButton = (direction) => {
const isLeft = direction === 'left';
const d = isLeft
? 'M15.41 16.09l-4.58-4.59 4.58-4.59-1.41-1.41-6 6 6 6z'
: 'M8.59 16.34l4.58-4.59-4.58-4.59 1.41-1.41 6 6-6 6z';
return (
<Button
size='small'
title={isLeft ? 'Go back' : 'Go forward'}
onClick={this.props.onClick}
disabled={this.props.disabled}
theme={this.props.theme}
>
<svg viewBox='0 0 24 24' preserveAspectRatio='xMidYMid meet' style={this.iconStyle()}>
<g>
<path d={d} />
</g>
</svg>
</Button>
);
};
render() {
switch (this.props.type) {
case 'play':
return this.renderPlayButton();
case 'pause':
return this.renderPauseButton();
case 'stepLeft':
return this.renderStepButton('left');
case 'stepRight':
return this.renderStepButton('right');
default:
return null;
}
}
}

View File

@ -0,0 +1,311 @@
import React, { Component, PureComponent } from 'react';
import PropTypes from 'prop-types';
import * as themes from 'redux-devtools-themes';
import { ActionCreators } from 'redux-devtools';
import { Toolbar, Divider } from 'devui/lib/Toolbar';
import Slider from 'devui/lib/Slider';
import Button from 'devui/lib/Button';
import SegmentedControl from 'devui/lib/SegmentedControl';
import reducer from './reducers';
import SliderButton from './SliderButton';
const { reset, jumpToState } = ActionCreators;
export default class SliderMonitor extends (PureComponent || Component) {
static update = reducer;
static propTypes = {
dispatch: PropTypes.func,
computedStates: PropTypes.array,
stagedActionIds: PropTypes.array,
actionsById: PropTypes.object,
currentStateIndex: PropTypes.number,
monitorState: PropTypes.shape({
initialScrollTop: PropTypes.number
}),
preserveScrollTop: PropTypes.bool,
stagedActions: PropTypes.array,
select: PropTypes.func.isRequired,
hideResetButton: PropTypes.bool,
theme: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string
]),
keyboardEnabled: PropTypes.bool
};
static defaultProps = {
select: state => state,
theme: 'nicinabox',
preserveScrollTop: true,
keyboardEnabled: true
};
state = {
timer: undefined,
replaySpeed: '1x'
};
componentDidMount() {
if (typeof window !== 'undefined') {
window.addEventListener('keydown', this.handleKeyPress);
}
}
componentWillUnmount() {
if (typeof window !== 'undefined') {
window.removeEventListener('keydown', this.handleKeyPress);
}
}
setUpTheme = () => {
let theme;
if (typeof this.props.theme === 'string') {
if (typeof themes[this.props.theme] !== 'undefined') {
theme = themes[this.props.theme];
} else {
theme = themes.nicinabox;
}
} else {
theme = this.props.theme;
}
return theme;
}
handleReset = () => {
this.pauseReplay();
this.props.dispatch(reset());
}
handleKeyPress = (event) => {
if (!this.props.keyboardEnabled) {
return null;
}
if (event.ctrlKey && event.keyCode === 74) { // ctrl+j
event.preventDefault();
if (this.state.timer) {
return this.pauseReplay();
}
if (this.state.replaySpeed === 'Live') {
this.startRealtimeReplay();
} else {
this.startReplay();
}
} else if (event.ctrlKey && event.keyCode === 219) { // ctrl+[
event.preventDefault();
this.stepLeft();
} else if (event.ctrlKey && event.keyCode === 221) { // ctrl+]
event.preventDefault();
this.stepRight();
}
return null;
}
handleSliderChange = (value) => {
if (this.state.timer) {
this.pauseReplay();
}
this.props.dispatch(jumpToState(value));
}
startReplay = () => {
const { computedStates, currentStateIndex, dispatch } = this.props;
if (computedStates.length < 2) {
return;
}
const speed = this.state.replaySpeed === '1x' ? 500 : 200;
let stateIndex;
if (currentStateIndex === computedStates.length - 1) {
dispatch(jumpToState(0));
stateIndex = 0;
} else if (currentStateIndex === computedStates.length - 2) {
dispatch(jumpToState(currentStateIndex + 1));
return;
} else {
stateIndex = currentStateIndex + 1;
dispatch(jumpToState(currentStateIndex + 1));
}
let counter = stateIndex;
const timer = setInterval(() => {
if (counter + 1 <= computedStates.length - 1) {
dispatch(jumpToState(counter + 1));
}
counter += 1;
if (counter >= computedStates.length - 1) {
clearInterval(this.state.timer);
this.setState({
timer: undefined
});
}
}, speed);
this.setState({ timer });
}
startRealtimeReplay = () => {
if (this.props.computedStates.length < 2) {
return;
}
if (this.props.currentStateIndex === this.props.computedStates.length - 1) {
this.props.dispatch(jumpToState(0));
this.loop(0);
} else {
this.loop(this.props.currentStateIndex);
}
}
loop = (index) => {
let currentTimestamp = Date.now();
let timestampDiff = this.getLatestTimestampDiff(index);
const aLoop = () => {
const replayDiff = Date.now() - currentTimestamp;
if (replayDiff >= timestampDiff) {
this.props.dispatch(jumpToState(this.props.currentStateIndex + 1));
if (this.props.currentStateIndex >= this.props.computedStates.length - 1) {
this.pauseReplay();
return;
}
timestampDiff = this.getLatestTimestampDiff(this.props.currentStateIndex);
currentTimestamp = Date.now();
this.setState({
timer: requestAnimationFrame(aLoop)
});
} else {
this.setState({
timer: requestAnimationFrame(aLoop)
});
}
};
if (index !== this.props.computedStates.length - 1) {
this.setState({
timer: requestAnimationFrame(aLoop)
});
}
}
getLatestTimestampDiff = index =>
this.getTimestampOfStateIndex(index + 1) - this.getTimestampOfStateIndex(index)
getTimestampOfStateIndex = (stateIndex) => {
const id = this.props.stagedActionIds[stateIndex];
return this.props.actionsById[id].timestamp;
}
pauseReplay = (cb) => {
if (this.state.timer) {
cancelAnimationFrame(this.state.timer);
clearInterval(this.state.timer);
this.setState({
timer: undefined
}, () => {
if (typeof cb === 'function') {
cb();
}
});
}
}
stepLeft = () => {
this.pauseReplay();
if (this.props.currentStateIndex !== 0) {
this.props.dispatch(jumpToState(this.props.currentStateIndex - 1));
}
}
stepRight = () => {
this.pauseReplay();
if (this.props.currentStateIndex !== this.props.computedStates.length - 1) {
this.props.dispatch(jumpToState(this.props.currentStateIndex + 1));
}
}
changeReplaySpeed = (replaySpeed) => {
this.setState({ replaySpeed });
if (this.state.timer) {
this.pauseReplay(() => {
if (replaySpeed === 'Live') {
this.startRealtimeReplay();
} else {
this.startReplay();
}
});
}
}
render() {
const {
currentStateIndex, computedStates, actionsById, stagedActionIds, hideResetButton
} = this.props;
const { replaySpeed } = this.state;
const theme = this.setUpTheme();
const max = computedStates.length - 1;
const actionId = stagedActionIds[currentStateIndex];
let actionType = actionsById[actionId].action.type;
if (actionType === undefined) actionType = '<UNDEFINED>';
else if (actionType === null) actionType = '<NULL>';
else actionType = actionType.toString() || '<EMPTY>';
const onPlayClick = replaySpeed === 'Live' ? this.startRealtimeReplay : this.startReplay;
const playPause = this.state.timer ?
<SliderButton theme={theme} type='pause' onClick={this.pauseReplay} /> :
<SliderButton theme={theme} type='play' disabled={max <= 0} onClick={onPlayClick} />;
return (
<Toolbar noBorder compact fullHeight theme={theme}>
{playPause}
<Slider
label={actionType}
sublabel={`(${actionId})`}
min={0}
max={max}
value={currentStateIndex}
onChange={this.handleSliderChange}
theme={theme}
/>
<SliderButton
theme={theme}
type='stepLeft'
disabled={currentStateIndex <= 0}
onClick={this.stepLeft}
/>
<SliderButton
theme={theme}
type='stepRight'
disabled={currentStateIndex === max}
onClick={this.stepRight}
/>
<Divider theme={theme} />
<SegmentedControl
theme={theme}
values={['Live', '1x', '2x']}
selected={replaySpeed}
onClick={this.changeReplaySpeed}
/>
{!hideResetButton && [
<Divider key='divider' theme={theme} />,
<Button key='reset' theme={theme} onClick={this.handleReset}>Reset</Button>
]}
</Toolbar>
);
}
}

View File

@ -0,0 +1,3 @@
export default function reducer() {
return {};
}

1182
yarn.lock

File diff suppressed because it is too large Load Diff