├── .babelrc ├── .gitignore ├── .npmignore ├── .storybook ├── addons.js └── config.js ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── preview.jpg ├── src ├── components │ ├── List.jsx │ ├── Node.jsx │ ├── Panel.jsx │ ├── PrimitiveValue.jsx │ └── index.js ├── getValueExcerpt.js ├── index.jsx ├── renderData.js └── themes │ ├── dark.js │ ├── index.js │ └── light.js ├── stories ├── Example.jsx ├── index.js └── styles.css └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["last 2 versions"] 7 | } 8 | }], 9 | "react" 10 | ], 11 | "plugins": [ 12 | "transform-export-extensions", 13 | "transform-class-properties" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .storybook 2 | stories 3 | src 4 | docs 5 | stories 6 | webpack.* 7 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | function loadStories() { 4 | require('../stories'); 5 | } 6 | 7 | configure(loadStories, module); 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Artem Zakharchenko 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-data-preview 2 | 3 | Preview any JavaScript data in a fancy interactive way. Heavily inspired by React DevTools props inspector. 4 | 5 |

6 | Interactive data preview 7 |

8 | 9 | ## Getting started 10 | ### Install 11 | ```bash 12 | npm install react-data-preview 13 | ``` 14 | 15 | ### Use 16 | ```jsx 17 | import React from 'react'; 18 | import Preview from 'react-data-preview'; 19 | 20 | const data = { 21 | firstName: 'John', 22 | lastName: 'Maverick', 23 | isAuthenticated: true 24 | image: class Image {}, 25 | logout() {}, 26 | orders: [{}, {}], 27 | settings: { 28 | nestedKey: 'foo' 29 | } 30 | }; 31 | 32 | export default class App extends React.Component { 33 | render() { 34 | return ( 35 | 36 | ); 37 | } 38 | } 39 | ``` 40 | 41 | ## License 42 | MIT 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-data-preview", 3 | "version": "1.0.0", 4 | "description": "Fancy preview for JavaScript data.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "storybook": "start-storybook -p 6006", 8 | "build": "NODE_ENV=production webpack", 9 | "prepublishOnly": "npm run build" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/kettanaito/react-data-preview.git" 14 | }, 15 | "keywords": [ 16 | "javascript", 17 | "data", 18 | "preview", 19 | "react", 20 | "react-data-preview" 21 | ], 22 | "author": "Artem Zakharchenko", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/kettanaito/react-data-preview/issues" 26 | }, 27 | "homepage": "https://github.com/kettanaito/react-data-preview#readme", 28 | "dependencies": { 29 | "react": "^16.2.0", 30 | "styled-components": "^3.2.3" 31 | }, 32 | "devDependencies": { 33 | "@storybook/addon-actions": "^3.3.15", 34 | "@storybook/addon-links": "^3.3.15", 35 | "@storybook/react": "^3.3.15", 36 | "babel-core": "^6.26.0", 37 | "babel-loader": "^7.1.4", 38 | "babel-minify-webpack-plugin": "^0.3.1", 39 | "babel-plugin-transform-class-properties": "^6.24.1", 40 | "babel-plugin-transform-export-extensions": "^6.22.0", 41 | "babel-preset-env": "^1.6.1", 42 | "babel-preset-react": "^6.24.1", 43 | "prop-types": "^15.6.1", 44 | "react-dom": "^16.2.0", 45 | "storybook": "^1.0.0", 46 | "webpack": "^4.2.0", 47 | "webpack-cli": "^2.0.12" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/react-data-preview/2e66005f8e119f818f5a910f16a524c4852baeda/preview.jpg -------------------------------------------------------------------------------- /src/components/List.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const List = styled.ul` 5 | margin: 0; 6 | padding: 0; 7 | 8 | list-style:none; 9 | `; 10 | 11 | export default List; 12 | -------------------------------------------------------------------------------- /src/components/Node.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import List from './List'; 5 | import renderData from '../renderData'; 6 | import getValueExcerpt from '../getValueExcerpt'; 7 | 8 | const NodeContainer = styled.li` 9 | position: relative; 10 | padding-left: .9rem; 11 | 12 | list-style: none; 13 | 14 | &:not(:last-child) { 15 | margin-bottom: .25rem; 16 | } 17 | `; 18 | 19 | const NodeArrow = styled.span` 20 | position: absolute; 21 | top: 6px; 22 | left: 3px; 23 | display: inline-block; 24 | margin: auto .3rem auto 0; 25 | height: 0; 26 | 27 | border-style: solid; 28 | border-width: 5px; 29 | border-color: ${({ theme }) => theme.node.arrowBackground} transparent transparent; 30 | 31 | transform: rotate(${({ isExpanded }) => isExpanded ? 0 : -90}deg); 32 | transform-origin: center; 33 | 34 | ${({ isExpanded}) => isExpanded && ` 35 | top: 7px; 36 | left: 0; 37 | `} 38 | `; 39 | 40 | const NodeName = styled.span` 41 | background-color: ${({ theme }) => theme.node.name.background}; 42 | color: ${({ theme }) => theme.node.name.foreground}; 43 | cursor: pointer; 44 | `; 45 | 46 | export default class Node extends React.PureComponent { 47 | static propTypes = { 48 | name: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, 49 | value: PropTypes.any.isRequired 50 | } 51 | 52 | state = { 53 | isExpanded: false 54 | } 55 | 56 | handleClick = (event) => { 57 | event.stopPropagation(); 58 | this.setState(({ isExpanded }) => ({ isExpanded: !isExpanded })); 59 | } 60 | 61 | renderChildren = () => { 62 | const { children, value } = this.props; 63 | if (children) return children; 64 | 65 | return ( 66 | 67 | { renderData(value) } 68 | 69 | ); 70 | } 71 | 72 | render() { 73 | const { isExpanded } = this.state; 74 | const { children, name, value } = this.props; 75 | const isExpandable = (Array.isArray(value)) || (value instanceof Object); 76 | const hasBlueValue = Array.isArray(value) || (typeof value === 'function'); 77 | 78 | return ( 79 | 80 | { isExpandable && () } 81 | { name }: 82 | { getValueExcerpt(value) } 83 | { isExpanded && this.renderChildren() } 84 | 85 | ); 86 | } 87 | } -------------------------------------------------------------------------------- /src/components/Panel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Panel = styled.ul` 5 | margin: 1rem; 6 | padding: 1rem; 7 | 8 | background-color: ${({ theme }) => theme.panel.background}; 9 | border-radius: 3px; 10 | box-shadow: 11 | 0 2px 12px rgba(0, 0, 0, .2), 12 | inset 0 0 0 1px ${({ theme }) => theme.panel.borderColor}, 13 | inset 0 0 0 2px rgba(255, 255, 255, .2); 14 | 15 | color: ${({ theme }) => theme.panel.foreground}; 16 | line-height: 1.4; 17 | `; 18 | 19 | export default Panel; 20 | -------------------------------------------------------------------------------- /src/components/PrimitiveValue.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export default styled.span` 4 | background-color: ${({ name, theme }) => theme.primitives[name].background}; 5 | color: ${({ name, theme }) => theme.primitives[name].foreground}; 6 | `; 7 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export Panel from './Panel'; 2 | export List from './List'; 3 | export Node from './Node'; 4 | export PrimitiveValue from './PrimitiveValue'; 5 | -------------------------------------------------------------------------------- /src/getValueExcerpt.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PrimitiveValue } from './components'; 3 | 4 | export default function getValueExcerpt(value) { 5 | if (typeof value === 'string') { 6 | return "{value}" 7 | } 8 | 9 | if (typeof value === 'boolean') { 10 | return {value.toString()}; 11 | } 12 | 13 | if (typeof value === 'function') { 14 | return {value.name || 'fn'}(); 15 | } 16 | 17 | if (Array.isArray(value)) { 18 | return Array[{value.length}]; 19 | } 20 | 21 | if (value instanceof Object) { 22 | return {…}; 23 | } 24 | 25 | if (!isNaN(value)) { 26 | return {value}; 27 | } 28 | 29 | 30 | return 'null'; 31 | } -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ThemeProvider } from 'styled-components'; 4 | import * as themes from './themes'; 5 | import { Panel, Node } from './components'; 6 | import renderData from './renderData'; 7 | 8 | export default class Preview extends React.Component { 9 | static propTypes = { 10 | data: PropTypes.any.isRequired, 11 | theme: PropTypes.string 12 | } 13 | 14 | static defaultProps = { 15 | theme: 'dark' 16 | } 17 | 18 | render() { 19 | const { data } = this.props; 20 | console.log(data); 21 | 22 | return ( 23 | 24 | 25 | { renderData(data) } 26 | 27 | 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/renderData.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { List, Node } from './components'; 3 | 4 | export default function renderData(data) { 5 | if (Array.isArray(data)) { 6 | return renderArray(data); 7 | } 8 | 9 | if (typeof data === 'function') { 10 | return renderFunction(data); 11 | } 12 | 13 | if (data instanceof Object) { 14 | return renderObject(data); 15 | } 16 | } 17 | 18 | function renderArray(data) { 19 | if (data.length === 0) { 20 | return (
Empty array
); 21 | } 22 | 23 | return data.map((value, index) => { 24 | return ( 25 | 29 | ); 30 | }); 31 | } 32 | 33 | function renderObject(data) { 34 | return Object.keys(data).map((keyName, index) => { 35 | return ( 36 | 40 | ); 41 | }); 42 | } 43 | 44 | function renderFunction(data) { 45 | const { prototype } = data; 46 | 47 | return ( 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/themes/dark.js: -------------------------------------------------------------------------------- 1 | export default { 2 | panel: { 3 | foreground: '#AFCCD5', 4 | background: '#243239', 5 | borderColor: '#243239' 6 | }, 7 | primitives: { 8 | string: { 9 | foreground: '#FF8B6E' 10 | }, 11 | boolean: { 12 | foreground: '#FF8B6E' 13 | }, 14 | number: { 15 | foreground: '#FF8B6E' 16 | }, 17 | array: { 18 | foregroud: '#79ABFA' 19 | }, 20 | object: { 21 | foreground: '#FF8B6E' 22 | }, 23 | function: { 24 | foreground: '#79ABFA' 25 | } 26 | }, 27 | node: { 28 | arrowBackground: 'rgba(255, 255, 255, .2)', 29 | name: { 30 | foreground: '#7AD9ED' 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/themes/index.js: -------------------------------------------------------------------------------- 1 | export dark from './dark'; 2 | export light from './light'; 3 | -------------------------------------------------------------------------------- /src/themes/light.js: -------------------------------------------------------------------------------- 1 | export default { 2 | panel: { 3 | foreground: '#888', 4 | background: '#f9f9f9', 5 | borderColor: '#ddd', 6 | }, 7 | primitives: { 8 | string: { 9 | foreground: '#F44C4F' 10 | }, 11 | boolean: { 12 | foreground: '#F44C4F' 13 | }, 14 | number: { 15 | foreground: '#F44C4F' 16 | }, 17 | array: { 18 | foreground: '#636DDC' 19 | }, 20 | object: { 21 | foreground: '#F44C4F' 22 | }, 23 | function: { 24 | foreground: '#636DDC' 25 | } 26 | }, 27 | node: { 28 | arrowBackground: 'rgba(0, 0, 0, .2)', 29 | name: { 30 | foreground: '#444' 31 | } 32 | } 33 | }; 34 | 35 | -------------------------------------------------------------------------------- /stories/Example.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Preview from '../src'; 3 | 4 | class UserImage {} 5 | 6 | const data = { 7 | firstName: 'John', 8 | lastName: 'Maverick', 9 | isAuthenticated: true, 10 | image: UserImage, 11 | logout() {}, 12 | orders: [ 13 | { 14 | id: 1, 15 | price: 1230, 16 | status: 'NEW', 17 | reorder: function() {} 18 | }, 19 | { 20 | id: 2, 21 | price: 3400, 22 | status: 'CLOSED', 23 | reorder: function() {} 24 | } 25 | ], 26 | settings: { 27 | isBusiness: false 28 | } 29 | }; 30 | 31 | export default class Example extends React.Component { 32 | render() { 33 | return ( 34 |
35 | 36 | 37 |
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import './styles.css'; 4 | 5 | import Example from './Example'; 6 | 7 | storiesOf('Welcome', module) 8 | .add('Example', () => ); 9 | -------------------------------------------------------------------------------- /stories/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 14px; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 4 | } 5 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | const PRODUCTION = (process.env.NODE_ENV === 'production'); 5 | 6 | module.exports = { 7 | mode: process.env.NODE_ENV, 8 | target: 'web', 9 | entry: { 10 | index: path.resolve(__dirname, 'src/index.jsx') 11 | }, 12 | externals: { 13 | react: 'umd react' 14 | }, 15 | output: { 16 | path: path.resolve(__dirname, 'lib'), 17 | filename: '[name].js', 18 | library: 'reactDataPreview', 19 | libraryTarget: 'umd', 20 | umdNamedDefine: true 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.jsx?$/, 26 | exclude: /node_module/, 27 | use: ['babel-loader'] 28 | } 29 | ] 30 | }, 31 | resolve: { 32 | extensions: ['.js', '.jsx'] 33 | } 34 | }; 35 | --------------------------------------------------------------------------------