├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .istanbul.yml ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── AddWidget.md ├── Dazzle.png ├── ImplementingACustomFrame.md ├── ImplementingCustomAddWidgetButton.md └── images │ ├── AddWidget.png │ └── Frame.Png ├── lib ├── components │ ├── AddWidget.js │ ├── Column.js │ ├── Dashboard.js │ ├── DefaultFrame.js │ ├── ItemTypes.js │ ├── LayoutRenderer.js │ ├── Row.js │ ├── WidgetFrame.js │ └── Widgets.js ├── index.js ├── style │ └── style.css └── util │ └── index.js ├── package-lock.json ├── package.json ├── sample ├── assets │ └── Dazzle.png ├── components │ ├── AddWidgetDialog.jsx │ ├── Container.jsx │ ├── CustomAddWidgetButton.jsx │ ├── EditBar.jsx │ ├── Header.jsx │ ├── app.jsx │ └── widgets │ │ ├── AnotherWidget │ │ └── index.jsx │ │ └── HelloWorld │ │ └── index.jsx ├── css │ └── custom.css ├── index.html └── index.js ├── server ├── node-app-server.js ├── node-proxy.js ├── node-server.js ├── proxy-config.js └── webpack-dev-proxy.js ├── test ├── components │ ├── AddWidget.spec.js │ ├── Column.spec.js │ ├── Dashboard.spec.js │ ├── Row.spec.js │ ├── WidgetFrame.spec.js │ └── Widgets.spec.js ├── entry.js ├── fake │ ├── ContainerWithDndContext.jsx │ ├── TestComponent.jsx │ └── TestCustomFrame.jsx └── util │ └── until.spec.js ├── webpack.config.js └── webpack.config.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": [ "istanbul" ] 5 | } 6 | }, 7 | "presets": ["env", "react", "stage-0"], 8 | "plugins": ["transform-decorators-legacy"] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | bin/** 2 | dist/** 3 | build/** 4 | tmp/** 5 | coverage/** 6 | node_modules/** 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", // https://github.com/babel/babel-eslint 3 | "plugins": [ 4 | "react" // https://github.com/yannickcr/eslint-plugin-react 5 | ], 6 | "env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments 7 | "browser": true, // browser global variables 8 | "node": true // Node.js global variables and Node.js-specific rules 9 | }, 10 | "parserOptions": { 11 | "ecmaVersion": 2018 12 | }, 13 | "rules": { 14 | /** 15 | * Strict mode 16 | */ 17 | "strict": [2, "never"], // http://eslint.org/docs/rules/strict 18 | 19 | /** 20 | * ES6 21 | */ 22 | "no-var": 2, // http://eslint.org/docs/rules/no-var 23 | "prefer-const": 2, // http://eslint.org/docs/rules/prefer-const 24 | 25 | /** 26 | * Variables 27 | */ 28 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow 29 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 30 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars 31 | "vars": "local", 32 | "args": "after-used" 33 | }], 34 | "no-use-before-define": 0, // http://eslint.org/docs/rules/no-use-before-define 35 | 36 | /** 37 | * Possible errors 38 | */ 39 | "comma-dangle": [2, "always-multiline"], // http://eslint.org/docs/rules/comma-dangle 40 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign 41 | "no-console": 1, // http://eslint.org/docs/rules/no-console 42 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger 43 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert 44 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition 45 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 46 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case 47 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 48 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 49 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast 50 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 51 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 52 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 53 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 54 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 55 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 56 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 57 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 58 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 59 | "block-scoped-var": 0, // http://eslint.org/docs/rules/block-scoped-var 60 | 61 | /** 62 | * Best practices 63 | */ 64 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 65 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly 66 | "default-case": 2, // http://eslint.org/docs/rules/default-case 67 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation 68 | "allowKeywords": true 69 | }], 70 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq 71 | "guard-for-in": 0, // http://eslint.org/docs/rules/guard-for-in 72 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 73 | "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return 74 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null 75 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval 76 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 77 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind 78 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough 79 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 80 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 81 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 82 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 83 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str 84 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 85 | "no-new": 2, // http://eslint.org/docs/rules/no-new 86 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 87 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 88 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 89 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 90 | "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign 91 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 92 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 93 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 94 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 95 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 96 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 97 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 98 | "no-with": 2, // http://eslint.org/docs/rules/no-with 99 | "radix": 2, // http://eslint.org/docs/rules/radix 100 | "vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top 101 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife 102 | "yoda": 2, // http://eslint.org/docs/rules/yoda 103 | 104 | /** 105 | * Style 106 | */ 107 | "indent": [2, 2], // http://eslint.org/docs/rules/indent 108 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style 109 | "1tbs", { 110 | "allowSingleLine": true 111 | }], 112 | "quotes": [ 113 | 2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes 114 | ], 115 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase 116 | "properties": "never" 117 | }], 118 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing 119 | "before": false, 120 | "after": true 121 | }], 122 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style 123 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last 124 | "func-names": 1, // http://eslint.org/docs/rules/func-names 125 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing 126 | "beforeColon": false, 127 | "afterColon": true 128 | }], 129 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap 130 | "newIsCap": true, 131 | "capIsNew": false 132 | }], 133 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines 134 | "max": 2 135 | }], 136 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary 137 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 138 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 139 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 140 | "no-extra-parens": [2, "functions"], // http://eslint.org/docs/rules/no-extra-parens 141 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 142 | "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var 143 | "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks 144 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi 145 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing 146 | "before": false, 147 | "after": true 148 | }], 149 | "keyword-spacing": 2, // http://eslint.org/docs/rules/keyword-spacing 150 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 151 | "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren 152 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 153 | "spaced-comment": 2, // http://eslint.org/docs/rules/spaced-comment 154 | 155 | /** 156 | * JSX style 157 | */ 158 | "react/display-name": 0, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/display-name.md 159 | "react/jsx-boolean-value": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-boolean-value.md 160 | "jsx-quotes": [2, "prefer-double"], // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-quotes.md 161 | "react/jsx-no-undef": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-undef.md 162 | "react/jsx-sort-props": 0, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-sort-props.md 163 | "react/jsx-sort-prop-types": 0, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-sort-prop-types.md 164 | "react/jsx-uses-react": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-uses-react.md 165 | "react/jsx-uses-vars": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-uses-vars.md 166 | "react/no-did-mount-set-state": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-did-mount-set-state.md 167 | "react/no-did-update-set-state": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-did-update-set-state.md 168 | "react/no-multi-comp": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-multi-comp.md 169 | "react/no-unknown-property": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-unknown-property.md 170 | "react/prop-types": 0, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/prop-types.md 171 | "react/react-in-jsx-scope": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/react-in-jsx-scope.md 172 | "react/self-closing-comp": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/self-closing-comp.md 173 | "react/sort-comp": [2, { // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/sort-comp.md 174 | "order": [ 175 | "displayName", 176 | "propTypes", 177 | "contextTypes", 178 | "childContextTypes", 179 | "mixins", 180 | "statics", 181 | "defaultProps", 182 | "/^_(?!(on|get|render))/", 183 | "constructor", 184 | "getDefaultProps", 185 | "getInitialState", 186 | "state", 187 | "getChildContext", 188 | "componentWillMount", 189 | "componentDidMount", 190 | "componentWillReceiveProps", 191 | "shouldComponentUpdate", 192 | "componentWillUpdate", 193 | "componentDidUpdate", 194 | "componentWillUnmount", 195 | "/^_?on.+$/", 196 | "/^_?get.+$/", 197 | "/^_?render.+$/", 198 | "render" 199 | ] 200 | }] 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | .DS_Store 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # Build directory 31 | # dist 32 | 33 | # Editor temporary files 34 | *~ 35 | \#*# 36 | .#* 37 | dist/lib.js 38 | dist/lib.js.map 39 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | root: ./lib 3 | extensions: ['.js', '.jsx'] 4 | reporting: 5 | print: summary 6 | reports: 7 | - lcov 8 | dir: ./coverage 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '8' 10 | before_install: 11 | - npm i -g npm@^5.0.0 12 | - npm i -g codecov 13 | before_script: 14 | - npm prune 15 | script: 16 | - npm run build 17 | - npm run test 18 | after_success: 19 | - npm run report-cover 20 | branches: 21 | except: 22 | - "/^v\\d+\\.\\d+\\.\\d+$/" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Raathigeshan Kugarajan 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 |

2 | Dazzle 3 |
4 | React Dazzle 5 |
6 |

Dashboards made easy in React JS

7 | 8 | 9 |

10 | 11 | License 13 | 14 | 15 | NPM Version 17 | 18 | 19 | Travis Build 21 | 22 | 23 | Coverage via Codecov 24 | 25 |

