├── .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 |
3 |
4 | React Dazzle
5 |
6 | Dashboards made easy in React JS
7 |
8 |
9 |
10 |
11 |
13 |
14 |
15 |
17 |
18 |
19 |
21 |
22 |
23 |
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 |
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 |
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 | 
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 | {text}
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 |
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 |
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 |
22 | ×
23 | Close
24 |
25 |
Add a widget
26 |
27 |
28 |
Pick a widget to add
29 | {widgetItems}
30 |
31 |
32 | Close
33 | Add
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 | {text}
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 |
8 |
9 | Edit
10 |
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 |
7 |
8 |
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 |
--------------------------------------------------------------------------------