26 |
27 | 28 | **Looking for maintainers https://github.com/Raathigesh/dazzle/issues/41** 29 | 30 | Dazzle is a library for building dashboards with React JS. Dazzle does not depend on any front-end libraries but it makes it easier to integrate with them. 31 | 32 | Dazzle's goal is to be flexible and simple. Even though there are some UI components readily available out of the box, you have the complete control to override them as you wish with your own styles and layout. 33 | 34 | ## Features 35 | - Grid based layout 36 | - Add/Remove widgets 37 | - Drag and drop widget re-ordering 38 | - UI framework agnostic 39 | - Simple yet flexible 40 | - Well documented (It's a feature! Don't you think?) 41 | 42 | ## Installation 43 | ``` 44 | $ npm install react-dazzle --save 45 | ``` 46 | 47 | ## Dazzle me 48 | [Here is a demo.](http://raathigesh.com/dazzle) Widgets shows fake data though but they look so damn cool (At least for me). 49 | 50 | ###### [Repository of the demo is available here.](https://github.com/Raathigesh/Dazzle-Starter-Kit) 51 | 52 | ## Usage 53 | ```javascript 54 | import React, { Component } from 'react'; 55 | import Dashboard from 'react-dazzle'; 56 | 57 | // Your widget. Just another react component. 58 | import CounterWidget from './widgets/CounterWidget'; 59 | 60 | // Default styles. 61 | import 'react-dazzle/lib/style/style.css'; 62 | 63 | class App extends Component { 64 | constructor() { 65 | this.state = { 66 | widgets: { 67 | WordCounter: { 68 | type: CounterWidget, 69 | title: 'Counter widget', 70 | } 71 | }, 72 | layout: { 73 | rows: [{ 74 | columns: [{ 75 | className: 'col-md-12', 76 | widgets: [{key: 'WordCounter'}], 77 | }], 78 | }], 79 | } 80 | }; 81 | } 82 | 83 | render() { 84 | return 85 | } 86 | } 87 | ``` 88 | 89 | Dazzle uses [react-dnd](https://github.com/react-dnd/react-dnd). The default _Dashboard_ component of Dazzle is wrapped by [_DragDropContext_](https://react-dnd.github.io/react-dnd/docs-drag-drop-context.html) of react-dnd. 90 | So you may want to use react-dnd in your React component hierarchy upper than where you use the _Dashboard_ component of Dazzle. If you do so then you can't let Dazzle creating the _DragDropContext_ because you want to create it yourself upper in the React component hierarchy of your application. 91 | So forth please use the _DashboardWithoutDndContext_ component of Dazzle and wrapped your own component with _DragDropContext(HTML5Backend)_: 92 | ```javascript 93 | import React, { Component } from 'react'; 94 | import { DashboardWithoutDndContext } from 'react-dazzle'; 95 | 96 | // react-dnd 97 | import { DragDropContext } from 'react-dnd'; 98 | import HTML5Backend from 'react-dnd-html5-backend'; 99 | 100 | // Your widget. Just another react component. 101 | import CounterWidget from './widgets/CounterWidget'; 102 | 103 | // Default styles. 104 | import 'react-dazzle/lib/style/style.css'; 105 | 106 | class App extends Component { 107 | constructor() { 108 | this.state = { 109 | widgets: { 110 | WordCounter: { 111 | type: CounterWidget, 112 | title: 'Counter widget', 113 | } 114 | }, 115 | layout: { 116 | rows: [{ 117 | columns: [{ 118 | className: 'col-md-12', 119 | widgets: [{key: 'WordCounter'}], 120 | }], 121 | }], 122 | } 123 | }; 124 | } 125 | 126 | render() { 127 | return 128 | } 129 | } 130 | 131 | export default DragDropContext(HTML5Backend)(App); 132 | ``` 133 | 134 | ## API 135 | | Props | Type| Description | Required | 136 | | --- | --- | --- | --- | 137 | | layout | Object | Layout of the dashboard. | Yes | 138 | | widgets | Object| Widgets that could be added to the dashboard. | Yes | 139 | | editable | Boolean |Indicates whether the dashboard is in editable mode. | No | 140 | | rowClass | String |CSS class name(s) that should be given to the row div element. Default is `row`. | No | 141 | | editableColumnClass | String |CSS class name(s) that should be used when a column is in editable mode. | No | 142 | | droppableColumnClass | String |CSS class name(s) that should be used when a widget is about to be dropped in a column. | No | 143 | | frameComponent | Component | Customized frame component which should be used instead of the default frame. [More on custom frame component.](https://github.com/Raathigesh/Dazzle/blob/master/docs/ImplementingACustomFrame.md) | No | 144 | | addWidgetComponent | Component | Customized add widget component which should be used instead of the default AddWidget component. [More on custom add widget component.](https://github.com/Raathigesh/Dazzle/blob/master/docs/ImplementingCustomAddWidgetButton.md) | No | 145 | | addWidgetComponentText | String | Text that should be displayed in the Add Widget component. Default is `Add Widget`. | No | 146 | | onAdd(layout, rowIndex, columnIndex) | function |Will be called when user clicks the `AddWidget` component.| No | 147 | | onRemove(layout) | function |Will be called when a widget is removed.| No | 148 | | onMove(layout) | function | Will be called when a widget is moved.| No | 149 | 150 | #### Providing `widgets` 151 | `widgets` prop of the dashboard component takes an object. A sample `widgets` object would look like below. This object holds all the widgets that could be used in the dashboard. 152 | 153 | ```javascript 154 | { 155 | HelloWorldWidget: { 156 | type: HelloWorld, 157 | title: 'Hello World Title', 158 | props: { 159 | text: 'Hello Humans!' 160 | } 161 | }, 162 | AnotherWidget: { 163 | type: AnotherWidget, 164 | title: 'Another Widget Title' 165 | } 166 | } 167 | ``` 168 | - `type` property - Should be a React component function or class. 169 | - `title` property - Title of the widget that should be displayed on top of the widget. 170 | - `props` property - Props that should be provided to the widget. 171 | 172 | 173 | #### Dashboard `layout` 174 | The `layout` prop takes the current layout of the dashboard. Layout could have multiple rows and columns. A sample layout object with a single row and two columns would look like below. 175 | 176 | ```javascript 177 | { 178 | rows: [{ 179 | columns: [{ 180 | className: 'col-md-6 col-sm-6 col-xs-12', 181 | widgets: [{key: 'HelloWorldWidget'}] 182 | }, { 183 | className: 'col-md-6 col-sm-6 col-xs-12', 184 | widgets: [{key: 'AnotherWidget'}] 185 | }] 186 | }] 187 | } 188 | ``` 189 | - `className` property - CSS class(es) that should be given to the column in the grid layout. Above sample layout uses the classes from bootstrap library. You could use the classes of your CSS library. 190 | - `widgets` property - An array of widgets that should be rendered in that particular column. `key` property of the widgets array should be a key from the `widgets` object. 191 | 192 | #### Edit mode 193 | Setting `editable` prop to `true` will make the dashboard editable. 194 | 195 | #### Add new widget 196 | When user tries to add a new widget, the `onAdd` callback will be called. More info here on how to handle widget addition. 197 | 198 | #### Remove a widget 199 | When a widget is removed, `onRemove` method will be called and new layout (The layout with the widget removed) will be available as an argument of `onRemove` method. Set the provided layout again to the dashboard to complete the widget removal. [The Sample repository has the this feature implemented](https://github.com/Raathigesh/Dazzle-Starter-Kit/blob/master/src/components/Dashboard.jsx). 200 | 201 | ## Customization 202 | 203 | #### Implementing custom `WidgetFrame` component 204 | A frame is the component which surrounds a widget. A frame has the title and the close button. Dazzle provides a default frame out of the box. But if you want, you can customize the frame as you like. More info here. 205 | 206 | #### Implementing custom `AddWidget` component 207 | Dazzle also allows you to customize the `Add Widget` component which appears when you enter edit mode. More info here. 208 | 209 | ## Issues 210 | - Improve drag and drop experience ([#1](https://github.com/Raathigesh/Dazzle/issues/1)) 211 | 212 | ## License 213 | MIT © [Raathigeshan](https://twitter.com/Raathigeshan) 214 | 215 | 216 | Sponsor 217 | 218 | -------------------------------------------------------------------------------- /docs/AddWidget.md: -------------------------------------------------------------------------------- 1 | ## Add new widget to dashboard 2 | 3 | When add widget is clicked, `onAdd` function will be called. The `onAdd` function will be provided with the current `layout`, index of the `row` and `column` where the new widget should be added. 4 | 5 | You could add a new widget to the dashboard by calling the method `addWidget` from dazzle and passing the parameters you received from the `onAdd` callback along with the key of the widget that should be added, 6 | 7 | Below is a sample of adding a widget when `Add Widget` is clicked. 8 | 9 | ```javascript 10 | import React, { Component } from 'react'; 11 | import Dashboard, { addWidget } from 'react-dazzle'; 12 | import HelloWorld from './widgets/HelloWorld'; 13 | 14 | class App extends Component { 15 | constructor() { 16 | this.state = { 17 | widgets: { 18 | GreetingsWidget: { 19 | type: HelloWorld, 20 | title: 'Hello World Greetings', 21 | } 22 | }, 23 | layout: { 24 | rows: [{ 25 | columns: [{ 26 | className: 'col-md-12', 27 | widgets: [{key: 'GreetingsWidget'}], 28 | }], 29 | }], 30 | } 31 | }; 32 | } 33 | 34 | onAdd = (layout, rowIndex, columnIndex) => { 35 | // Add another Greetings Widget 36 | this.setState({ 37 | layout: addWidget(layout, rowIndex, columnIndex, 'GreetingsWidget'), 38 | }); 39 | } 40 | 41 | render() { 42 | return ( 43 | Below example uses [React's state-less components](https://facebook.github.io/react/docs/reusable-components.html#stateless-functions). 11 | 12 | ```javascript 13 | const CustomFrame = ({title, editable, children, onRemove }) => { 14 | return ( 15 |
16 |
17 |

{title}

18 | {editable && {onRemove();}} >Remove} 19 |
20 |
21 | {children} 22 |
23 |
24 | ); 25 | }; 26 | ``` 27 | 28 | A custom frame is just another React component. Custom frame will be provided with 4 props. 29 | 30 | | Props | Type | Description | 31 | | --- | --- | --- | 32 | | title | String | Title that should be displayed in the frame. | 33 | | editable | Boolean | Denotes weather the dashboard is in editable mode or not. | 34 | | children | React element | Children of the frame. The widget that is going to be rendered. | 35 | | onRemove | function | The function that should be called when you want to remove the widget. | 36 | 37 | ### How to let dazzle know about this? 38 | Well, it's extremely simple. 39 | 40 | ```javascript 41 | import Dashboard from 'react-dazzle'; 42 | import CustomFrame from './CustomFrame'; 43 | 44 | 45 | ``` 46 | 47 | Now all the widgets will use your fancy frame. 48 | 49 | #### More docs 50 | - [Readme](../README.md) 51 | - [Add a widget](./AddWidget.md) 52 | - [Implementing custom Frame component](./ImplementingACustomFrame.md) 53 | - [Implementing custom AddWidget component](./ImplementingCustomAddWidgetButton.md) 54 | -------------------------------------------------------------------------------- /docs/ImplementingCustomAddWidgetButton.md: -------------------------------------------------------------------------------- 1 | ## Implementing a custom `AddWidget` component 2 | Add widget component is the one which appears on top of columns when dashboard is in edit mode. When you click on it, it will allow you to add new widgets. Dazzle by default comes with a default `AddWidget` component. If you wish you could customize this component as you prefer. 3 | 4 | ![Add Widget](./images/AddWidget.png) 5 | 6 | ### Show me the code 7 | 8 | > Below example uses [React's state-less components](https://facebook.github.io/react/docs/reusable-components.html#stateless-functions). 9 | 10 | ```javascript 11 | const CustomAddWidgetButton = ({text, onClick}) => { 12 | return ( 13 |
14 | 15 |
16 | ); 17 | }; 18 | ``` 19 | 20 | A custom `AddWidget` component is just another React component. This component will be provided with 2 props. 21 | 22 | | Props | Type | Description | 23 | | --- | --- | --- | 24 | | text | String | Text that should be displayed. This text is provided through the `addWidgetComponentText` prop of dashboard. | 25 | | onClick | function | The function should be called when user wants to add a widget. | 26 | 27 | ### How to let dazzle know about this? 28 | ```javascript 29 | import Dashboard from 'react-dazzle'; 30 | import CustomAddWidget from './CustomAddWidget'; 31 | 32 | 33 | ``` 34 | 35 | #### More docs 36 | - [Readme](../README.md) 37 | - [Add a widget](./AddWidget.md) 38 | - [Implementing custom Frame component](./ImplementingACustomFrame.md) 39 | - [Implementing custom AddWidget component](./ImplementingCustomAddWidgetButton.md) 40 | -------------------------------------------------------------------------------- /docs/images/AddWidget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Raathigesh/dazzle/c4a46f62401a0a1b34efa3a45134a6a34a121770/docs/images/AddWidget.png -------------------------------------------------------------------------------- /docs/images/Frame.Png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Raathigesh/dazzle/c4a46f62401a0a1b34efa3a45134a6a34a121770/docs/images/Frame.Png -------------------------------------------------------------------------------- /lib/components/AddWidget.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * Default AddWidget component. 6 | * @param {[type]} {text [description] 7 | * @param {[type]} onClick} [description] 8 | * @return {[type]} [description] 9 | */ 10 | const AddWidget = ({ text, onClick }) => ( 11 |
12 | {text} 13 |
14 | ); 15 | 16 | AddWidget.propTypes = { 17 | /** 18 | * Should be called when 'add' is clicked 19 | */ 20 | onClick: PropTypes.func, 21 | 22 | /** 23 | * Text that should be displyed in the component 24 | */ 25 | text: PropTypes.string, 26 | }; 27 | 28 | AddWidget.defaultProps = { 29 | text: 'Add Widget', 30 | }; 31 | 32 | export default AddWidget; 33 | -------------------------------------------------------------------------------- /lib/components/Column.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createElement } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { DropTarget } from 'react-dnd'; 4 | import { WIDGET } from './ItemTypes'; 5 | import AddWidget from './AddWidget'; 6 | import { moveWidget } from '../util'; 7 | 8 | const columnTarget = { 9 | drop(props, monitor) { 10 | const { layout, rowIndex, columnIndex, onMove } = props; 11 | const item = monitor.getItem(); 12 | if (item.columnIndex !== columnIndex || item.rowIndex !== rowIndex) { 13 | const movedLayout = moveWidget(layout, { 14 | rowIndex: item.rowIndex, 15 | columnIndex: item.columnIndex, 16 | widgetIndex: item.widgetIndex, 17 | }, { 18 | rowIndex, 19 | columnIndex, 20 | }, item.widgetName); 21 | onMove(movedLayout); 22 | } 23 | }, 24 | }; 25 | 26 | /** 27 | * Colum of the dashboard grid. A column holds multiple widgets. 28 | */ 29 | @DropTarget(WIDGET, columnTarget, (connect, monitor) => ({ 30 | connectDropTarget: connect.dropTarget(), 31 | isOver: monitor.isOver(), 32 | canDrop: monitor.canDrop(), 33 | })) 34 | class Column extends Component { 35 | render() { 36 | const { 37 | className, 38 | layout, 39 | rowIndex, 40 | columnIndex, 41 | editable, 42 | children, 43 | connectDropTarget, 44 | onAdd, 45 | isOver, 46 | canDrop, 47 | editableColumnClass, 48 | droppableColumnClass, 49 | addWidgetComponentText, 50 | addWidgetComponent, 51 | } = this.props; 52 | 53 | let classes = className; 54 | classes = editable ? `${className} ${editableColumnClass}` : classes; 55 | const isActive = isOver && canDrop; 56 | classes = isActive ? `${classes} ${droppableColumnClass}` : classes; 57 | 58 | let addWidgetComponentToUse = null; 59 | if (addWidgetComponent) { 60 | // eslint max-len=off 61 | addWidgetComponentToUse = createElement(addWidgetComponent, { text: addWidgetComponentText, onClick: () => {onAdd(layout, rowIndex, columnIndex);} }); // eslint-disable-line 62 | } else { 63 | addWidgetComponentToUse = {onAdd(layout, rowIndex, columnIndex);}}/>; // eslint-disable-line 64 | } 65 | 66 | return ( 67 | connectDropTarget( 68 |
69 | {editable && addWidgetComponentToUse} 70 | { children } 71 |
72 | ) 73 | ); 74 | } 75 | } 76 | 77 | Column.propTypes = { 78 | /** 79 | * Children of the column 80 | */ 81 | children: PropTypes.node, 82 | 83 | /** 84 | * CSS class that should be used with the column. 85 | */ 86 | className: PropTypes.string, 87 | 88 | /** 89 | * Function that should be called when user tries to add a widget 90 | * to the column. 91 | */ 92 | onAdd: PropTypes.func, 93 | 94 | /** 95 | * Layout of the dashboard. 96 | */ 97 | layout: PropTypes.object, 98 | 99 | /** 100 | * Index of the row that this column resides. 101 | */ 102 | rowIndex: PropTypes.number, 103 | 104 | /** 105 | * Index of this column. 106 | */ 107 | columnIndex: PropTypes.number, 108 | 109 | /** 110 | * Indicates weather dashboard is in editable state 111 | */ 112 | editable: PropTypes.bool, 113 | 114 | /** 115 | * Indicates weather a widget is being draged over. 116 | */ 117 | isOver: PropTypes.bool, 118 | 119 | /** 120 | * Indicated a widget can be dropped. 121 | */ 122 | canDrop: PropTypes.bool, 123 | 124 | /** 125 | * Class to be used for columns in editable mode. 126 | */ 127 | editableColumnClass: PropTypes.string, 128 | 129 | /** 130 | * CSS class to be used for columns when a widget is droppable. 131 | */ 132 | droppableColumnClass: PropTypes.string, 133 | 134 | /** 135 | * Text that should be given to the AddWidget component. 136 | */ 137 | addWidgetComponentText: PropTypes.string, 138 | 139 | /** 140 | * ReactDnd's connectDropTarget. 141 | */ 142 | connectDropTarget: PropTypes.func, 143 | 144 | /** 145 | * Customized AddWidget component. 146 | */ 147 | addWidgetComponent: PropTypes.func, 148 | }; 149 | 150 | Column.defaultProps = { 151 | editableColumnClass: 'editable-column', 152 | droppableColumnClass: 'droppable-column', 153 | }; 154 | 155 | export default Column; 156 | -------------------------------------------------------------------------------- /lib/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { DragDropContext } from 'react-dnd'; 4 | import HTML5Backend from 'react-dnd-html5-backend'; 5 | import LayoutRenderer from './LayoutRenderer'; 6 | 7 | /** 8 | * Main dashboard component. This is where all of this starts. 9 | */ 10 | /* eslint react/prefer-stateless-function: "off" */ 11 | class Dashboard extends Component { 12 | render() { 13 | return ( 14 |
15 | 16 |
17 | ); 18 | } 19 | } 20 | 21 | Dashboard.propTypes = { 22 | /** 23 | * The layout of the dashboard. 24 | */ 25 | layout: PropTypes.object, 26 | 27 | /** 28 | * List of widgets that are avilable in the dashboard. 29 | */ 30 | widgets: PropTypes.object, 31 | 32 | /** 33 | * Indicates weather the dashoard is in editable state or not. 34 | */ 35 | editable: PropTypes.bool, 36 | 37 | /** 38 | * CSS class name that should be provided to the row. Default is 'row'. 39 | */ 40 | rowClass: PropTypes.string, 41 | 42 | /** 43 | * Customized widget frame. The dashboard supports a default frame. But if 44 | * it doesn't suit your needs or the look and feel is not what you wanted, you 45 | * could create your own widget frame and pass it through here. Ever widget Will 46 | * use this as the outer container which displays controls like 'remove' button 47 | * on edit mode. 48 | */ 49 | frameComponent: PropTypes.func, 50 | 51 | /** 52 | * A custom component for the `add widget` button. 53 | */ 54 | addWidgetComponent: PropTypes.func, 55 | 56 | /** 57 | * Class to be used for columns in editable mode. 58 | */ 59 | editableColumnClass: PropTypes.string, 60 | 61 | /** 62 | * CSS class to be used for columns when a widget is droppable. 63 | */ 64 | droppableColumnClass: PropTypes.string, 65 | 66 | /** 67 | * Text that should be displayed in the `AddWidget` component. 68 | */ 69 | addWidgetComponentText: PropTypes.string, 70 | 71 | /** 72 | * Will be called when a widget removed by the user from the dashboard. 73 | * Should be handled if the dashbord supports edit functionality. 74 | * provides the updated layout object. This layout object with the removed widget 75 | * should be given back to the dashboard through the layout prop to re-render the dashboard. 76 | */ 77 | onRemove: PropTypes.func, 78 | 79 | /** 80 | * Will be called when user tries to add a widget into a column. 81 | */ 82 | onAdd: PropTypes.func, 83 | 84 | /** 85 | * Function to be called when a widget is moved by the user. 86 | */ 87 | onMove: PropTypes.func, 88 | /** 89 | * Function to be called when a widget is edited. 90 | */ 91 | onEdit: PropTypes.func, 92 | }; 93 | 94 | export { Dashboard as DashboardWithoutDndContext }; 95 | export default DragDropContext(HTML5Backend)(Dashboard); 96 | -------------------------------------------------------------------------------- /lib/components/DefaultFrame.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * Default frame that will be used with the widgets. 6 | */ 7 | const DefaultFrame = ({ children, onRemove, onEdit, editable, title }) => ( 8 |
9 |
10 | {title} 11 | {editable && onEdit()}>Edit} 12 | {editable && onRemove()}>Remove} 13 |
14 | {children} 15 |
16 | ); 17 | 18 | DefaultFrame.propTypes = { 19 | /** 20 | * Indicates weather the dashboard is in editable mode. 21 | */ 22 | editable: PropTypes.bool, 23 | 24 | /** 25 | * Children of the frame. 26 | */ 27 | children: PropTypes.node, 28 | 29 | /** 30 | * Function to call when the widget is removed. 31 | */ 32 | onRemove: PropTypes.func, 33 | 34 | /** 35 | * Title of the widget 36 | */ 37 | title: PropTypes.string, 38 | }; 39 | 40 | export default DefaultFrame; 41 | -------------------------------------------------------------------------------- /lib/components/ItemTypes.js: -------------------------------------------------------------------------------- 1 | export const WIDGET = 'WIDGET'; 2 | -------------------------------------------------------------------------------- /lib/components/LayoutRenderer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Row from './Row'; 4 | 5 | /** 6 | * Renders the row, column layout based on the layout provided to the dashboard. 7 | */ 8 | const LayoutRenderer = (props) => { 9 | const { 10 | layout, 11 | widgets, 12 | onRemove, 13 | editable, 14 | onAdd, 15 | frameComponent, 16 | rowClass, 17 | onMove, 18 | onEdit, 19 | editableColumnClass, 20 | droppableColumnClass, 21 | addWidgetComponentText, 22 | addWidgetComponent, 23 | } = props; 24 | 25 | const rows = layout.rows.map((row, rowIndex) => { // eslint-disable-line arrow-body-style 26 | return ( 27 | 45 | ); 46 | }); 47 | 48 | return ( 49 |
50 | {rows} 51 |
52 | ); 53 | }; 54 | 55 | LayoutRenderer.propTypes = { 56 | /** 57 | * Layout of the dashboard. 58 | */ 59 | layout: PropTypes.object, 60 | 61 | /** 62 | * Widgets that the dashboard supports. 63 | */ 64 | widgets: PropTypes.object, 65 | 66 | /** 67 | * Indicates weather this dashboard is in editable mode. 68 | */ 69 | editable: PropTypes.bool, 70 | 71 | /** 72 | * Function that will be called when user removed a widget. 73 | */ 74 | onRemove: PropTypes.func, 75 | 76 | /** 77 | * Function that will be called user tries to add a widget. 78 | */ 79 | onAdd: PropTypes.func, 80 | 81 | /** 82 | * Frame that should be used as the outer cotnainer of the widget. 83 | */ 84 | frameComponent: PropTypes.func, 85 | 86 | /** 87 | * Class name that should be provided to the row component. 88 | */ 89 | rowClass: PropTypes.string, 90 | 91 | /** 92 | * Function to be called when a widget is moved by the user. 93 | */ 94 | onMove: PropTypes.func, 95 | 96 | onEdit: PropTypes.func, 97 | 98 | /** 99 | * Class to be used for columns in editable mode. 100 | */ 101 | editableColumnClass: PropTypes.string, 102 | 103 | /** 104 | * CSS class to be used for columns when a widget is droppable. 105 | */ 106 | droppableColumnClass: PropTypes.string, 107 | 108 | /** 109 | * Customized AddWidget component. 110 | */ 111 | addWidgetComponent: PropTypes.func, 112 | 113 | /** 114 | * Text that should be displayed in the `AddWidget` component. 115 | */ 116 | addWidgetComponentText: PropTypes.string, 117 | }; 118 | 119 | LayoutRenderer.defaultProps = { 120 | /** 121 | * Default layout. 122 | */ 123 | layout: { 124 | rows: [], 125 | }, 126 | }; 127 | 128 | export default LayoutRenderer; 129 | -------------------------------------------------------------------------------- /lib/components/Row.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Column from './Column'; 4 | import Widgets from './Widgets'; 5 | 6 | /** 7 | * Returns a set of columns that belongs to a row. 8 | */ 9 | function Row(props) { 10 | const { 11 | rowClass, 12 | columns, 13 | widgets, 14 | onRemove, 15 | layout, 16 | rowIndex, 17 | editable, 18 | frameComponent, 19 | editableColumnClass, 20 | droppableColumnClass, 21 | addWidgetComponentText, 22 | addWidgetComponent, 23 | onAdd, 24 | onMove, 25 | onEdit, 26 | } = props; 27 | 28 | const items = columns.map((column, index) => { // eslint-disable-line arrow-body-style 29 | return ( 30 | 44 | 58 | 59 | ); 60 | }); 61 | 62 | return ( 63 |
64 | {items} 65 |
66 | ); 67 | } 68 | 69 | Row.propTypes = { 70 | /** 71 | * CSS class that should be used to represent a row. 72 | */ 73 | rowClass: PropTypes.string, 74 | 75 | /** 76 | * Columns of the layout. 77 | */ 78 | columns: PropTypes.array, 79 | 80 | /** 81 | * Widgets that should be used in the dashboard. 82 | */ 83 | widgets: PropTypes.object, 84 | 85 | /** 86 | * Layout of the dashboard. 87 | */ 88 | layout: PropTypes.object, 89 | 90 | /** 91 | * Index of the row where this column is in. 92 | */ 93 | rowIndex: PropTypes.number, 94 | 95 | /** 96 | * Indicates weather the dashboard is in editable mode or not. 97 | */ 98 | editable: PropTypes.bool, 99 | 100 | /** 101 | * Custom frame that should be used with the widget. 102 | */ 103 | frameComponent: PropTypes.func, 104 | 105 | /** 106 | * Class to be used for columns in editable mode. 107 | */ 108 | editableColumnClass: PropTypes.string, 109 | 110 | /** 111 | * CSS class to be used for columns when a widget is droppable. 112 | */ 113 | droppableColumnClass: PropTypes.string, 114 | 115 | /** 116 | * Custom AddWidget component. 117 | */ 118 | addWidgetComponent: PropTypes.func, 119 | 120 | /** 121 | * Text that should be displyed in the AddWidget component. 122 | */ 123 | addWidgetComponentText: PropTypes.string, 124 | 125 | /** 126 | * Method that should be called when a component is added. 127 | */ 128 | onAdd: PropTypes.func, 129 | 130 | /** 131 | * Method that should be called when a component is removed. 132 | */ 133 | onRemove: PropTypes.func, 134 | 135 | /** 136 | * Method that should be called when a widget is moved. 137 | */ 138 | onMove: PropTypes.func, 139 | onEdit: PropTypes.func, 140 | }; 141 | 142 | Row.defaultProps = { 143 | /** 144 | * Most CSS grid systems uses 'row' as the class name. Or not ? 145 | */ 146 | rowClass: 'row', 147 | }; 148 | 149 | export default Row; 150 | -------------------------------------------------------------------------------- /lib/components/WidgetFrame.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createElement } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { findDOMNode } from 'react-dom'; 4 | import { DragSource, DropTarget } from 'react-dnd'; 5 | import { WIDGET } from './ItemTypes'; 6 | import { removeWidget, sortWidget } from '../util'; 7 | import DefaultFrame from './DefaultFrame'; 8 | 9 | const boxSource = { 10 | beginDrag(props) { 11 | return { 12 | widgetName: props.widgetName, 13 | rowIndex: props.rowIndex, 14 | columnIndex: props.columnIndex, 15 | widgetIndex: props.widgetIndex, 16 | }; 17 | }, 18 | }; 19 | 20 | const cardTarget = { 21 | hover(props, monitor, component) { 22 | const dragIndex = monitor.getItem().widgetIndex; 23 | const hoverIndex = props.widgetIndex; 24 | 25 | // Don't replace items with themselves 26 | if (dragIndex === hoverIndex) { 27 | return; 28 | } 29 | 30 | // Determine rectangle on screen 31 | const hoverBoundingRect = findDOMNode(component).getBoundingClientRect(); 32 | 33 | // Get vertical middle 34 | const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; 35 | 36 | // Determine mouse position 37 | const clientOffset = monitor.getClientOffset(); 38 | 39 | // Get pixels to the top 40 | const hoverClientY = clientOffset.y - hoverBoundingRect.top; 41 | 42 | // Only perform the move when the mouse has crossed half of the items height 43 | // When dragging downwards, only move when the cursor is below 50% 44 | // When dragging upwards, only move when the cursor is above 50% 45 | 46 | // Dragging downwards 47 | if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { 48 | return; 49 | } 50 | 51 | // Dragging upwards 52 | if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { 53 | return; 54 | } 55 | 56 | // Time to actually perform the action 57 | const { layout, columnIndex, rowIndex } = props; 58 | 59 | if (monitor.getItem().rowIndex === rowIndex && monitor.getItem().columnIndex === columnIndex) { 60 | const newLayout = sortWidget(layout, { 61 | rowIndex, 62 | columnIndex, 63 | widgetIndex: dragIndex, 64 | }, { 65 | rowIndex, 66 | columnIndex, 67 | widgetIndex: hoverIndex, 68 | }, monitor.getItem().widgetName); 69 | 70 | props.onMove(newLayout); 71 | 72 | // Note: we're mutating the monitor item here! 73 | // Generally it's better to avoid mutations, 74 | // but it's good here for the sake of performance 75 | // to avoid expensive index searches. 76 | monitor.getItem().widgetIndex = hoverIndex; // eslint-disable-line no-param-reassign 77 | } 78 | }, 79 | }; 80 | 81 | /** 82 | * Frame component which surrounds each widget. 83 | */ 84 | @DropTarget(WIDGET, cardTarget, connect => ({ 85 | connectDropTarget: connect.dropTarget(), 86 | })) 87 | @DragSource(WIDGET, boxSource, (connect, monitor) => ({ 88 | connectDragSource: connect.dragSource(), 89 | isDragging: monitor.isDragging(), 90 | })) 91 | class WidgetFrame extends Component { 92 | render() { 93 | const { 94 | frameComponent, 95 | children, 96 | editable, 97 | title, 98 | frameSettings, 99 | connectDragSource, 100 | connectDropTarget, 101 | isDragging, 102 | rowIndex, 103 | columnIndex, 104 | widgetIndex, 105 | } = this.props; 106 | 107 | let selected = null; 108 | 109 | if (frameComponent) { 110 | // if user provided a custom frame, use it 111 | selected = createElement(frameComponent, { 112 | children, 113 | editable, 114 | title, 115 | settings: frameSettings, 116 | onRemove: this.remove, 117 | onEdit: this.edit, 118 | rowIndex, 119 | columnIndex, 120 | widgetIndex, 121 | isDragging, 122 | }); 123 | } else { 124 | // else use the default frame 125 | selected = ( 126 | 133 | ); 134 | } 135 | const opacity = isDragging ? 0 : 1; 136 | const widgetFrame = ( 137 |
138 | {selected} 139 |
140 | ); 141 | 142 | return editable ? connectDragSource(connectDropTarget(widgetFrame)) : widgetFrame; 143 | } 144 | 145 | edit = () => { 146 | const { layout, rowIndex, columnIndex, widgetIndex } = this.props; 147 | this.props.onEdit(layout.rows[rowIndex].columns[columnIndex].widgets[widgetIndex].key); 148 | } 149 | 150 | remove = () => { 151 | const { layout, rowIndex, columnIndex, widgetIndex } = this.props; 152 | const newLayout = removeWidget(layout, rowIndex, columnIndex, widgetIndex); 153 | this.props.onRemove(newLayout, rowIndex, columnIndex, widgetIndex); 154 | } 155 | } 156 | 157 | WidgetFrame.propTypes = { 158 | /** 159 | * Childrens of the widget frame. 160 | */ 161 | children: PropTypes.element, 162 | 163 | 164 | /** 165 | * Layout of the dahsboard. 166 | */ 167 | layout: PropTypes.object, 168 | 169 | /** 170 | * Index of the column these widgets should be placed. 171 | */ 172 | columnIndex: PropTypes.number, 173 | 174 | /** 175 | * Index of the row these widgets should be placed. 176 | */ 177 | rowIndex: PropTypes.number, 178 | 179 | /** 180 | * Index of the widget. 181 | */ 182 | widgetIndex: PropTypes.number, 183 | 184 | /** 185 | * Indicates weatehr dashboard is in ediable mode or not. 186 | */ 187 | editable: PropTypes.bool, 188 | 189 | /** 190 | * User provided widget frame that should be used instead of the default one. 191 | */ 192 | frameComponent: PropTypes.func, 193 | 194 | /** 195 | * User provided settings for be use by custom widget frame. 196 | */ 197 | frameSettings: PropTypes.object, 198 | 199 | /** 200 | * Name of the widget. 201 | */ 202 | widgetName: PropTypes.string, 203 | 204 | /** 205 | * Title of the widget. 206 | */ 207 | title: PropTypes.string, 208 | 209 | /** 210 | * Weather the component is being dragged. 211 | */ 212 | isDragging: PropTypes.bool, 213 | 214 | /** 215 | * ReactDnd's connectDragSource(). 216 | */ 217 | connectDragSource: PropTypes.func, 218 | 219 | /** 220 | * ReactDnd's connectDropTarget(). 221 | */ 222 | connectDropTarget: PropTypes.func, 223 | 224 | /** 225 | * Function that should be called when a widget is about to be removed. 226 | */ 227 | onRemove: PropTypes.func, 228 | 229 | /** 230 | * Function called when to edit a widget. 231 | */ 232 | onEdit: PropTypes.func, 233 | }; 234 | 235 | WidgetFrame.defaultProps = { 236 | frameSettings: {}, 237 | }; 238 | 239 | export default WidgetFrame; 240 | -------------------------------------------------------------------------------- /lib/components/Widgets.js: -------------------------------------------------------------------------------- 1 | import React, { createElement } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import WidgetFrame from './WidgetFrame'; 4 | 5 | /** 6 | * Component that renders the widget which belongs to a column. 7 | */ 8 | /* eslint max-len: "off" */ 9 | const Widgets = ({ widgets, widgetTypes, onRemove, layout, columnIndex, rowIndex, editable, frameComponent, onMove, containerClassName, onEdit }) => { 10 | const createdWidgets = widgets.map((widget, index) => { // eslint-disable-line arrow-body-style 11 | return ( 12 | 27 | { 28 | createElement(widgetTypes[widget.key].type, widgetTypes[widget.key].props) 29 | } 30 | 31 | ); 32 | }); 33 | return
{createdWidgets}
; 34 | }; 35 | 36 | Widgets.propTypes = { 37 | /** 38 | * CSS class name that should be provided to the widgets container. 39 | */ 40 | containerClassName: PropTypes.string, 41 | /** 42 | * Widgets that should be rendered. 43 | */ 44 | widgets: PropTypes.array, 45 | 46 | /** 47 | * Widgets that are available in the dashboard. 48 | */ 49 | widgetTypes: PropTypes.object, 50 | 51 | /** 52 | * Function that should be called when a widget is about to be removed. 53 | */ 54 | onRemove: PropTypes.func, 55 | 56 | /** 57 | * Layout of the dahsboard. 58 | */ 59 | layout: PropTypes.object, 60 | 61 | /** 62 | * Index of the column these widgets should be placed. 63 | */ 64 | columnIndex: PropTypes.number, 65 | 66 | /** 67 | * Index of the row these widgets should be placed. 68 | */ 69 | rowIndex: PropTypes.number, 70 | 71 | /** 72 | * Indicates weatehr dashboard is in ediable mode or not. 73 | */ 74 | editable: PropTypes.bool, 75 | 76 | /** 77 | * User provided widget frame that should be used instead of the default one. 78 | */ 79 | frameComponent: PropTypes.func, 80 | 81 | /** 82 | * Method to call when a widget is moved. 83 | */ 84 | onMove: PropTypes.func, 85 | onEdit: PropTypes.func, 86 | }; 87 | 88 | export default Widgets; 89 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | export { default as default, DashboardWithoutDndContext as DashboardWithoutDndContext } from './components/Dashboard'; 2 | export { addWidget } from './util'; 3 | -------------------------------------------------------------------------------- /lib/style/style.css: -------------------------------------------------------------------------------- 1 | .defaultWidgetFrame { 2 | position: relative; 3 | width: 100%; 4 | margin-bottom: 10px; 5 | padding: 10px 17px; 6 | display: inline-block; 7 | background: #fff; 8 | border: 1px solid #E6E9ED; 9 | } 10 | 11 | .defaultWidgetFrameHeader { 12 | border-bottom: 2px solid #E6E9ED; 13 | padding: 1px 5px 6px; 14 | margin-bottom: 10px; 15 | height: 35px; 16 | } 17 | 18 | .defaultWidgetFrameHeader .title { 19 | font-size: 18px; 20 | float: left; 21 | display: block; 22 | text-overflow: ellipsis; 23 | overflow: hidden; 24 | white-space: nowrap; 25 | } 26 | 27 | .defaultWidgetFrameHeader .remove { 28 | float: right; 29 | font-size: 11px; 30 | cursor: pointer; 31 | text-decoration: none; 32 | margin-top: 5px; 33 | } 34 | 35 | .add-widget-button { 36 | padding: 10px; 37 | text-align: center; 38 | border: 1px dotted #DCDCDC; 39 | margin-bottom: 10px; 40 | cursor: pointer; 41 | text-decoration: none; 42 | } 43 | 44 | .add-widget-link { 45 | text-decoration: none; 46 | } 47 | 48 | .editable-column { 49 | border: 1px dotted #8C8080; 50 | padding: 10px; 51 | } 52 | 53 | .droppable-column { 54 | background-color: #E7E7E7; 55 | } 56 | -------------------------------------------------------------------------------- /lib/util/index.js: -------------------------------------------------------------------------------- 1 | import update from 'immutability-helper'; 2 | 3 | /** 4 | * Adds the specified widget to the specified position in the layout. 5 | */ 6 | export function addWidget(layout, rowIndex, columnIndex, widgetName) { 7 | return update(layout, { 8 | rows: { 9 | [rowIndex]: { 10 | columns: { 11 | [columnIndex]: { 12 | widgets: { 13 | $push: [{ 14 | key: widgetName, 15 | }], 16 | }, 17 | }, 18 | }, 19 | }, 20 | }, 21 | }); 22 | } 23 | 24 | /** 25 | * Removes the widget at a specified index. 26 | */ 27 | export function removeWidget(layout, rowIndex, columnIndex, widgetIndex) { 28 | return update(layout, { 29 | rows: { 30 | [rowIndex]: { 31 | columns: { 32 | [columnIndex]: { 33 | widgets: { 34 | $splice: [ 35 | [widgetIndex, 1], 36 | ], 37 | }, 38 | }, 39 | }, 40 | }, 41 | }, 42 | }); 43 | } 44 | 45 | /** 46 | * Moves a widget from column to column. 47 | */ 48 | export function moveWidget(layout, initialLocation, destination, widgetName) { 49 | /* eslint max-len: "off" */ 50 | const removedLayout = removeWidget(layout, initialLocation.rowIndex, initialLocation.columnIndex, initialLocation.widgetIndex); 51 | const movedLayout = addWidget(removedLayout, destination.rowIndex, destination.columnIndex, widgetName); 52 | return movedLayout; 53 | } 54 | 55 | /** 56 | * Sorts a widget in the same column. 57 | */ 58 | export function sortWidget(layout, initialLocation, destination, widgetName) { 59 | return update(layout, { 60 | rows: { 61 | [initialLocation.rowIndex]: { 62 | columns: { 63 | [initialLocation.columnIndex]: { 64 | widgets: { 65 | $splice: [ 66 | [initialLocation.widgetIndex, 1], 67 | [destination.widgetIndex, 0, { 68 | key: widgetName, 69 | }], 70 | ], 71 | }, 72 | }, 73 | }, 74 | }, 75 | }, 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dazzle", 3 | "version": "1.4.0", 4 | "description": "The simple yet flexible dashbording solution for React", 5 | "license": "MIT", 6 | "main": "dist/lib.js", 7 | "style": "lib/style/style.css", 8 | "scripts": { 9 | "prebuild": "npm run clean", 10 | "build": "cross-env NODE_ENV=production webpack --mode production --config webpack.config.prod.js -p", 11 | "clean": "rimraf dist coverage", 12 | "start": "cross-env NODE_ENV=production node server/node-server.js", 13 | "dev": "cross-env NODE_ENV=development webpack-dev-server --mode development -d --hot --progress", 14 | "lint": "npm run lint-js", 15 | "lint-js": "eslint --fix --ext .js,.jsx .", 16 | "test": "babel-node node_modules/mocha/bin/_mocha -- ./test/entry.js ./test/**/*.spec.js", 17 | "test:watch": "babel-node node_modules/mocha/bin/_mocha -- ./test/entry.js ./test/**/*.spec.js --watch", 18 | "cover": "nyc babel-node node_modules/mocha/bin/_mocha -- ./test/entry.js ./test/**/*.spec.js", 19 | "report-cover": "nyc babel-node node_modules/mocha/bin/_mocha -- ./test/entry.js ./test/**/*.spec.js && codecov", 20 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 21 | "commit": "git-cz" 22 | }, 23 | "keywords": [ 24 | "react", 25 | "dashboard", 26 | "react-component", 27 | "widgets", 28 | "react-dazzle", 29 | "analytics-dashboard", 30 | "analytics" 31 | ], 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/Raathigesh/dazzle.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/Raathigesh/dazzle/issues" 38 | }, 39 | "devDependencies": { 40 | "babel-cli": "^6.26.0", 41 | "babel-core": "^6.26.0", 42 | "babel-eslint": "^8.2.2", 43 | "babel-loader": "^7.1.4", 44 | "babel-plugin-istanbul": "^5.0.1", 45 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 46 | "babel-preset-env": "^1.6.1", 47 | "babel-preset-react": "^6.24.1", 48 | "babel-preset-stage-0": "^6.24.1", 49 | "bootstrap": "^4.0.0", 50 | "chai": "^4.1.2", 51 | "commitizen": "^2.7.6", 52 | "cross-env": "^5.1.4", 53 | "css-loader": "^0.28.11", 54 | "cz-conventional-changelog": "^2.1.0", 55 | "enzyme": "^3.3.0", 56 | "enzyme-adapter-react-16": "^1.1.1", 57 | "eslint": "^4.19.1", 58 | "eslint-config-airbnb": "^16.1.0", 59 | "eslint-loader": "^2.0.0", 60 | "eslint-plugin-import": "^2.9.0", 61 | "eslint-plugin-jsx-a11y": "^6.0.3", 62 | "eslint-plugin-react": "^7.7.0", 63 | "express": "^4.16.3", 64 | "file-loader": "^1.1.11", 65 | "html-webpack-plugin": "^3.1.0", 66 | "http-proxy": "^1.16.2", 67 | "istanbul": "^0.4.5", 68 | "jsdom": "^11.6.0", 69 | "json-loader": "^0.5.7", 70 | "mocha": "^5.0.0", 71 | "nyc": "^13.0.1", 72 | "react": "^16.0.0", 73 | "react-dom": "^16.0.0", 74 | "react-hot-loader": "^4.0.0", 75 | "react-modal": "^3.3.2", 76 | "rimraf": "^2.6.2", 77 | "semantic-release": "^15.1.4", 78 | "sinon": "^4.4.9", 79 | "source-map-loader": "^0.2.3", 80 | "style-loader": "^0.20.3", 81 | "stylelint": "^9.1.3", 82 | "uglifyjs-webpack-plugin": "^1.2.4", 83 | "url-loader": "^1.0.1", 84 | "webpack": "^4.4.1", 85 | "webpack-cli": "^2.0.13", 86 | "webpack-dev-server": "^3.1.9", 87 | "webpack-hot-middleware": "^2.21.2", 88 | "winston": "^2.4.1" 89 | }, 90 | "dependencies": { 91 | "immutability-helper": "^2.3.1", 92 | "jquery": "^3.3.1", 93 | "popper.js": "^1.14.1", 94 | "prop-types": "^15.5.10", 95 | "react-dnd": "^2.6.0", 96 | "react-dnd-html5-backend": "^2.6.0" 97 | }, 98 | "peerDependencies": { 99 | "react": "^16.0.0", 100 | "react-dom": "^16.0.0" 101 | }, 102 | "czConfig": { 103 | "path": "node_modules/cz-conventional-changelog" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /sample/assets/Dazzle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Raathigesh/dazzle/c4a46f62401a0a1b34efa3a45134a6a34a121770/sample/assets/Dazzle.png -------------------------------------------------------------------------------- /sample/components/AddWidgetDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Modal from 'react-modal'; 4 | 5 | const AddWidgetDialog = ({ widgets, isModalOpen, onRequestClose, onWidgetSelect }) => { 6 | const widgetItems = Object.keys(widgets).map((widget, index) => ( 7 | 12 | )); 13 | return ( 14 | 19 |
20 |
21 | 25 |

Add a widget

26 |
27 |
28 |
Pick a widget to add
29 | {widgetItems} 30 |
31 |
32 | 33 | 34 |
35 |
36 |
37 | ); 38 | }; 39 | 40 | AddWidgetDialog.propTypes = { 41 | widgets: PropTypes.object, 42 | isModalOpen: PropTypes.bool, 43 | onRequestClose: PropTypes.func, 44 | onWidgetSelect: PropTypes.func, 45 | }; 46 | 47 | export default AddWidgetDialog; 48 | -------------------------------------------------------------------------------- /sample/components/Container.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Container = ({ children }) => ( 5 |
6 |
7 | {children} 8 |
9 |
10 | ); 11 | 12 | Container.propTypes = { 13 | children: PropTypes.array, 14 | }; 15 | 16 | export default Container; 17 | -------------------------------------------------------------------------------- /sample/components/CustomAddWidgetButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const CustomAddWidgetButton = ({ text, onClick }) => ( 5 |
6 | 7 |
8 | ); 9 | 10 | CustomAddWidgetButton.propTypes = { 11 | text: PropTypes.string, 12 | onClick: PropTypes.func, 13 | }; 14 | 15 | export default CustomAddWidgetButton; 16 | -------------------------------------------------------------------------------- /sample/components/EditBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const EditBar = ({ onEdit }) => ( 5 |
6 |
7 | 11 |
12 |
13 | ); 14 | 15 | EditBar.propTypes = { 16 | onEdit: PropTypes.func, 17 | }; 18 | 19 | export default EditBar; 20 | -------------------------------------------------------------------------------- /sample/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Header = () => ( 4 |
5 |
6 | 9 |
10 |
11 | ); 12 | 13 | export default Header; 14 | -------------------------------------------------------------------------------- /sample/components/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dashboard, { addWidget } from '../../lib'; 3 | 4 | // App Components 5 | import Header from './Header'; 6 | import EditBar from './EditBar'; 7 | import Container from './Container'; 8 | 9 | // Widgets 10 | import HelloWorld from './widgets/HelloWorld'; 11 | import AnotherWidget from './widgets/AnotherWidget'; 12 | import AddWidgetDialog from './AddWidgetDialog'; 13 | // import CustomAddWidgetButton from './CustomAddWidgetButton'; 14 | 15 | import 'bootstrap/dist/css/bootstrap.css'; 16 | import '../css/custom.css'; 17 | import '../../lib/style/style.css'; 18 | 19 | class App extends React.Component { 20 | constructor(props) { 21 | super(props); 22 | this.state = { 23 | layout: { 24 | rows: [{ 25 | columns: [{ 26 | className: 'col-md-4 col-sm-6 col-xs-6', 27 | widgets: [{ key: 'RocketWidget' }, { key: 'AlienWidget' }, { key: 'RocketWidget' }], 28 | }, { 29 | className: 'col-md-4 col-sm-6 col-xs-6', 30 | widgets: [{ key: 'RocketWidget' }], 31 | }, { 32 | className: 'col-md-4 col-sm-6 col-xs-6', 33 | widgets: [{ key: 'RocketWidget' }], 34 | }], 35 | }, { 36 | columns: [{ 37 | className: 'col-md-4 col-sm-6 col-xs-6', 38 | widgets: [{ key: 'RocketWidget' }], 39 | }, { 40 | className: 'col-md-4 col-sm-6 col-xs-6', 41 | widgets: [{ key: 'RocketWidget' }], 42 | }, { 43 | className: 'col-md-4 col-sm-6 col-xs-6', 44 | widgets: [{ key: 'RocketWidget' }], 45 | }], 46 | }], 47 | }, 48 | widgets: { 49 | RocketWidget: { 50 | type: HelloWorld, 51 | title: 'Rocket Widget', 52 | }, 53 | AlienWidget: { 54 | type: AnotherWidget, 55 | title: 'Alien Widget', 56 | }, 57 | }, 58 | editMode: false, 59 | isModalOpen: false, 60 | addWidgetOptions: null, 61 | }; 62 | } 63 | 64 | onRemove = (layout) => { 65 | this.setState({ 66 | layout, 67 | }); 68 | } 69 | 70 | onAdd = (layout, rowIndex, columnIndex) => { 71 | this.setState({ 72 | isModalOpen: true, 73 | addWidgetOptions: { 74 | layout, 75 | rowIndex, 76 | columnIndex, 77 | }, 78 | }); 79 | } 80 | 81 | onMove = (layout) => { 82 | this.setState({ 83 | layout, 84 | }); 85 | } 86 | 87 | onRequestClose = () => { 88 | this.setState({ 89 | isModalOpen: false, 90 | }); 91 | } 92 | 93 | render() { 94 | /* eslint max-len: "off" */ 95 | return ( 96 | 97 |
98 | 99 | 108 | 109 | 110 | ); 111 | } 112 | 113 | toggleEdit = () => { 114 | this.setState({ 115 | editMode: !this.state.editMode, 116 | }); 117 | }; 118 | 119 | widgetSelected = (widgetName) => { 120 | const { layout, rowIndex, columnIndex } = this.state.addWidgetOptions; 121 | this.setState({ 122 | layout: addWidget(layout, rowIndex, columnIndex, widgetName), 123 | }); 124 | this.onRequestClose(); 125 | } 126 | } 127 | 128 | export default App; 129 | -------------------------------------------------------------------------------- /sample/components/widgets/AnotherWidget/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const AnotherWidget = () => ( 3 |
4 | 5 |
6 | ); 7 | 8 | export default AnotherWidget; 9 | -------------------------------------------------------------------------------- /sample/components/widgets/HelloWorld/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const HelloWorld = () => ( 3 |
4 | 5 |
6 | ); 7 | 8 | export default HelloWorld; 9 | -------------------------------------------------------------------------------- /sample/css/custom.css: -------------------------------------------------------------------------------- 1 | .edit-bar { 2 | padding-top: 10px; 3 | padding-bottom: 10px; 4 | } 5 | 6 | .dashboardHeader { 7 | text-align: center; 8 | height: 45px; 9 | padding-top: 10px; 10 | background-color: white; 11 | } 12 | -------------------------------------------------------------------------------- /sample/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Dazzle Sample 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /sample/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './components/app'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /server/node-app-server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | 4 | /** 5 | * Installs routes that serve production-bundled client-side assets. 6 | * It is set up to allow for HTML5 mode routing (404 -> /dist/index.html). 7 | * This should be the last router in your express server's chain. 8 | */ 9 | module.exports = (app) => { 10 | const distPath = path.join(__dirname, '../dist'); 11 | const indexFileName = 'index.html'; 12 | app.use(express.static(distPath)); 13 | app.get('*', (req, res) => res.sendFile(path.join(distPath, indexFileName))); 14 | }; 15 | -------------------------------------------------------------------------------- /server/node-proxy.js: -------------------------------------------------------------------------------- 1 | const httpProxy = require('http-proxy'); 2 | const winston = require('winston'); 3 | const proxyConfig = require('./proxy-config'); 4 | 5 | /* 6 | * Installs routes that proxy based on the settings in ./proxy-config. 7 | * If no settings are provided, no proxies are installed. 8 | */ 9 | module.exports = (app) => { 10 | const paths = Object.keys(proxyConfig); 11 | if (!paths.length) { 12 | return; 13 | } 14 | 15 | const proxy = httpProxy.createProxyServer() 16 | .on('error', e => winston.error(e)); 17 | 18 | paths.forEach(path => { 19 | const config = proxyConfig[path]; 20 | if (path && config) { 21 | winston.info(`Enabling proxy ${path} => `, config); 22 | app.use(path, (req, res) => { 23 | proxy.web(req, res, config); 24 | }); 25 | } 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /server/node-server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const winston = require('winston'); 3 | const helmet = require('helmet'); 4 | const nodeProxy = require('./node-proxy'); 5 | const nodeAppServer = require('./node-app-server'); 6 | 7 | /** 8 | * Heroku-friendly production http server. 9 | * 10 | * Serves your app and allows you to proxy APIs if needed. 11 | */ 12 | 13 | const app = express(); 14 | const PORT = process.env.PORT || 8080; 15 | 16 | // Enable various security helpers. 17 | app.use(helmet()); 18 | 19 | // API proxy logic: if you need to talk to a remote server from your client-side 20 | // app you can proxy it though here by editing ./proxy-config.js 21 | nodeProxy(app); 22 | 23 | // Serve the distributed assets and allow HTML5 mode routing. NB: must be last. 24 | nodeAppServer(app); 25 | 26 | // Start up the server. 27 | app.listen(PORT, (err) => { 28 | if (err) { 29 | winston.error(err); 30 | return; 31 | } 32 | 33 | winston.info(`Listening on port ${PORT}`); 34 | }); 35 | -------------------------------------------------------------------------------- /server/proxy-config.js: -------------------------------------------------------------------------------- 1 | // Proxying to remote HTTP APIs: 2 | // 3 | // Proxy settings in this file are used by both the production express server 4 | // and webpack-dev-server. 5 | // 6 | // In either case, the config format is that used by node-http-proxy: 7 | // https://github.com/nodejitsu/node-http-proxy#options 8 | // 9 | // Note that in production it's better to either 10 | // 1. deploy the app on the same domain as the API, 11 | // 2. get the API to expose an x-allow-origin header, or 12 | // 3. use a dedicated reverse proxy (e.g. Nginx) to do this instead. 13 | 14 | module.exports = { 15 | // Calls to /api/foo will get routed to 16 | // http://jsonplaceholder.typicode.com/foo. 17 | /* 18 | '/api/': { 19 | target: 'http://jsonplaceholder.typicode.com', 20 | changeOrigin: true, 21 | }, 22 | */ 23 | }; 24 | -------------------------------------------------------------------------------- /server/webpack-dev-proxy.js: -------------------------------------------------------------------------------- 1 | const config = require('./proxy-config'); 2 | 3 | /* eslint no-param-reassign: "off" */ 4 | module.exports = function getWebpackConfig() { 5 | // Webpack needs the paths to end with a wildcard, node doesn't. 6 | // Webpack also needs to be told to strip the path off the proxied 7 | // request. 8 | return Object.keys(config).reduce((acc, path) => { 9 | acc[`${path}*`] = config[path]; 10 | acc[`${path}*`].rewrite = (req) => { 11 | req.url = req.url.replace(path, ''); 12 | }; 13 | 14 | return acc; 15 | }, {}); 16 | }; 17 | -------------------------------------------------------------------------------- /test/components/AddWidget.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import React from 'react'; 3 | import { shallow } from 'enzyme'; 4 | import { spy } from 'sinon'; 5 | import AddWidget from '../../lib/components/AddWidget'; 6 | 7 | describe('', () => { 8 | it('Should render the children', () => { 9 | const widgetText = 'Add new widget yo!'; 10 | const component = shallow(); 11 | expect(component.find('a').first().text()).to.equal(widgetText); 12 | }); 13 | 14 | it('Should use the default text when text is not provided', () => { 15 | const component = shallow(); 16 | expect(component.find('a').first().text()).to.equal('Add Widget'); 17 | }); 18 | 19 | it('Should call onClick when clicked', () => { 20 | const onClick = spy(); 21 | const component = shallow(); 22 | component.find('div').simulate('click'); 23 | expect(onClick.calledOnce).to.equal(true); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/components/Column.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { spy } from 'sinon'; 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import Column from '../../lib/components/Column'; 6 | 7 | /* eslint max-len: "off" */ 8 | describe('', () => { 9 | it('Should call onAdd when add is clicked', () => { 10 | const onAdd = spy(); 11 | const layout = {}; 12 | const rowIndex = 1; 13 | const columnIndex = 2; 14 | const OriginalColumn = Column.DecoratedComponent; 15 | const identity = (el) => el; 16 | const component = mount(); 17 | component.find('.add-widget-button').simulate('click'); 18 | expect(onAdd.calledWithExactly(layout, rowIndex, columnIndex)).to.equal(true); 19 | }); 20 | 21 | it('Should render the children', () => { 22 | const OriginalColumn = Column.DecoratedComponent; 23 | const identity = (el) => el; 24 | const component = mount(

HelloWorld

); 25 | expect(component.contains(

HelloWorld

)).to.equal(true); 26 | }); 27 | 28 | it('Should have the column class rendered', () => { 29 | const OriginalColumn = Column.DecoratedComponent; 30 | const identity = (el) => el; 31 | const component = mount(); 32 | expect(component.find('.ColumnClass')).to.have.length(2); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/components/Dashboard.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import React from 'react'; 3 | import { mount } from 'enzyme'; 4 | import { default as Dashboard, DashboardWithoutDndContext as DashboardWithoutDndContext } from '../../lib/components/Dashboard'; 5 | import LayoutRenderer from '../../lib/components/LayoutRenderer'; 6 | 7 | describe('', () => { 8 | it('Should have a ', () => { 9 | const component = mount(); 10 | expect(component.find(LayoutRenderer)).to.have.length(1); 11 | }); 12 | }); 13 | 14 | describe('', () => { 15 | it('Should have a ', () => { 16 | const component = mount(); 17 | expect(component.find(LayoutRenderer)).to.have.length(1); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/components/Row.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import React from 'react'; 3 | import { mount, shallow } from 'enzyme'; 4 | import Column from '../../lib/components/Column'; 5 | import Row from '../../lib/components/Row'; 6 | import Widgets from '../../lib/components/Widgets'; 7 | import TestComponent from '../fake/TestComponent'; 8 | import TestCustomFrame from '../fake/TestCustomFrame'; 9 | import ContainerWithDndContext from '../fake/ContainerWithDndContext'; 10 | 11 | function setup() { 12 | const columns = [{ 13 | className: 'col-md-4 col-sm-6 col-xs-6', 14 | widgets: [{ key: 'HelloWorld' }], 15 | }, { 16 | className: 'col-md-4 col-sm-6 col-xs-6', 17 | widgets: [{ key: 'HelloWorld' }], 18 | }, { 19 | className: 'col-md-4 col-sm-6 col-xs-6', 20 | widgets: [{ key: 'HelloWorld' }], 21 | }]; 22 | 23 | const widgets = { 24 | HelloWorld: { 25 | type: TestComponent, 26 | title: 'Sample Hello World App', 27 | }, 28 | }; 29 | 30 | return { 31 | columns, 32 | widgets, 33 | onAdd: () => {}, 34 | onRemove: () => {}, 35 | layout: {}, 36 | rowIndex: 1, 37 | }; 38 | } 39 | 40 | describe('', () => { 41 | /* eslint max-len: "off" */ 42 | it('Should render the correct number of ', () => { 43 | const { columns, widgets } = setup(); 44 | const component = mount(); 45 | expect(component.find(Column)).to.have.length(3); 46 | }); 47 | 48 | it('Should have the row class rendered', () => { 49 | const { columns, widgets, onAdd, layout, rowIndex } = setup(); 50 | 51 | const component = shallow( 52 | 61 | ); 62 | expect(component.find('.RowClass')).to.have.length(1); 63 | }); 64 | 65 | it('Should pass the required properties to ', () => { 66 | const { columns, widgets, onAdd, layout, rowIndex } = setup(); 67 | const component = mount( 68 | 69 | 77 | 78 | ); 79 | expect(component.find(Column).first().prop('className')).to.equal('col-md-4 col-sm-6 col-xs-6'); 80 | expect(component.find(Column).first().prop('onAdd')).to.equal(onAdd); 81 | expect(component.find(Column).first().prop('layout')).to.equal(layout); 82 | expect(component.find(Column).first().prop('rowIndex')).to.equal(rowIndex); 83 | expect(component.find(Column).first().prop('columnIndex')).to.equal(0); 84 | expect(component.find(Column).first().prop('editable')).to.equal(true); 85 | }); 86 | 87 | it('Should pass the required properties to ', () => { 88 | const { columns, widgets, onAdd, onRemove, layout, rowIndex } = setup(); 89 | const component = mount( 90 | 91 | 101 | 102 | ); 103 | expect(component.find(Widgets).first().prop('widgets')).to.equal(columns[0].widgets); 104 | expect(component.find(Widgets).first().prop('widgetTypes')).to.equal(widgets); 105 | expect(component.find(Widgets).first().prop('onRemove')).to.equal(onRemove); 106 | expect(component.find(Widgets).first().prop('layout')).to.equal(layout); 107 | expect(component.find(Widgets).first().prop('rowIndex')).to.equal(rowIndex); 108 | expect(component.find(Widgets).first().prop('columnIndex')).to.equal(0); 109 | expect(component.find(Widgets).first().prop('editable')).to.equal(true); 110 | expect(component.find(Widgets).first().prop('frameComponent')).to.equal(TestCustomFrame); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/components/WidgetFrame.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { spy } from 'sinon'; 3 | import React from 'react'; 4 | import { mount } from 'enzyme'; 5 | import WidgetFrame from '../../lib/components/WidgetFrame'; 6 | import DefaultFrame from '../../lib/components/DefaultFrame'; 7 | import TestCustomFrame from '../fake/TestCustomFrame'; 8 | import ContainerWithDndContext from '../fake/ContainerWithDndContext'; 9 | 10 | describe('', () => { 11 | it('Default frame should be used when customized frame is not provided', () => { 12 | const onAdd = spy(); 13 | const layout = {}; 14 | const rowIndex = 1; 15 | const columnIndex = 2; 16 | const OriginalWidgetFrame = WidgetFrame.DecoratedComponent; 17 | const identity = (el) => el; 18 | const component = mount( 19 | 20 | 29 | 30 | ); 31 | expect(component.find(DefaultFrame)).to.have.length(1); 32 | }); 33 | 34 | it('DefaultFrame should be provided with necessary props', () => { 35 | const children = []; 36 | const editable = false; 37 | const onRemove = () => {}; 38 | const title = 'Widget Title'; 39 | const OriginalWidgetFrame = WidgetFrame.DecoratedComponent; 40 | const identity = (el) => el; 41 | const component = mount( 42 | 43 | 51 | 52 | ); 53 | expect(component.find(DefaultFrame).first().prop('children')).to.equal(children); 54 | expect(component.find(DefaultFrame).first().prop('editable')).to.equal(editable); 55 | expect(component.find(DefaultFrame).first().prop('title')).to.equal(title); 56 | expect(component.find('WidgetFrame div').first().prop('style').opacity).to.equal(1); 57 | }); 58 | 59 | it('DefaultFrame onRemove should be called when close is clicked', () => { 60 | const children = []; 61 | const editable = false; 62 | const onRemove = spy(); 63 | const title = 'Widget Title'; 64 | const layout = { 65 | rows: [{ 66 | columns: [{ 67 | className: 'col-md-4', 68 | widgets: [{ name: 'HelloWorld' }], 69 | }], 70 | }], 71 | }; 72 | 73 | const OriginalWidgetFrame = WidgetFrame.DecoratedComponent; 74 | const identity = (el) => el; 75 | const component = mount( 76 | 77 | 90 | 91 | ); 92 | component.find('a.remove').simulate('click'); 93 | expect(onRemove.calledWithExactly({ 94 | rows: [{ 95 | columns: [{ 96 | className: 'col-md-4', 97 | widgets: [], 98 | }], 99 | }], 100 | }, 0, 0, 0)).to.equal(true); 101 | }); 102 | 103 | it('Customized frame should be used if provided', () => { 104 | const onAdd = spy(); 105 | const layout = {}; 106 | const rowIndex = 1; 107 | const columnIndex = 2; 108 | const OriginalWidgetFrame = WidgetFrame.DecoratedComponent; 109 | const identity = (el) => el; 110 | const component = mount( 111 | 112 | 122 | 123 | ); 124 | expect(component.find(TestCustomFrame)).to.have.length(1); 125 | }); 126 | 127 | it('Customized frame should be provided with necessary props', () => { 128 | const children = []; 129 | const editable = false; 130 | const onRemove = () => {}; 131 | const title = 'Widget Title'; 132 | const OriginalWidgetFrame = WidgetFrame.DecoratedComponent; 133 | const identity = (el) => el; 134 | const isDragging = false; 135 | const settings = { 136 | color: '#E140AD', 137 | }; 138 | 139 | const component = mount( 140 | 141 | 152 | 153 | ); 154 | 155 | expect(component.find(TestCustomFrame).first().prop('children')).to.equal(children); 156 | expect(component.find(TestCustomFrame).first().prop('editable')).to.equal(editable); 157 | expect(component.find(TestCustomFrame).first().prop('title')).to.equal(title); 158 | expect(component.find(TestCustomFrame).first().prop('settings')).to.equal(settings); 159 | expect(component.find(TestCustomFrame).first().prop('isDragging')).to.equal(isDragging); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /test/components/Widgets.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import React from 'react'; 3 | import { shallow } from 'enzyme'; 4 | import Widgets from '../../lib/components/Widgets'; 5 | import WidgetFrame from '../../lib/components/WidgetFrame'; 6 | import TestComponent from '../fake/TestComponent'; 7 | 8 | describe('', () => { 9 | const layout = {}; 10 | const columnIndex = 5; 11 | const rowIndex = 6; 12 | const widgetIndex = 0; 13 | const editable = false; 14 | const frame = () => {}; 15 | const onRemove = () => {}; 16 | 17 | it('Should render widgets with widget frames', () => { 18 | const widgets = [{ key: 'HelloWorld' }]; 19 | const widgetTypes = { 20 | HelloWorld: { 21 | type: TestComponent, 22 | title: 'Sample Hello World App', 23 | }, 24 | }; 25 | const component = shallow(); 26 | expect(component.find(WidgetFrame)).to.have.length(1); 27 | }); 28 | 29 | it('Should pass the properties to WidgetFrame', () => { 30 | const widgets = [{ key: 'HelloWorld' }]; 31 | const widgetTypes = { 32 | HelloWorld: { 33 | type: TestComponent, 34 | title: 'Sample Hello World App', 35 | }, 36 | }; 37 | 38 | const component = shallow( 39 | 50 | ); 51 | 52 | expect(component.find(WidgetFrame).at(0).prop('title')).to.equal('Sample Hello World App'); 53 | expect(component.find(WidgetFrame).at(0).prop('layout')).to.equal(layout); 54 | expect(component.find(WidgetFrame).at(0).prop('columnIndex')).to.equal(columnIndex); 55 | expect(component.find(WidgetFrame).at(0).prop('rowIndex')).to.equal(rowIndex); 56 | expect(component.find(WidgetFrame).at(0).prop('widgetIndex')).to.equal(widgetIndex); 57 | expect(component.find(WidgetFrame).at(0).prop('editable')).to.equal(editable); 58 | expect(component.find(WidgetFrame).at(0).prop('frameComponent')).to.equal(frame); 59 | expect(component.find(WidgetFrame).at(0).prop('onRemove')).to.equal(onRemove); 60 | }); 61 | 62 | it('Should pass optional `frameSettings` to WidgetFrame', () => { 63 | const widgets = [{ key: 'HelloWorld' }]; 64 | const widgetTypes = { 65 | HelloWorld: { 66 | type: TestComponent, 67 | title: 'Sample Hello World App', 68 | frameSettings: { 69 | color: '#E140AD', 70 | }, 71 | }, 72 | }; 73 | 74 | const component = shallow( 75 | 86 | ); 87 | 88 | expect(component.find(WidgetFrame).at(0).prop('frameSettings')).to.deep.equal({ 89 | color: '#E140AD', 90 | }); 91 | }); 92 | 93 | it('Frame should have the actual widget as children', () => { 94 | const widgets = [{ key: 'HelloWorld' }]; 95 | const widgetTypes = { 96 | HelloWorld: { 97 | type: TestComponent, 98 | title: 'Sample Hello World App', 99 | }, 100 | }; 101 | 102 | const component = shallow( 103 | 107 | ); 108 | 109 | expect(component.find(WidgetFrame).first().childAt(0).type()).to.equal(TestComponent); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /test/entry.js: -------------------------------------------------------------------------------- 1 | const jsdom = require('jsdom'); 2 | const { JSDOM } = jsdom; 3 | 4 | // setup file 5 | import { configure } from 'enzyme'; 6 | import Adapter from 'enzyme-adapter-react-16'; 7 | 8 | configure({ adapter: new Adapter() }); 9 | 10 | const { document } = (new JSDOM('')).window; 11 | global.document = document; 12 | global.window = document.defaultView; 13 | global.navigator = global.window.navigator; 14 | window.localStorage = window.sessionStorage = { 15 | getItem(key) { 16 | return this[key]; 17 | }, 18 | setItem(key, value) { 19 | this[key] = value; 20 | }, 21 | removeItem(key) { 22 | this[key] = undefined; 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /test/fake/ContainerWithDndContext.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { DragDropContext } from 'react-dnd'; 4 | import HTML5Backend from 'react-dnd-html5-backend'; 5 | 6 | @DragDropContext(HTML5Backend) 7 | class ContainerWithDndContext extends Component { 8 | render() { 9 | return ( 10 |
{this.props.children}
11 | ); 12 | } 13 | } 14 | 15 | ContainerWithDndContext.propTypes = { 16 | children: PropTypes.element, 17 | }; 18 | 19 | export default ContainerWithDndContext; 20 | -------------------------------------------------------------------------------- /test/fake/TestComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TestComponent = () => { 4 | return ( 5 |
6 | ); 7 | }; 8 | 9 | export default TestComponent; 10 | -------------------------------------------------------------------------------- /test/fake/TestCustomFrame.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class TestCustomFrame extends React.Component { 4 | render() { 5 | return
; 6 | } 7 | } 8 | 9 | export default TestCustomFrame; 10 | -------------------------------------------------------------------------------- /test/util/until.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { addWidget, removeWidget } from '../../lib/util'; 3 | 4 | function setup() { 5 | return { 6 | rows: [{ 7 | columns: [{ 8 | className: 'col-md-4 col-sm-6 col-xs-6', 9 | widgets: [{ key: 'HelloWorld' }], 10 | }, { 11 | className: 'col-md-4 col-sm-6 col-xs-6', 12 | widgets: [], 13 | }], 14 | }], 15 | }; 16 | } 17 | 18 | describe('Util.addWidget()', () => { 19 | it('Should add a new widget to the specified location', () => { 20 | const layout = setup(); 21 | expect({ 22 | rows: [{ 23 | columns: [{ 24 | className: 'col-md-4 col-sm-6 col-xs-6', 25 | widgets: [{ key: 'HelloWorld' }, { key: 'NewWidget' }], 26 | }, { 27 | className: 'col-md-4 col-sm-6 col-xs-6', 28 | widgets: [], 29 | }], 30 | }], 31 | }).to.eql(addWidget(layout, 0, 0, 'NewWidget')); 32 | }); 33 | }); 34 | 35 | describe('Util.removeWidget()', () => { 36 | it('Should remove a widget from the specified location', () => { 37 | const layout = setup(); 38 | expect({ 39 | rows: [{ 40 | columns: [{ 41 | className: 'col-md-4 col-sm-6 col-xs-6', 42 | widgets: [], 43 | }, { 44 | className: 'col-md-4 col-sm-6 col-xs-6', 45 | widgets: [], 46 | }], 47 | }], 48 | }).to.eql(removeWidget(layout, 0, 0, 0)); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const proxy = require('./server/webpack-dev-proxy'); 5 | 6 | function getEntrySources(sources) { 7 | if (process.env.NODE_ENV !== 'production') { 8 | sources.push('webpack-hot-middleware/client'); 9 | } 10 | 11 | return sources; 12 | } 13 | 14 | const basePlugins = [ 15 | new webpack.DefinePlugin({ 16 | __DEV__: process.env.NODE_ENV !== 'production', 17 | __PRODUCTION__: process.env.NODE_ENV === 'production', 18 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 19 | }), 20 | new HtmlWebpackPlugin({ 21 | template: './sample/index.html', 22 | inject: 'body', 23 | }), 24 | ]; 25 | 26 | const devPlugins = [ 27 | new webpack.NoEmitOnErrorsPlugin(), 28 | ]; 29 | 30 | const prodPlugins = [ 31 | new webpack.optimize.OccurrenceOrderPlugin(), 32 | ]; 33 | 34 | const plugins = basePlugins 35 | .concat(process.env.NODE_ENV === 'production' ? prodPlugins : []) 36 | .concat(process.env.NODE_ENV === 'development' ? devPlugins : []); 37 | 38 | /* eslint max-len: "off" */ 39 | module.exports = { 40 | entry: { 41 | app: getEntrySources(['./sample/index.js']), 42 | vendor: [ 43 | 'react', 44 | ], 45 | }, 46 | 47 | resolve: { 48 | extensions: ['.js', '.jsx'], 49 | }, 50 | 51 | output: { 52 | path: path.join(__dirname, 'dist'), 53 | filename: '[name].[hash].js', 54 | publicPath: '/', 55 | sourceMapFilename: '[name].[hash].js.map', 56 | chunkFilename: '[id].chunk.js', 57 | }, 58 | 59 | devtool: 'source-map', 60 | plugins, 61 | 62 | devServer: { 63 | historyApiFallback: { index: '/' }, 64 | proxy: proxy(), 65 | }, 66 | 67 | module: { 68 | rules: [ 69 | { test: /\.(js|jsx)$$/, enforce: 'pre', loader: 'source-map-loader' }, 70 | { test: /\.(js|jsx)$$/, enforce: 'pre', loader: 'eslint-loader' }, 71 | { test: /\.css$/, loader: 'style-loader!css-loader' }, 72 | { test: /\.(js|jsx)$/, loaders: ['babel-loader'], exclude: /node_modules/ }, 73 | { test: /\.json$/, loader: 'json-loader' }, 74 | { test: /\.(png|jpg|jpeg|gif|svg)$/, loader: 'url-loader?prefix=img/&limit=5000' }, 75 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader' }, 76 | { test: /\.(woff)$/, loader: 'url-loader?prefix=font/&limit=5000' }, 77 | { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=application/octet-stream' }, 78 | { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=image/svg+xml' }, 79 | { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&minetype=application/font-wof' }, 80 | { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&minetype=application/font-woff2' }, 81 | ], 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const basePlugins = [ 4 | new webpack.DefinePlugin({ 5 | __DEV__: process.env.NODE_ENV !== 'production', 6 | __PRODUCTION__: process.env.NODE_ENV === 'production', 7 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 8 | }), 9 | ]; 10 | 11 | const devPlugins = [ 12 | new webpack.NoEmitOnErrorsPlugin(), 13 | ]; 14 | 15 | const prodPlugins = [ 16 | new webpack.optimize.OccurrenceOrderPlugin(), 17 | ]; 18 | 19 | const plugins = basePlugins 20 | .concat(process.env.NODE_ENV === 'production' ? prodPlugins : []) 21 | .concat(process.env.NODE_ENV === 'development' ? devPlugins : []); 22 | 23 | module.exports = { 24 | entry: { 25 | lib: ['./lib/index.js'], 26 | }, 27 | 28 | resolve: { 29 | extensions: ['.js', '.jsx'], 30 | }, 31 | 32 | output: { 33 | path: path.join(__dirname, 'dist'), 34 | filename: '[name].js', 35 | publicPath: '/', 36 | sourceMapFilename: '[name].js.map', 37 | library: 'dazzle', 38 | libraryTarget: 'umd', 39 | umdNamedDefine: true, 40 | }, 41 | 42 | externals: { 43 | react: 'react', 44 | 'react-dom': 'react-dom', 45 | }, 46 | 47 | devtool: 'source-map', 48 | plugins, 49 | 50 | module: { 51 | rules: [ 52 | { test: /\.(js|jsx)$$/, enforce: 'pre', loader: 'source-map-loader' }, 53 | { test: /\.(js|jsx)$$/, enforce: 'pre', loader: 'eslint-loader' }, 54 | { test: /\.css$/, loader: 'style-loader!css-loader' }, 55 | { test: /\.(js|jsx)$/, loaders: ['babel-loader'], exclude: /node_modules/ }, 56 | ], 57 | }, 58 | node: { 59 | global: false, 60 | }, 61 | }; 62 | --------------------------------------------------------------------------------