├── .babelrc
├── .circleci
└── config.yml
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .npmignore
├── .prettierrc
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── commitlint.config.js
├── dist
├── index.html
├── main.min.js
├── style.css
├── style.min.css
└── viewer.min.js
├── jestSetup.js
├── lib
├── actions
│ ├── actionTypes.js
│ ├── constants.js
│ ├── rootActions.js
│ └── rootActions.test.js
├── builder
│ ├── components
│ │ ├── Buttons.js
│ │ ├── Chart.js
│ │ ├── ChartContainer.js
│ │ ├── CustomChartTheme.js
│ │ ├── Editor.js
│ │ ├── EditorDashboardTopBar.js
│ │ ├── EditorDashboardsSwitch.js
│ │ ├── EditorToolbar.js
│ │ ├── EmbedDashboard.js
│ │ ├── Explorer.js
│ │ ├── Image.js
│ │ ├── Main.js
│ │ ├── NewDashboardButton.js
│ │ ├── Paragraph.js
│ │ ├── SavedQueriesSelect.js
│ │ ├── Settings.js
│ │ ├── SettingsChart.js
│ │ ├── SettingsDashboard.js
│ │ ├── SettingsImage.js
│ │ ├── SettingsParagraph.js
│ │ ├── ShareDashboard.js
│ │ ├── Switcher.js
│ │ └── TextEditor.js
│ └── index.js
├── constants.js
├── contexts
│ └── keenAnalysis.js
├── func
│ ├── ChartType.js
│ ├── __snapshots__
│ │ ├── classicDashboardDataParse.test.js.snap
│ │ └── newGridDataParse.test.js.snap
│ ├── checkBoundaries.js
│ ├── classicDashboardDataParse.js
│ ├── classicDashboardDataParse.test.js
│ ├── copyToClipboard.js
│ ├── getKeyFromAPI.js
│ ├── newGridDataParse.js
│ ├── newGridDataParse.test.js
│ ├── sortDashboardList.js
│ ├── transformChart.js
│ ├── transformChart.test.js
│ └── updateApiKey.js
├── index.js
├── middleware
│ └── onlyUIMiddleware.js
├── reducers
│ ├── defaultDashboardInfo.js
│ └── rootReducer.js
├── selectors
│ ├── app.js
│ └── editor.js
├── utils
│ ├── dashboardObserver.js
│ ├── dashboardObserver.test.js
│ ├── generateUniqueId.js
│ ├── generateUniqueId.test.js
│ ├── loadFontsFromDashboard.js
│ └── loadFontsFromDashboard.test.js
└── viewer
│ ├── components
│ ├── Editor.js
│ ├── EditorContainer.js
│ ├── EditorDashboard.js
│ ├── EditorTopToolbar.js
│ ├── EditorTopToolbarTitle.js
│ ├── ExplorerButton.js
│ ├── Main.js
│ ├── MainContainer.js
│ ├── MainListItem.js
│ ├── MainListItemButtons.js
│ ├── MainTopToolbar.js
│ └── SwitchDashboard.js
│ └── index.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── styles
└── style.css
├── test
├── demo
│ ├── index-viewer.html
│ └── index.html
└── setupTests.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"],
3 | "plugins": [
4 | "@babel/plugin-proposal-object-rest-spread",
5 | "transform-es2015-arrow-functions",
6 | "babel-plugin-transform-class-properties",
7 | "@babel/plugin-transform-runtime",
8 | ["styled-jsx/babel", {
9 | "optimizeForSpeed": true
10 | }],
11 | ["prismjs", {
12 | "languages": ["javascript", "css", "markup"],
13 | "plugins": ["line-numbers"],
14 | "theme": "default",
15 | "css": true
16 | }]
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build:
4 | docker:
5 | - image: 'circleci/node:latest'
6 | steps:
7 | - checkout
8 | - restore_cache:
9 | key: npm-deps-{{ arch }}-{{ .Branch }}-{{ checksum "package-lock.json" }}
10 | - run:
11 | name: Install dependencies
12 | command: npm install
13 | - save_cache:
14 | key: npm-deps-{{ arch }}-{{ .Branch }}-{{ checksum "package-lock.json" }}
15 | paths:
16 | - ./node_modules
17 | - run:
18 | name: Lint
19 | command: npm run lint
20 | - run:
21 | name: Detect circular dependencies
22 | command: npm run circular
23 | - run:
24 | name: Unit tests
25 | command: npm run test
26 | - run:
27 | name: Build
28 | command: npm run build
29 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | *.test.js
2 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['airbnb', 'prettier', 'prettier/react'],
3 | plugins: ['react', 'react-hooks', 'jest', 'prettier'],
4 | parser: 'babel-eslint',
5 | rules: {
6 | 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
7 | 'react/no-unused-prop-types': 1,
8 | 'react/forbid-prop-types': 1,
9 | 'react/no-unescaped-entities': 1,
10 | 'react/require-default-props': 1,
11 | 'react/no-did-mount-set-state': 1,
12 | 'react/sort-comp': 1,
13 | 'react/prop-types': 1,
14 | 'import/first': 1,
15 | 'import/prefer-default-export': 1,
16 | camelcase: 1,
17 | 'jsx-a11y/click-events-have-key-events': 1,
18 | 'jsx-a11y/no-static-element-interactions': 1,
19 | 'jsx-a11y/anchor-is-valid': 1,
20 | 'jsx-a11y/alt-text': 1,
21 | 'jsx-a11y/mouse-events-have-key-events': 1,
22 | 'jsx-a11y/no-noninteractive-element-interactions': 1,
23 | 'no-unused-expressions': 1,
24 | 'no-unused-vars': 1,
25 | 'no-nested-ternary': 1,
26 | 'no-useless-constructor': 1,
27 | 'no-restricted-globals': 1,
28 | 'array-callback-return': 1,
29 | 'consistent-return': 1,
30 | 'no-new': 1,
31 | 'import/first': 1,
32 | 'no-use-before-define': 1,
33 | 'no-shadow': 1
34 | },
35 | settings: {
36 | react: {
37 | version: 'detect'
38 | }
39 | },
40 | env: {
41 | browser: true
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | coverage/
4 | demo-config.js
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .bowerrc
2 | .DS_Store
3 | .git*
4 | *.log
5 | *.md
6 |
7 | .babelrc
8 | test/components
9 | setupTests.js
10 | .travis.yml
11 | jest.config.js
12 | webpack.config.js
13 | .eslintrc
14 |
15 | CHANGELOG.md
16 | README.md
17 |
18 | docs
19 | test
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # We <3 Contributions!
2 |
3 | This is an open source project and we love involvement from the community! Hit us up with pull requests and issues. The more contributions the better!
4 |
5 | Run the following commands to install and build this project:
6 |
7 | ```ssh
8 | # Clone the repo
9 | $ git clone https://github.com/keen/dashboard-builder.git && cd dashboard-builder
10 |
11 | # Install project dependencies
12 | npm install
13 |
14 | # Start dev server
15 | npm start
16 |
17 | # Build
18 | npm run build
19 | ```
20 |
21 | ## Submitting a Pull Request
22 |
23 | Use the template below. If certain testing steps are not relevant, specify that in the PR. If additional checks are needed, add 'em! Please run through all testing steps before asking for a review.
24 |
25 | ```
26 | ## What does this PR do? How does it affect users?
27 |
28 | ## How should this be tested?
29 |
30 | Step through the code line by line. Things to keep in mind as you review:
31 | - Are there any edge cases not covered by this code?
32 | - Does this code follow conventions (naming, formatting, modularization, etc) where applicable?
33 |
34 | Fetch the branch and/or deploy to staging to test the following:
35 |
36 | - [ ] Does the code compile without warnings (check shell, console)?
37 | - [ ] Do all tests pass?
38 | - [ ] Does the UI, pixel by pixel, look exactly as expected (check various screen sizes, including mobile)?
39 | - [ ] If the feature makes requests from the browser, inspect them in the Web Inspector. Do they look as expected (parameters, headers, etc)?
40 | - [ ] If the feature sends data to Keen, is the data visible in the project if you run an extraction (include link to collection/query)?
41 | - [ ] If the feature saves data to a database, can you confirm the data is indeed created in the database?
42 |
43 | ## Related tickets?
44 | ```
45 |
46 | This PR template can be viewed rendered in Markdown [here](./.github/PULL_REQUEST_TEMPLATE.md).
47 |
48 | ## Publishing on the NPM
49 |
50 | ```ssh
51 | # create new tag - patch | minor | major (SEMVER)
52 | $ npm version patch
53 | ```
54 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Keen IO
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Keen Dashboard Builder
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ## Build status
12 |
13 | [](https://circleci.com/gh/keen/dashboard-builder/tree/develop)
14 |
15 | ## Install
16 |
17 | For npm package manager
18 |
19 | ```ssh
20 | npm install keen-dashboard-builder --save
21 | ```
22 | For yarn
23 | ```ssh
24 | yarn add keen-dashboard-builder
25 | ```
26 |
27 | ## Example
28 |
29 | ```javascript
30 | const myDashboardBuilder = new DashboardBuilder({
31 | container: '#app-container',
32 | keenAnalysis: {
33 | config: {
34 | projectId: 'YOUR_PROJECT_ID',
35 | masterKey: 'YOUR_MASTER_KEY',
36 | protocol: 'https',
37 | host: 'api.keen.io'
38 | }
39 | },
40 | keenWebHost: 'keen.io' // optional, the default is window.location.host
41 | });
42 | ```
43 |
44 | ## React component
45 |
46 | https://github.com/keen/react-dashboards
47 |
48 | ## npm scripts
49 |
50 | List of useful commands that could be used by developers.
51 |
52 | | Command | Description |
53 | | --------------------- | --------------------------------------------------------------------------------- |
54 | | `lint` | run linter against current application codebase. |
55 | | `test` | run unit tests against current application codebase. |
56 | | `circular` | run scripts responsible for the detection of circular dependencies between files. |
57 | | `commit` | run commit command line interface. |
58 | | `prettier` | run code formatter process against current codebase. |
59 |
60 | ## commit
61 |
62 | This project uses [Conventional Commits](https://www.conventionalcommits.org) to enforce common commit standards.
63 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional']
3 | };
4 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
50 |
51 |
52 |
53 | {dashboardSaved && (
54 |
55 |
61 | Saving...
62 |
63 | )}
64 | {keenWebHost !== 'none' && (dashboardsMenu || isDashboardLoading) && (
65 |
this.props.toggleDashboardsMenu()
70 | : null
71 | }
72 | />
73 | )}
74 | {keenWebHost !== 'none' && isDashboardLoading && (
75 |
76 |
77 |
78 |
79 |
80 | )}
81 |
82 | );
83 | }
84 | }
85 |
86 | const mapStateToProps = state => {
87 | const { dashboardSaved, dashboardsMenu, isDashboardLoading } = state.app;
88 | return {
89 | dashboardSaved,
90 | dashboardsMenu,
91 | isDashboardLoading
92 | };
93 | };
94 |
95 | const mapDispatchToProps = {
96 | getSavedQueries,
97 | loadDashboardInfo,
98 | clearDashboardInfo,
99 | toggleDashboardsMenu,
100 | loadDashboards,
101 | loadingSingleDashboard
102 | };
103 |
104 | export default connect(
105 | mapStateToProps,
106 | mapDispatchToProps
107 | )(Editor);
108 |
--------------------------------------------------------------------------------
/lib/builder/components/EditorDashboardTopBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import {
4 | saveDashboard,
5 | hideSavedDashboardMessage,
6 | toggleDashboardsMenu,
7 | makeDashboardPublicAndSave
8 | } from '../../actions/rootActions';
9 | import { Link } from 'react-router-dom';
10 | import domtoimage from 'dom-to-image';
11 | import { saveAs } from 'file-saver';
12 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
13 | import ReactTooltip from 'react-tooltip';
14 |
15 | const EditorDashboardTopBar = props => {
16 | const {
17 | dashboardInfo,
18 | version,
19 | screenSize,
20 | saveDashboard,
21 | hideSavedDashboardMessage,
22 | toggleDashboardsMenu,
23 | makeDashboardPublicAndSave
24 | } = props;
25 | const hideSavedDashboardMessageHandler = () => {
26 | setTimeout(() => {
27 | hideSavedDashboardMessage();
28 | }, 2000);
29 | };
30 | const saveDashboardHandler = () => {
31 | saveDashboard(props.dashboardInfo);
32 | hideSavedDashboardMessageHandler();
33 | };
34 | const handleShareIconClick = () => {
35 | toggleDashboardsMenu('share');
36 | makeDashboardPublicAndSave(dashboardInfo);
37 | hideSavedDashboardMessageHandler();
38 | };
39 | const handleEmbedIconClick = () => {
40 | toggleDashboardsMenu('embed');
41 | makeDashboardPublicAndSave(dashboardInfo);
42 | hideSavedDashboardMessageHandler();
43 | };
44 |
45 | const exportToImage = node => {
46 | if (!node) return;
47 | domtoimage.toBlob(node).then(blob => {
48 | saveAs(blob, 'dashboard.png');
49 | });
50 | };
51 |
52 | const dashboardElement = document.getElementById(
53 | `dashboard-${dashboardInfo.id}`
54 | );
55 |
56 | return (
57 |
58 |
59 | {version === 'viewer' && (
60 | exportToImage(dashboardElement)}
67 | />
68 | )}
69 | dataTip}
75 | />
76 |
77 |
78 | {version === 'viewer' && (
79 |
80 |
86 |
87 |
88 |
dataTip}
94 | />
95 |
96 | )}
97 | {version === 'viewer' && (
98 |
99 |
105 |
106 |
107 |
dataTip}
113 | />
114 |
115 | )}
116 | {version === 'editor' && (
117 | <>
118 |
119 |
120 | saveDashboard(dashboardInfo)}
124 | />
125 |
126 |
127 |
dataTip}
133 | />
134 | >
135 | )}
136 | {version === 'editor' && (
137 |
143 | )}
144 |
145 |
146 | );
147 | };
148 |
149 | const mapStateToProps = state => {
150 | const { dashboardInfo, dashboardsMenu, screenSize } = state.app;
151 | return {
152 | dashboardInfo,
153 | dashboardsMenu,
154 | screenSize
155 | };
156 | };
157 |
158 | const mapDispatchToProps = {
159 | saveDashboard,
160 | hideSavedDashboardMessage,
161 | toggleDashboardsMenu,
162 | makeDashboardPublicAndSave
163 | };
164 |
165 | export default connect(
166 | mapStateToProps,
167 | mapDispatchToProps
168 | )(EditorDashboardTopBar);
169 |
--------------------------------------------------------------------------------
/lib/builder/components/EditorDashboardsSwitch.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { withRouter } from 'react-router-dom';
4 | import {
5 | toggleDashboardsMenu,
6 | loadDashboardInfo,
7 | clearAccessKey,
8 | setNewDashboardForFocus,
9 | addDashboardItem,
10 | loadingSingleDashboard,
11 | filterDashboardsMenu
12 | } from '../../actions/rootActions';
13 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
14 | import NewDashboardButton from './NewDashboardButton';
15 | import ReactTimeAgo from 'react-time-ago';
16 |
17 | const EditorDashboardsSwitch = props => {
18 | const changeDashboard = id => {
19 | props.loadingSingleDashboard();
20 | props.setNewDashboardForFocus(id);
21 | props.history.push(id);
22 | props.loadDashboardInfo(id);
23 | props.toggleDashboardsMenu();
24 | props.clearAccessKey();
25 | };
26 |
27 | const { dashboardList, active, dashboardMenuFilter } = props;
28 | const filteredDashboards = dashboardList.filter(
29 | el =>
30 | dashboardMenuFilter === '' ||
31 | (el.title &&
32 | el.title.toLowerCase().includes(dashboardMenuFilter.toLowerCase()))
33 | );
34 | return (
35 |
36 |
37 |
38 | Dashboards
39 | props.toggleDashboardsMenu()}
43 | />
44 |
45 |
46 | props.filterDashboardsMenu(e.target.value)}
50 | />
51 |
52 |
53 | {filteredDashboards.map(el => (
54 |
changeDashboard(el.id)}
57 | className={active === el.id ? 'item active' : 'item'}
58 | >
59 |
60 | {active === el.id && (
61 |
62 | )}
63 | {el.title}
64 |
65 |
69 |
70 | ))}
71 |
72 |
73 |
74 |
75 |
76 |
77 | );
78 | };
79 |
80 | const mapStateToProps = state => {
81 | const {
82 | dashboardList,
83 | dashboardInfo: { id: active },
84 | dashboardMenuFilter
85 | } = state.app;
86 | return {
87 | dashboardList,
88 | active,
89 | dashboardMenuFilter
90 | };
91 | };
92 |
93 | const mapDispatchToProps = {
94 | toggleDashboardsMenu,
95 | loadDashboardInfo,
96 | clearAccessKey,
97 | setNewDashboardForFocus,
98 | addDashboardItem,
99 | loadingSingleDashboard,
100 | filterDashboardsMenu
101 | };
102 |
103 | export default withRouter(
104 | connect(
105 | mapStateToProps,
106 | mapDispatchToProps
107 | )(EditorDashboardsSwitch)
108 | );
109 |
--------------------------------------------------------------------------------
/lib/builder/components/EditorToolbar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import {
4 | dragStartHandler,
5 | showToolbar,
6 | closeToolbar,
7 | dropHandler
8 | } from '../../actions/rootActions';
9 | import { generateUniqueId } from '../../utils/generateUniqueId';
10 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
11 | import ReactTooltip from 'react-tooltip';
12 |
13 | class EditorToolbar extends Component {
14 | constructor(props) {
15 | super(props);
16 | }
17 |
18 | dragStartHandler = e => {
19 | // this is a hack for firefox
20 | // Firefox requires some kind of initialization
21 | // which we can do by adding this attribute
22 | // @see https://bugzilla.mozilla.org/show_bug.cgi?id=568313
23 | e.dataTransfer.setData('text/plain', '');
24 | this.props.dragStartHandler(e.target.getAttribute('name'));
25 | };
26 |
27 | addElementOnClick = (e, name) => {
28 | const { dropHandler } = this.props;
29 | const id = `chart-${generateUniqueId()}`;
30 | const newElement = {
31 | i: id,
32 | x: 0,
33 | y: Infinity,
34 | w: 2,
35 | h: 4,
36 | type: name,
37 | savedQuery: [],
38 | src: '',
39 | text: '',
40 | error: false
41 | };
42 | dropHandler(newElement, id);
43 | };
44 |
45 | render() {
46 | return (
47 |
53 |
57 |
58 |
Charts
59 |
this.dragStartHandler(e)}
64 | name="metric"
65 | onClick={e => this.addElementOnClick(e, 'metric')}
66 | className="metric-chart-icon"
67 | >
68 | 123
69 |
70 |
this.dragStartHandler(e)}
75 | name="bar"
76 | onClick={e => this.addElementOnClick(e, 'bar')}
77 | >
78 |
79 |
80 |
this.dragStartHandler(e)}
85 | name="line"
86 | onClick={e => this.addElementOnClick(e, 'line')}
87 | >
88 |
89 |
90 |
this.dragStartHandler(e)}
95 | name="area"
96 | onClick={e => this.addElementOnClick(e, 'area')}
97 | >
98 |
99 |
100 |
this.dragStartHandler(e)}
105 | name="pie"
106 | onClick={e => this.addElementOnClick(e, 'pie')}
107 | >
108 |
109 |
110 |
this.dragStartHandler(e)}
115 | name="table"
116 | onClick={e => this.addElementOnClick(e, 'table')}
117 | >
118 |
119 |
120 |
this.dragStartHandler(e)}
125 | name="funnel"
126 | onClick={e => this.addElementOnClick(e, 'funnel')}
127 | >
128 |
129 |
130 |
Elements
131 |
this.dragStartHandler(e)}
136 | name="paragraph"
137 | onClick={e => this.addElementOnClick(e, 'paragraph')}
138 | >
139 |
140 |
141 |
this.dragStartHandler(e)}
146 | name="image"
147 | onClick={e => this.addElementOnClick(e, 'image')}
148 | >
149 |
150 |
151 |
dataTip}
157 | />
158 |
159 |
160 |
161 |
162 |
Add item
163 |
164 |
165 | );
166 | }
167 | }
168 |
169 | const mapStateToProps = state => {
170 | const {
171 | dashboardInfo: {
172 | id,
173 | settings: { layout }
174 | },
175 | isMoving,
176 | isResizing,
177 | settingsVisible,
178 | toolbarVisible
179 | } = state.app;
180 | return {
181 | id,
182 | layout,
183 | isMoving,
184 | isResizing,
185 | settingsVisible,
186 | toolbarVisible
187 | };
188 | };
189 |
190 | const mapDispatchToProps = {
191 | dragStartHandler,
192 | showToolbar,
193 | closeToolbar,
194 | dropHandler
195 | };
196 |
197 | export default connect(
198 | mapStateToProps,
199 | mapDispatchToProps
200 | )(EditorToolbar);
201 |
--------------------------------------------------------------------------------
/lib/builder/components/EmbedDashboard.js:
--------------------------------------------------------------------------------
1 | /* global KEEN_DASHBOARD_BUILDER_VERSION */
2 | import React, { useEffect, useContext } from 'react';
3 | import { connect } from 'react-redux';
4 | import { withRouter } from 'react-router-dom';
5 | import Prism from 'prismjs';
6 | import { toggleDashboardsMenu, setAccessKey } from '../../actions/rootActions';
7 | import getKeyFromAPI from '../../func/getKeyFromAPI';
8 | import copyToClipboard from '../../func/copyToClipboard';
9 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
10 |
11 | import KeenAnalysisContext from '../../contexts/keenAnalysis';
12 |
13 | const EmbedDashboards = props => {
14 | const keenAnalysis = useContext(KeenAnalysisContext);
15 | useEffect(() => {
16 | if (!props.accessKey) {
17 | getKeyFromAPI(props.dashboardInfo.data, props.id, keenAnalysis).then(
18 | data => props.setAccessKey(data)
19 | );
20 | }
21 | }, []);
22 |
23 | const code = `
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
55 |
56 | `;
57 |
58 | const prismedHtml = Prism.highlight(code, Prism.languages.html, 'html');
59 |
60 | return (
61 |
62 |
63 | Embed Dashboard
64 | props.toggleDashboardsMenu()}
68 | />
69 |
70 |
71 |
75 |
83 |
84 |
85 | );
86 | };
87 |
88 | const mapStateToProps = state => {
89 | const { dashboardInfo, accessKey } = state.app;
90 | return {
91 | dashboardInfo,
92 | accessKey
93 | };
94 | };
95 |
96 | const mapDispatchToProps = {
97 | toggleDashboardsMenu,
98 | setAccessKey: value => setAccessKey(value)
99 | };
100 |
101 | export default withRouter(
102 | connect(
103 | mapStateToProps,
104 | mapDispatchToProps
105 | )(EmbedDashboards)
106 | );
107 |
--------------------------------------------------------------------------------
/lib/builder/components/Explorer.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useContext } from 'react';
2 | import PropTypes from 'prop-types';
3 | import KeenExplorer from 'keen-explorer';
4 |
5 | import KeenAnalysisContext from '../../contexts/keenAnalysis';
6 |
7 | const Explorer = props => {
8 | const client = useContext(KeenAnalysisContext);
9 | useEffect(() => {
10 | new KeenExplorer({
11 | container: '#dashboard-builder-explorer',
12 | keenAnalysis: {
13 | config: {
14 | projectId: client.projectId(),
15 | masterKey: client.masterKey(),
16 | readKey: client.readKey()
17 | }
18 | },
19 | components: {
20 | results: false
21 | },
22 | ...props
23 | });
24 | }, []);
25 |
26 | return
;
27 | };
28 |
29 | export default Explorer;
30 |
31 | Explorer.propTypes = {
32 | container: PropTypes.string,
33 | keenAnalysis: PropTypes.shape({
34 | projectId: PropTypes.string,
35 | masterKey: PropTypes.string,
36 | readKey: PropTypes.string
37 | })
38 | };
39 |
--------------------------------------------------------------------------------
/lib/builder/components/Image.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 |
4 | const Image = ({ src }) => {
5 | return src ? (
6 |

7 | ) : (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default Image;
15 |
--------------------------------------------------------------------------------
/lib/builder/components/Main.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { connect } from 'react-redux';
3 | import { loadDashboards } from '../../actions/rootActions';
4 | import MainContainer from '../../viewer/components/MainContainer';
5 |
6 | const Main = props => {
7 | useEffect(() => {
8 | props.loadDashboards();
9 | }, []);
10 | return
;
11 | };
12 |
13 | const mapDispatchToProps = {
14 | loadDashboards
15 | };
16 |
17 | export default connect(
18 | null,
19 | mapDispatchToProps
20 | )(Main);
21 |
--------------------------------------------------------------------------------
/lib/builder/components/NewDashboardButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { withRouter } from 'react-router-dom';
4 | import {
5 | addDashboardItem,
6 | toggleDashboardsMenu,
7 | setNewDashboardForFocus,
8 | clearAccessKey
9 | } from '../../actions/rootActions';
10 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
11 |
12 | class NewDashboardButton extends React.Component {
13 | constructor(props) {
14 | super(props);
15 | }
16 |
17 | addDashboard = () => {
18 | this.props.addDashboardItem();
19 | };
20 |
21 | componentDidUpdate() {
22 | let dashboardId;
23 | if (this.props.id && this.props.newDashboardId === false) {
24 | dashboardId = this.props.id;
25 | }
26 | if (this.props.id === this.props.newDashboardId) {
27 | dashboardId = this.props.newDashboardId;
28 | }
29 | if (dashboardId) {
30 | this.props.history.push(`/editor/${dashboardId}`);
31 | this.props.setNewDashboardForFocus(dashboardId);
32 | this.props.dashboardsMenu && this.props.toggleDashboardsMenu();
33 | this.props.clearAccessKey();
34 | }
35 | }
36 |
37 | render() {
38 | return (
39 |
40 | New dashboard
41 |
42 | );
43 | }
44 | }
45 |
46 | const mapStateToProps = state => {
47 | const {
48 | dashboardInfo: { id },
49 | dashboardsMenu,
50 | newDashboardId
51 | } = state.app;
52 | return {
53 | id,
54 | dashboardsMenu,
55 | newDashboardId
56 | };
57 | };
58 |
59 | const mapDispatchToProps = {
60 | addDashboardItem,
61 | toggleDashboardsMenu,
62 | setNewDashboardForFocus,
63 | clearAccessKey
64 | };
65 |
66 | export default withRouter(
67 | connect(
68 | mapStateToProps,
69 | mapDispatchToProps
70 | )(NewDashboardButton)
71 | );
72 |
--------------------------------------------------------------------------------
/lib/builder/components/Paragraph.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactHtmlParser from 'react-html-parser';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 |
5 | const Paragraph = ({ width, height, text }) => {
6 | return text ? (
7 |
8 | {ReactHtmlParser(text)}
9 |
10 | ) : (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default Paragraph;
18 |
--------------------------------------------------------------------------------
/lib/builder/components/SavedQueriesSelect.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { setLoading, selectSavedQuery } from '../../actions/rootActions';
4 | import ChartTypeUtils from '../../func/ChartType';
5 | import Select from 'react-select';
6 |
7 | class SavedQueriesSelect extends Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | savedQueriesForChart: []
12 | };
13 | }
14 |
15 | componentDidMount() {
16 | this.setState({
17 | savedQueriesForChart: this.props.savedQueries
18 | .filter(el => {
19 | if (
20 | ChartTypeUtils.getChartTypeOptions(el.query).includes(
21 | this.props.type
22 | ) &&
23 | el.metadata &&
24 | el.metadata.display_name
25 | ) {
26 | return el;
27 | }
28 | })
29 | .map(el => ({
30 | value: el.query_name,
31 | label: el.metadata.display_name
32 | }))
33 | });
34 | }
35 |
36 | UNSAFE_componentWillReceiveProps(nextProps) {
37 | if (
38 | this.props.type !== nextProps.type ||
39 | this.props.settingsVisible !== nextProps.settingsVisible
40 | ) {
41 | this.setState({
42 | savedQueriesForChart: nextProps.savedQueries
43 | .filter(el => {
44 | if (
45 | ChartTypeUtils.getChartTypeOptions(el.query).includes(
46 | nextProps.type
47 | ) &&
48 | el.metadata &&
49 | el.metadata.display_name
50 | ) {
51 | return el;
52 | }
53 | })
54 | .map(el => ({
55 | value: el.query_name,
56 | label: el.metadata.display_name
57 | }))
58 | });
59 | }
60 | }
61 |
62 | selectSavedQuery = (query, id) => {
63 | const { items } = this.props;
64 | const item = items.find(item => item.i === id);
65 | const index = item.i;
66 | const loader = query.length ? index : false;
67 |
68 | this.props.setLoading(loader);
69 | this.props.selectSavedQuery(query, index);
70 | };
71 |
72 | render() {
73 | const { savedQueriesForChart } = this.state;
74 | const { index, value, type } = this.props;
75 | return (
76 |
77 | {savedQueriesForChart.length > 0 ? (
78 |
90 | );
91 | }
92 | }
93 |
94 | const mapStateToProps = state => {
95 | const {
96 | savedQueries,
97 | settingsVisible,
98 | dashboardInfo: {
99 | settings: { items }
100 | }
101 | } = state.app;
102 | return {
103 | savedQueries,
104 | settingsVisible,
105 | items
106 | };
107 | };
108 |
109 | const mapDispatchTopProsp = {
110 | setLoading,
111 | selectSavedQuery
112 | };
113 |
114 | export default connect(
115 | mapStateToProps,
116 | mapDispatchTopProsp
117 | )(SavedQueriesSelect);
118 |
--------------------------------------------------------------------------------
/lib/builder/components/Settings.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import SettingsChart from './SettingsChart';
4 | import SettingsImage from './SettingsImage';
5 | import SettingsParagraph from './SettingsParagraph';
6 | import SettingsDashboard from './SettingsDashboard';
7 |
8 | import KeenAnalysisContext from '../../contexts/keenAnalysis';
9 |
10 | const Settings = ({ settingsVisible, items, querySource }) => {
11 | return (
12 |
13 |
14 | {settingsVisible !== false && items ? (
15 |
16 | {/* Chart settings */}
17 | {items.type !== 'image' && items.type !== 'paragraph' && (
18 |
19 | {keenAnalysis => (
20 |
24 | )}
25 |
26 | )}
27 | {/* Image settings */}
28 | {items.type === 'image' && (
29 |
30 |
31 |
32 | )}
33 | {/* Paragraph settings */}
34 | {items.type === 'paragraph' && (
35 |
36 |
37 |
38 | )}
39 |
40 | ) : (
41 |
42 | )}
43 |
44 |
45 | );
46 | };
47 |
48 | const mapStateToProps = state => {
49 | const {
50 | settingsVisible,
51 | dashboardInfo: {
52 | settings: { items = [] }
53 | }
54 | } = state.app;
55 | return {
56 | settingsVisible,
57 | items: items.find(item => item.i === settingsVisible)
58 | };
59 | };
60 |
61 | export default connect(mapStateToProps)(Settings);
62 |
--------------------------------------------------------------------------------
/lib/builder/components/SettingsChart.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | import React, { Component } from 'react';
4 | import { connect } from 'react-redux';
5 | import {
6 | savedQueryError,
7 | changeChartType,
8 | setChartTheme
9 | } from '../../actions/rootActions';
10 | import Select from 'react-select';
11 | import SavedQueriesSelect from './SavedQueriesSelect';
12 | import Explorer from './Explorer';
13 | import ChartTypeUtils from '../../func/ChartType';
14 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
15 | import isEqual from 'lodash/isEqual';
16 | import debounce from 'lodash/debounce';
17 | import Builder from 'keen-theme-builder';
18 |
19 | class SettingsChart extends Component {
20 | constructor(props) {
21 | super(props);
22 | this.state = {
23 | results: '',
24 | loading: false,
25 | type: '',
26 | chartTypeForSelect: {},
27 | availableChartTypes: []
28 | };
29 | }
30 |
31 | generateSettings = props => {
32 | if (props.items.savedQuery[0]) {
33 | this.setState({
34 | loading: true
35 | });
36 | this.props.keenAnalysis
37 | .get({
38 | url: this.props.keenAnalysis.url(
39 | 'queries',
40 | 'saved',
41 | props.items.savedQuery[0].value
42 | ),
43 | api_key: this.props.keenAnalysis.masterKey()
44 | })
45 | .then(results => {
46 | const newType = ChartTypeUtils.getChartTypeOptions(results.query);
47 | const type = props.items.type ? props.items.type : newType[0];
48 | this.setState({
49 | results,
50 | type,
51 | loading: false
52 | });
53 | if (this.state.type) {
54 | const { type, results } = this.state;
55 | this.props.changeChartType(type, this.props.settingsVisible);
56 | const typeForSelect = item => {
57 | const label = item.replace('-', ' ');
58 | return label.charAt(0).toUpperCase() + label.slice(1);
59 | };
60 |
61 | const chartList =
62 | results &&
63 | ChartTypeUtils.getChartTypeOptions(results.query).map(value => {
64 | return {
65 | value,
66 | label: typeForSelect(value)
67 | };
68 | });
69 |
70 | const chartListFiltered = chartList.filter(
71 | el =>
72 | el.value &&
73 | (el.value.includes('area') || el.value.includes('line'))
74 | );
75 |
76 | const availableChartTypes =
77 | props.items.savedQuery.length > 1 ? chartListFiltered : chartList;
78 |
79 | const chartTypeForSelect = {
80 | value: type,
81 | label: typeForSelect(type)
82 | };
83 |
84 | this.setState({
85 | chartTypeForSelect,
86 | availableChartTypes
87 | });
88 | }
89 | })
90 | .catch(({ body: err }) => {
91 | props.savedQueryError(err, props.settingsVisible);
92 | this.setState({
93 | loading: false
94 | });
95 | });
96 | }
97 | this.setState({
98 | type: props.items.type
99 | });
100 | };
101 |
102 | componentDidMount() {
103 | if (this.props.items.savedQuery) {
104 | this.generateSettings(this.props);
105 | }
106 | }
107 |
108 | UNSAFE_componentWillReceiveProps(nextProps) {
109 | if (
110 | this.props.settingsVisible !== nextProps.settingsVisible ||
111 | nextProps.items.type !== this.props.items.type ||
112 | isEqual(nextProps.items.savedQuery, this.props.items.savedQuery) ===
113 | false ||
114 | nextProps.items.savedQuery.length === 0
115 | ) {
116 | this.generateSettings(nextProps);
117 | this.setState({
118 | type: nextProps.items.type
119 | });
120 | }
121 | }
122 |
123 | builderChartStyle = debounce(data => {
124 | this.props.setChartTheme(this.props.settingsVisible, data);
125 | }, 500);
126 |
127 | render() {
128 | const {
129 | settingsVisible,
130 | items: { savedQuery },
131 | isLoading,
132 | id,
133 | theme,
134 | charts_theme,
135 | querySource
136 | } = this.props;
137 |
138 | const {
139 | results,
140 | loading,
141 | type,
142 | chartTypeForSelect,
143 | availableChartTypes
144 | } = this.state;
145 | const builderOptions = {
146 | chart: type
147 | };
148 |
149 | return (
150 |
151 | {querySource === 'explorer' &&
}
152 |
153 | {!querySource && (
154 | <>
155 |
Saved Query
156 |
161 | >
162 | )}
163 | {savedQuery && savedQuery.length !== 0 && (
164 | <>
165 | Chart type
166 |
176 | {savedQuery.length !== 0 && (
177 |
194 | )}
195 | {(loading || isLoading === settingsVisible) && (
196 |
197 |
198 |
199 |
200 |
201 | )}
202 |
203 | );
204 | }
205 | }
206 |
207 | const mapStateToProps = state => {
208 | const {
209 | settingsVisible,
210 | dashboardInfo: {
211 | id,
212 | settings: { theme, charts_theme, items }
213 | },
214 | isLoading
215 | } = state.app;
216 | return {
217 | id,
218 | settingsVisible,
219 | items: items.find(item => item.i === settingsVisible),
220 | isLoading,
221 | theme,
222 | charts_theme
223 | };
224 | };
225 |
226 | const mapDispatchToProps = {
227 | savedQueryError,
228 | changeChartType,
229 | setChartTheme
230 | };
231 |
232 | export default connect(
233 | mapStateToProps,
234 | mapDispatchToProps
235 | )(SettingsChart);
236 |
--------------------------------------------------------------------------------
/lib/builder/components/SettingsDashboard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import {
4 | toggleDryRun,
5 | toggleIsPublic,
6 | setTheme
7 | } from '../../actions/rootActions';
8 | import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
9 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
10 | import Builder from 'keen-theme-builder';
11 | import Switcher from './Switcher';
12 | import ReactTooltip from 'react-tooltip';
13 | import debounce from 'lodash/debounce';
14 |
15 | const SettingsDashboard = props => {
16 | const builderOnChange = debounce(data => {
17 | props.setTheme(data);
18 | }, 500);
19 |
20 | return (
21 |
22 |
23 | Theme
24 | Settings
25 |
26 |
27 |
28 |
35 |
36 |
37 |
38 |
45 | dataTip}
52 | />
53 |
54 | Is Public
55 |
56 | dataTip}
62 | />
63 |
64 |
65 |
69 | Dry run
70 |
71 | dataTip}
77 | />
78 |
79 |
80 |
81 | );
82 | };
83 |
84 | const mapStateToProps = state => {
85 | const { dryRun, theme } = state.app.dashboardInfo.settings;
86 | const { is_public, id } = state.app.dashboardInfo;
87 | return {
88 | dryRun,
89 | theme,
90 | id,
91 | isPublic: is_public
92 | };
93 | };
94 |
95 | const mapDispatchToProps = {
96 | toggleDryRun,
97 | toggleIsPublic,
98 | setTheme
99 | };
100 |
101 | export default connect(
102 | mapStateToProps,
103 | mapDispatchToProps
104 | )(SettingsDashboard);
105 |
--------------------------------------------------------------------------------
/lib/builder/components/SettingsImage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { setSrcForImg } from '../../actions/rootActions';
4 |
5 | const SettingsImage = props => {
6 | const { items, settingsVisible } = props;
7 | const setSrcForImg = e => {
8 | props.setSrcForImg(e.target.value, settingsVisible);
9 | };
10 | const item = items.find(item => item.i === settingsVisible);
11 | const imageSrc = (item && item.src) || '';
12 | return (
13 |
14 |
Image url
15 | setSrcForImg(e)}
17 | className="settings-input"
18 | defaultValue={imageSrc}
19 | />
20 |
21 | );
22 | };
23 |
24 | const mapStateToProps = state => {
25 | const {
26 | settingsVisible,
27 | dashboardInfo: {
28 | settings: { items }
29 | }
30 | } = state.app;
31 | return {
32 | settingsVisible,
33 | items
34 | };
35 | };
36 |
37 | const mapDispatchTopProps = {
38 | setSrcForImg
39 | };
40 |
41 | export default connect(
42 | mapStateToProps,
43 | mapDispatchTopProps
44 | )(SettingsImage);
45 |
--------------------------------------------------------------------------------
/lib/builder/components/SettingsParagraph.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TextEditor from './TextEditor';
3 |
4 | const SettingsParagraph = () => {
5 | return (
6 |
7 |
8 |
Text
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | export default SettingsParagraph;
16 |
--------------------------------------------------------------------------------
/lib/builder/components/ShareDashboard.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useContext } from 'react';
2 | import { connect } from 'react-redux';
3 | import { withRouter } from 'react-router-dom';
4 | import {
5 | toggleDashboardsMenu,
6 | setAccessKey,
7 | clearAccessKey
8 | } from '../../actions/rootActions';
9 | import getKeyFromAPI from '../../func/getKeyFromAPI';
10 | import copyToClipboard from '../../func/copyToClipboard';
11 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
12 |
13 | import KeenAnalysisContext from '../../contexts/keenAnalysis';
14 |
15 | const ShareDashboards = props => {
16 | const keenAnalysis = useContext(KeenAnalysisContext);
17 | useEffect(() => {
18 | if (!props.accessKey) {
19 | getKeyFromAPI(props.savedQueriesList, props.id, keenAnalysis).then(data =>
20 | props.setAccessKey(data)
21 | );
22 | }
23 | }, []);
24 |
25 | // TODO: Remove hardcoded URL
26 | const getShareUrl = () => {
27 | return `https://dashboards.keen.io/?id=${props.id}&projectId=${props.project_id}&accessKey=${props.accessKey}`;
28 | };
29 |
30 | return (
31 |
32 |
33 | Share Your Dashboard
34 | props.toggleDashboardsMenu()}
38 | />
39 |
40 |
41 |
Here's the link for the read-only access to your dashboard.
42 |
43 | Your dashboard will be accessible to all of the people who you share
44 | this link with.
45 |
46 |
52 |
60 |
61 |
62 | );
63 | };
64 |
65 | const mapStateToProps = state => {
66 | const {
67 | dashboardInfo: {
68 | id,
69 | title,
70 | data,
71 | project_id,
72 | settings: { savedQueriesList }
73 | },
74 | accessKey
75 | } = state.app;
76 | return {
77 | id,
78 | title,
79 | data,
80 | project_id,
81 | accessKey,
82 | savedQueriesList
83 | };
84 | };
85 |
86 | const mapDispatchToProps = {
87 | toggleDashboardsMenu,
88 | setAccessKey,
89 | clearAccessKey
90 | };
91 |
92 | export default withRouter(
93 | connect(
94 | mapStateToProps,
95 | mapDispatchToProps
96 | )(ShareDashboards)
97 | );
98 |
--------------------------------------------------------------------------------
/lib/builder/components/Switcher.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | import React from 'react';
4 | import PropTypes from 'prop-types';
5 |
6 | const Switcher = props => {
7 | const {
8 | text: { title = '', on = 'On', off = 'Off' },
9 | checked,
10 | onChange,
11 | children
12 | } = props;
13 | const switcherTitle = children || (
14 |
{title}
15 | );
16 | return (
17 |
18 |
35 |
36 | );
37 | };
38 |
39 | export default Switcher;
40 |
41 | Switcher.defaultProps = {
42 | text: {},
43 | onChange: () => {}
44 | };
45 |
46 | Switcher.propTypes = {
47 | text: PropTypes.shape({
48 | title: PropTypes.string,
49 | on: PropTypes.string,
50 | off: PropTypes.string
51 | }),
52 | checked: PropTypes.bool.isRequired,
53 | onChange: PropTypes.func.isRequired
54 | };
55 |
--------------------------------------------------------------------------------
/lib/builder/components/TextEditor.js:
--------------------------------------------------------------------------------
1 | import ReactQuill from 'react-quill';
2 | import React from 'react';
3 | import { connect } from 'react-redux';
4 | import { setTextForParagraph } from '../../actions/rootActions';
5 | import 'react-quill/dist/quill.snow.css';
6 |
7 | const TextEditor = props => {
8 | const { text, settingsVisible, items } = props;
9 | const item = items.find(item => item.i === settingsVisible);
10 | const editorText = (item && item.text) || '';
11 | return (
12 |
15 | props.setTextForParagraph(newValue, source, settingsVisible)
16 | }
17 | modules={TextEditor.modules}
18 | />
19 | );
20 | };
21 |
22 | const mapStateToProps = state => {
23 | const {
24 | settingsVisible,
25 | dashboardInfo: {
26 | settings: { items }
27 | }
28 | } = state.app;
29 | return {
30 | settingsVisible,
31 | items
32 | };
33 | };
34 |
35 | const mapDispatchToProps = {
36 | setTextForParagraph
37 | };
38 |
39 | export default connect(
40 | mapStateToProps,
41 | mapDispatchToProps
42 | )(TextEditor);
43 |
44 | TextEditor.modules = {
45 | toolbar: {
46 | container: [
47 | ['bold', 'italic', 'underline', 'strike'],
48 | [{ list: 'ordered' }, { list: 'bullet' }],
49 | [{ header: [1, 2, 3, 4, 5, 6, false] }],
50 | [{ color: [] }],
51 | ['clean']
52 | ]
53 | }
54 | };
55 |
--------------------------------------------------------------------------------
/lib/builder/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import { HashRouter as Router, Route } from 'react-router-dom';
6 | import { createStore, applyMiddleware, compose } from 'redux';
7 | import { Provider } from 'react-redux';
8 | import thunk from 'redux-thunk';
9 | import KeenAnalysis from 'keen-analysis';
10 | import rootReducer from '../reducers/rootReducer';
11 | import Main from './components/Main';
12 | import Editor from './components/Editor';
13 | import 'keen-dataviz/dist/keen-dataviz.css';
14 | import '../../styles/style.css';
15 | import { library } from '@fortawesome/fontawesome-svg-core';
16 | import {
17 | faTrashAlt,
18 | faChartArea,
19 | faChartBar,
20 | faChartLine,
21 | faChartPie,
22 | faPlusCircle,
23 | faSave,
24 | faSearch,
25 | faTable,
26 | faParagraph,
27 | faImage,
28 | faCog,
29 | faClone,
30 | faSpinner,
31 | faShareAlt,
32 | faBars,
33 | faTimes,
34 | faAngleDoubleRight,
35 | faEdit,
36 | faEye,
37 | faFilter
38 | } from '@fortawesome/free-solid-svg-icons';
39 |
40 | library.add(
41 | faTrashAlt,
42 | faChartArea,
43 | faChartBar,
44 | faChartLine,
45 | faChartPie,
46 | faPlusCircle,
47 | faSave,
48 | faSearch,
49 | faTable,
50 | faParagraph,
51 | faImage,
52 | faCog,
53 | faClone,
54 | faSpinner,
55 | faShareAlt,
56 | faBars,
57 | faTimes,
58 | faAngleDoubleRight,
59 | faEdit,
60 | faEye,
61 | faFilter
62 | );
63 | import JavascriptTimeAgo from 'javascript-time-ago';
64 | import en from 'javascript-time-ago/locale/en';
65 |
66 | import KeenAnalysisContext from './contexts/keenAnalysis';
67 |
68 | JavascriptTimeAgo.locale(en);
69 |
70 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
71 |
72 | export let keenGlobals = undefined;
73 | if (typeof webpackKeenGlobals !== 'undefined') {
74 | keenGlobals = webpackKeenGlobals;
75 | }
76 |
77 | export class DashboardBuilder {
78 | constructor(props) {
79 | const { keenAnalysis } = props;
80 |
81 | const client =
82 | keenAnalysis.instance || new KeenAnalysis(keenAnalysis.config);
83 | const keenWebHost = props.keenWebHost || window.location.host;
84 | let keenWebFetchOptions;
85 |
86 | if (!!props.keenWebHost) {
87 | keenWebFetchOptions = {
88 | mode: 'cors',
89 | credentials: 'include'
90 | };
91 | }
92 |
93 | const store = createStore(
94 | rootReducer,
95 | composeEnhancers(
96 | applyMiddleware(
97 | thunk.withExtraArgument({
98 | keenClient: client,
99 | keenWebHost,
100 | keenWebFetchOptions
101 | })
102 | )
103 | )
104 | );
105 |
106 | ReactDOM.render(
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | ,
115 | document.querySelector(props.container)
116 | );
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/lib/constants.js:
--------------------------------------------------------------------------------
1 | export const APP_REDUCER = 'app';
2 |
--------------------------------------------------------------------------------
/lib/contexts/keenAnalysis.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const KeenAnalysisContext = React.createContext({});
4 |
5 | export default KeenAnalysisContext;
6 |
--------------------------------------------------------------------------------
/lib/func/ChartType.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | import _ from 'lodash';
4 |
5 | const ChartTypeUtils = {
6 | getQueryDataType: function(query) {
7 | var isInterval = typeof query.interval === 'string';
8 | var isGroupBy =
9 | typeof query.group_by === 'string' ||
10 | (query.group_by instanceof Array && query.group_by.length === 1);
11 | var is2xGroupBy =
12 | query.group_by instanceof Array && query.group_by.length === 2;
13 | var dataType;
14 |
15 | if (query.analysis_type === 'funnel') {
16 | dataType = 'cat-ordinal';
17 | } else if (query.analysis_type === 'extraction') {
18 | dataType = 'extraction';
19 | } else if (query.analysis_type === 'select_unique') {
20 | dataType = 'nominal';
21 | }
22 |
23 | // metric
24 | else if (!isGroupBy && !isInterval && !is2xGroupBy) {
25 | dataType = 'singular';
26 | }
27 |
28 | // group_by, no interval
29 | else if (isGroupBy && !isInterval) {
30 | dataType = 'categorical';
31 | }
32 |
33 | // interval, no group_by
34 | else if (isInterval && !isGroupBy && !is2xGroupBy) {
35 | dataType = 'chronological';
36 | }
37 |
38 | // interval, group_by
39 | else if (isInterval && (isGroupBy || is2xGroupBy)) {
40 | dataType = 'cat-chronological';
41 | }
42 |
43 | // 2x group_by
44 | // TODO: research possible dataType options
45 | else if (!isInterval && is2xGroupBy) {
46 | dataType = 'categorical';
47 | }
48 |
49 | return dataType;
50 | },
51 |
52 | getChartTypeOptions: function(query) {
53 | var dataTypes = {
54 | singular: ['metric'],
55 | categorical: [
56 | 'pie',
57 | 'bar',
58 | 'choropleth',
59 | 'horizontal-bar',
60 | 'donut',
61 | 'table'
62 | ],
63 | 'cat-interval': [
64 | 'area',
65 | 'bar',
66 | 'horizontal-bar',
67 | 'line',
68 | 'spline',
69 | 'area-spline',
70 | 'step',
71 | 'area-step',
72 | 'table'
73 | ],
74 | 'cat-ordinal': [
75 | 'area',
76 | 'bar',
77 | 'horizontal-bar',
78 | 'line',
79 | 'spline',
80 | 'area-spline',
81 | 'step',
82 | 'area-step',
83 | 'table',
84 | 'funnel',
85 | 'funnel-3d',
86 | 'horizontal-funnel',
87 | 'horizontal-funnel-3d'
88 | ],
89 | chronological: [
90 | 'area',
91 | 'bar',
92 | 'line',
93 | 'spline',
94 | 'area-spline',
95 | 'step',
96 | 'area-step',
97 | 'table',
98 | 'heatmap'
99 | ],
100 | 'cat-chronological': [
101 | 'area',
102 | 'bar',
103 | 'horizontal-bar',
104 | 'line',
105 | 'spline',
106 | 'area-spline',
107 | 'step',
108 | 'area-step',
109 | 'table'
110 | ],
111 | nominal: ['table'],
112 | extraction: ['table']
113 | };
114 | var queryDataType = ChartTypeUtils.getQueryDataType(query);
115 | return dataTypes[queryDataType];
116 | },
117 |
118 | responseSupportsChartType: function(query, chartType) {
119 | return _.includes(ChartTypeUtils.getChartTypeOptions(query), chartType);
120 | },
121 |
122 | isTableChartType: function(chartType) {
123 | return chartType == 'table';
124 | }
125 | };
126 |
127 | export default ChartTypeUtils;
128 |
--------------------------------------------------------------------------------
/lib/func/__snapshots__/classicDashboardDataParse.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`classicDashboardDataParse() should parse data correctly 1`] = `
4 | Object {
5 | "rows": Array [
6 | Object {
7 | "height": 280,
8 | "tiles": Array [
9 | Object {
10 | "column_width": 6,
11 | "query_name": "users",
12 | },
13 | Object {
14 | "column_width": 6,
15 | "query_name": "sales",
16 | },
17 | ],
18 | "title": "Users and sales",
19 | },
20 | ],
21 | "settings": Object {
22 | "dryRun": false,
23 | "items": Array [
24 | Object {
25 | "h": 1,
26 | "i": "7a99a8d0-bf4f-429b-8bb7-520bf069ac59",
27 | "text": "Users and sales
",
28 | "type": "paragraph",
29 | "w": 12,
30 | "x": 0,
31 | "y": 0,
32 | },
33 | Object {
34 | "h": 8,
35 | "i": "7a99a8d0-bf4f-429b-8bb7-520bf069ac59",
36 | "savedQuery": Array [
37 | Object {
38 | "label": "Users",
39 | "value": "users",
40 | },
41 | ],
42 | "sparkline": false,
43 | "w": 6,
44 | "x": 0,
45 | "y": 2,
46 | },
47 | Object {
48 | "h": 8,
49 | "i": "7a99a8d0-bf4f-429b-8bb7-520bf069ac59",
50 | "savedQuery": Array [
51 | Object {
52 | "label": "Sales",
53 | "value": "sales",
54 | },
55 | ],
56 | "sparkline": false,
57 | "w": 6,
58 | "x": 6,
59 | "y": 2,
60 | },
61 | ],
62 | "layout": Array [
63 | Object {
64 | "h": 1,
65 | "i": "7a99a8d0-bf4f-429b-8bb7-520bf069ac59",
66 | "text": "Users and sales
",
67 | "type": "paragraph",
68 | "w": 12,
69 | "x": 0,
70 | "y": 0,
71 | },
72 | Object {
73 | "h": 8,
74 | "i": "7a99a8d0-bf4f-429b-8bb7-520bf069ac59",
75 | "savedQuery": Array [
76 | Object {
77 | "label": "Users",
78 | "value": "users",
79 | },
80 | ],
81 | "sparkline": false,
82 | "w": 6,
83 | "x": 0,
84 | "y": 2,
85 | },
86 | Object {
87 | "h": 8,
88 | "i": "7a99a8d0-bf4f-429b-8bb7-520bf069ac59",
89 | "savedQuery": Array [
90 | Object {
91 | "label": "Sales",
92 | "value": "sales",
93 | },
94 | ],
95 | "sparkline": false,
96 | "w": 6,
97 | "x": 6,
98 | "y": 2,
99 | },
100 | ],
101 | },
102 | }
103 | `;
104 |
--------------------------------------------------------------------------------
/lib/func/__snapshots__/newGridDataParse.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`newGridDataParse() should parse data correctly 1`] = `
4 | Object {
5 | "data": Object {
6 | "items": Array [
7 | Object {
8 | "height": 40,
9 | "left": 20,
10 | "text": "Component
",
11 | "top": 20,
12 | "type": "paragraph",
13 | "width": 1160,
14 | },
15 | Object {
16 | "chartTitle": "Title",
17 | "height": 2800,
18 | "left": 620,
19 | "savedQuery": Array [
20 | Object {
21 | "label": "Saved query",
22 | "value": "saved_query",
23 | },
24 | ],
25 | "top": 80,
26 | "type": "bar",
27 | "width": 560,
28 | },
29 | ],
30 | },
31 | "settings": Object {
32 | "items": Array [
33 | Object {
34 | "h": 0,
35 | "height": 40,
36 | "i": "7a99a8d0-bf4f-429b-8bb7-520bf069ac59",
37 | "left": 20,
38 | "text": "Component
",
39 | "top": 20,
40 | "type": "paragraph",
41 | "w": 11,
42 | "width": 1160,
43 | "x": 0,
44 | "y": 0,
45 | },
46 | Object {
47 | "chartTitle": "Title",
48 | "h": 84,
49 | "height": 2800,
50 | "i": "7a99a8d0-bf4f-429b-8bb7-520bf069ac59",
51 | "left": 620,
52 | "savedQuery": Array [
53 | Object {
54 | "label": "Saved query",
55 | "value": "saved_query",
56 | },
57 | ],
58 | "top": 80,
59 | "type": "bar",
60 | "w": 5,
61 | "width": 560,
62 | "x": 6,
63 | "y": 0,
64 | },
65 | ],
66 | "layout": Array [
67 | Object {
68 | "h": 0,
69 | "height": 40,
70 | "i": "7a99a8d0-bf4f-429b-8bb7-520bf069ac59",
71 | "left": 20,
72 | "text": "Component
",
73 | "top": 20,
74 | "type": "paragraph",
75 | "w": 11,
76 | "width": 1160,
77 | "x": 0,
78 | "y": 0,
79 | },
80 | Object {
81 | "chartTitle": "Title",
82 | "h": 84,
83 | "height": 2800,
84 | "i": "7a99a8d0-bf4f-429b-8bb7-520bf069ac59",
85 | "left": 620,
86 | "savedQuery": Array [
87 | Object {
88 | "label": "Saved query",
89 | "value": "saved_query",
90 | },
91 | ],
92 | "top": 80,
93 | "type": "bar",
94 | "w": 5,
95 | "width": 560,
96 | "x": 6,
97 | "y": 0,
98 | },
99 | ],
100 | },
101 | }
102 | `;
103 |
--------------------------------------------------------------------------------
/lib/func/checkBoundaries.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | export default function checkBoundaries(topPos, leftPos, elWidth, ref) {
4 | const { width } = ref;
5 | const grid = 20;
6 | const top = Math.round((ref.top + window.pageYOffset) / grid) * grid;
7 | const margin = {
8 | top: 20,
9 | left: 20,
10 | right: 20
11 | };
12 | return {
13 | top: topPos <= 0 + top ? margin.top : topPos - top,
14 | left:
15 | leftPos <= 0
16 | ? margin.left
17 | : leftPos + elWidth >= width - margin.right
18 | ? width - elWidth - margin.right
19 | : leftPos
20 | };
21 | }
22 |
--------------------------------------------------------------------------------
/lib/func/classicDashboardDataParse.js:
--------------------------------------------------------------------------------
1 | export default function classicDashboardDataParse(oldDashboard, chartId) {
2 | const typeForSelect = (item = '') => {
3 | if (!item) return '';
4 | const label = item.replace(/-/g, ' ');
5 | return label.charAt(0).toUpperCase() + label.slice(1);
6 | };
7 | let rowTitle;
8 | let topPosition = 0;
9 | const firstOldDashboards = {
10 | ...oldDashboard,
11 | settings: {
12 | items: oldDashboard.rows.map(r => {
13 | topPosition += Math.floor(r.height / 100);
14 | if (r.title) {
15 | rowTitle = {
16 | i: chartId(),
17 | type: 'paragraph',
18 | text: `${r.title}
`,
19 | y: topPosition - Math.floor(r.height / 100),
20 | x: 0,
21 | h: 1,
22 | w: 12
23 | };
24 | topPosition += 2;
25 | }
26 | const elements = r.tiles.map((t, j) => {
27 | let prevElWidth = 0;
28 | if (j !== 0) {
29 | prevElWidth = r.tiles[j - 1].column_width;
30 | }
31 | const elHeight = Math.floor(r.height / 100);
32 | return {
33 | i: chartId(),
34 | h: elHeight * 4,
35 | w: t.column_width,
36 | y: topPosition - elHeight,
37 | x: prevElWidth,
38 | sparkline: false,
39 | savedQuery: [
40 | {
41 | value: t.query_name,
42 | label: typeForSelect(t.query_name)
43 | }
44 | ]
45 | };
46 | });
47 | if (r.title) {
48 | elements.unshift(rowTitle);
49 | }
50 | return elements;
51 | }),
52 | dryRun: false
53 | }
54 | };
55 | const oneArray = [];
56 | firstOldDashboards.settings.items.forEach(r => {
57 | oneArray.push(...r);
58 | });
59 | const translateOldDashboards = {
60 | ...firstOldDashboards,
61 | settings: {
62 | ...firstOldDashboards.settings,
63 | items: oneArray,
64 | layout: oneArray
65 | }
66 | };
67 | return translateOldDashboards;
68 | }
69 |
--------------------------------------------------------------------------------
/lib/func/classicDashboardDataParse.test.js:
--------------------------------------------------------------------------------
1 | import classicDashboardDataParse from './classicDashboardDataParse';
2 |
3 | describe('classicDashboardDataParse()', () => {
4 | it('should parse data correctly', () => {
5 | const dashboardInfo = {
6 | rows: [
7 | {
8 | height: 280,
9 | tiles: [
10 | {
11 | column_width: 6,
12 | query_name: 'users'
13 | },
14 | {
15 | column_width: 6,
16 | query_name: 'sales'
17 | }
18 | ],
19 | title: 'Users and sales'
20 | }
21 | ]
22 | };
23 | const chartUniqueId = jest
24 | .fn()
25 | .mockImplementation(() => '1433be06-9dfa-4caf-8558-f5959e0535eb')
26 | .mockImplementation(() => '76d83720-c703-4127-ad92-7cd37d1ea893')
27 | .mockImplementation(() => '7a99a8d0-bf4f-429b-8bb7-520bf069ac59');
28 | const parsedData = classicDashboardDataParse(dashboardInfo, chartUniqueId);
29 | expect(parsedData).toMatchSnapshot();
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/lib/func/copyToClipboard.js:
--------------------------------------------------------------------------------
1 | const copyToClipboard = str => {
2 | const el = document.createElement('textarea');
3 | el.value = str;
4 | document.body.appendChild(el);
5 | el.select();
6 | document.execCommand('copy');
7 | document.body.removeChild(el);
8 | };
9 |
10 | export default copyToClipboard;
11 |
--------------------------------------------------------------------------------
/lib/func/getKeyFromAPI.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | const getKeyFromAPI = async (savedQueries, id, keenAnalysis) => {
4 | const savedQueriesSet = new Set(savedQueries);
5 | const uniqueSavedQueries = Array.from(savedQueriesSet);
6 | const keyName = `public-dashboard: ${id}`;
7 | let key = await keenAnalysis.get({
8 | url: keenAnalysis.url('projectId', `keys?name=${keyName}`),
9 | api_key: keenAnalysis.masterKey()
10 | });
11 |
12 | if (!key.length) {
13 | key = await keenAnalysis.post({
14 | url: keenAnalysis.url('projectId', 'keys'),
15 | api_key: keenAnalysis.masterKey(),
16 | params: {
17 | name: keyName,
18 | is_active: true,
19 | permitted: ['saved_queries', 'cached_queries'],
20 | options: {
21 | saved_queries: {
22 | allowed: uniqueSavedQueries
23 | },
24 | cached_queries: {
25 | allowed: uniqueSavedQueries
26 | }
27 | }
28 | }
29 | });
30 | return key.key;
31 | }
32 | return key[0].key;
33 | };
34 |
35 | export default getKeyFromAPI;
36 |
--------------------------------------------------------------------------------
/lib/func/newGridDataParse.js:
--------------------------------------------------------------------------------
1 | const newGridDataParse = (dashboardInfo, chartId) => {
2 | const { items } = dashboardInfo.data;
3 | const newGridItems = items.map(chart => {
4 | return {
5 | ...chart,
6 | i: chartId(),
7 | w: Math.floor(chart.width / 100),
8 | h: Math.floor(chart.height / 100) * 3,
9 | x: Math.floor(chart.left / 100),
10 | y: Math.floor(chart.top / 100)
11 | };
12 | });
13 | const newDashboardInfo = {
14 | ...dashboardInfo,
15 | settings: {
16 | ...dashboardInfo.settings,
17 | layout: [...newGridItems],
18 | items: [...newGridItems]
19 | }
20 | };
21 | return newDashboardInfo;
22 | };
23 |
24 | export default newGridDataParse;
25 |
--------------------------------------------------------------------------------
/lib/func/newGridDataParse.test.js:
--------------------------------------------------------------------------------
1 | import newGridDataParse from './newGridDataParse';
2 |
3 | describe('newGridDataParse()', () => {
4 | it('should parse data correctly', () => {
5 | const dashboardInfo = {
6 | data: {
7 | items: [
8 | {
9 | height: 40,
10 | left: 20,
11 | text: 'Component
',
12 | top: 20,
13 | type: 'paragraph',
14 | width: 1160
15 | },
16 | {
17 | height: 2800,
18 | left: 620,
19 | chartTitle: 'Title',
20 | top: 80,
21 | savedQuery: [
22 | {
23 | label: 'Saved query',
24 | value: 'saved_query'
25 | }
26 | ],
27 | type: 'bar',
28 | width: 560
29 | }
30 | ]
31 | }
32 | };
33 | const chartUniqueId = jest
34 | .fn()
35 | .mockImplementation(() => '1433be06-9dfa-4caf-8558-f5959e0535eb')
36 | .mockImplementation(() => '76d83720-c703-4127-ad92-7cd37d1ea893')
37 | .mockImplementation(() => '7a99a8d0-bf4f-429b-8bb7-520bf069ac59');
38 | const parsedData = newGridDataParse(dashboardInfo, chartUniqueId);
39 | expect(parsedData).toMatchSnapshot();
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/lib/func/sortDashboardList.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | const sortDashboardList = (sortOption, arrayToSort) => {
4 | switch (sortOption) {
5 | case 'az':
6 | return arrayToSort.sort((a, b) => {
7 | if (a.title.toLowerCase() < b.title.toLowerCase()) {
8 | return -1;
9 | }
10 | if (a.title.toLowerCase() > b.title.toLowerCase()) {
11 | return 1;
12 | }
13 | return 0;
14 | });
15 |
16 | case 'za':
17 | return arrayToSort.sort((a, b) => {
18 | if (a.title.toLowerCase() < b.title.toLowerCase()) {
19 | return 1;
20 | }
21 | if (a.title.toLowerCase() > b.title.toLowerCase()) {
22 | return -1;
23 | }
24 | return 0;
25 | });
26 |
27 | case 'latest':
28 | return arrayToSort.sort((a, b) => {
29 | if (new Date(a.last_modified_date) < new Date(b.last_modified_date)) {
30 | return 1;
31 | }
32 | if (new Date(a.last_modified_date) > new Date(b.last_modified_date)) {
33 | return -1;
34 | }
35 | return 0;
36 | });
37 |
38 | case 'oldest':
39 | return arrayToSort.sort((a, b) => {
40 | if (new Date(a.last_modified_date) < new Date(b.last_modified_date)) {
41 | return -1;
42 | }
43 | if (new Date(a.last_modified_date) > new Date(b.last_modified_date)) {
44 | return 1;
45 | }
46 | return 0;
47 | });
48 |
49 | default:
50 | break;
51 | }
52 | };
53 |
54 | export default sortDashboardList;
55 |
--------------------------------------------------------------------------------
/lib/func/transformChart.js:
--------------------------------------------------------------------------------
1 | import { generateUniqueId } from '../utils/generateUniqueId';
2 |
3 | const transformChart = chart => {
4 | return chart.id !== undefined
5 | ? chart
6 | : { ...chart, id: `chart-${generateUniqueId()}` };
7 | };
8 |
9 | export default transformChart;
10 |
--------------------------------------------------------------------------------
/lib/func/transformChart.test.js:
--------------------------------------------------------------------------------
1 | import transformChart from './transformChart';
2 |
3 | describe('transformChart', () => {
4 | const chartWithId = {
5 | data: {},
6 | id: 1
7 | };
8 |
9 | const chartNoId = {
10 | data: {}
11 | };
12 |
13 | it('should not add chart id', () => {
14 | const transformedChart = transformChart(chartWithId);
15 | expect(transformedChart).toEqual(chartWithId);
16 | });
17 |
18 | it('should add chart id', () => {
19 | const transformedChart = transformChart(chartNoId);
20 | expect(transformedChart.id).toBeTruthy();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/lib/func/updateApiKey.js:
--------------------------------------------------------------------------------
1 | const updateAPIKey = (savedQueries, id, client) => {
2 | const uniqueSavedQueries = Array.from(new Set(savedQueries));
3 | const keyName = `public-dashboard: ${id}`;
4 |
5 | client
6 | .get({
7 | url: client.url('projectId', `keys?name=${keyName}`),
8 | api_key: client.masterKey()
9 | })
10 | .then(res => {
11 | client.post({
12 | url: client.url('projectId', 'keys', res[0].key),
13 | api_key: client.masterKey(),
14 | params: {
15 | name: keyName,
16 | is_active: true,
17 | permitted: ['saved_queries', 'cached_queries'],
18 | options: {
19 | saved_queries: {
20 | allowed: uniqueSavedQueries
21 | },
22 | cached_queries: {
23 | allowed: uniqueSavedQueries
24 | }
25 | }
26 | }
27 | });
28 | });
29 | };
30 |
31 | export default updateAPIKey;
32 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import { HashRouter as Router, Route } from 'react-router-dom';
6 | import { createStore, applyMiddleware, compose } from 'redux';
7 | import { Provider } from 'react-redux';
8 | import thunk from 'redux-thunk';
9 | import KeenAnalysis from 'keen-analysis';
10 | import rootReducer from './reducers/rootReducer';
11 | import onlyUIMiddleware from './middleware/onlyUIMiddleware';
12 | import 'keen-dataviz/dist/keen-dataviz.css';
13 | import '../styles/style.css';
14 | import { library } from '@fortawesome/fontawesome-svg-core';
15 | import { isEqual, omit } from 'lodash';
16 | import JavascriptTimeAgo from 'javascript-time-ago';
17 | import en from 'javascript-time-ago/locale/en';
18 |
19 | import {
20 | faTrashAlt,
21 | faChartArea,
22 | faChartBar,
23 | faChartLine,
24 | faChartPie,
25 | faPlusCircle,
26 | faSave,
27 | faSearch,
28 | faTable,
29 | faParagraph,
30 | faImage,
31 | faCog,
32 | faClone,
33 | faSpinner,
34 | faShareAlt,
35 | faBars,
36 | faTimes,
37 | faAngleDoubleRight,
38 | faEdit,
39 | faEye,
40 | faMobileAlt,
41 | faTabletAlt,
42 | faLaptop,
43 | faFilter,
44 | faExternalLinkAlt,
45 | faCode,
46 | faCopy,
47 | faFileDownload,
48 | faInfoCircle,
49 | faChevronRight
50 | } from '@fortawesome/free-solid-svg-icons';
51 |
52 | import 'keen-dataviz/dist/keen-dataviz.css';
53 |
54 | import KeenAnalysisContext from './contexts/keenAnalysis';
55 |
56 | import '../styles/style.css';
57 |
58 | import { saveDashboard } from './actions/rootActions';
59 |
60 | import createOnlyUIMiddleware from './middleware/onlyUIMiddleware';
61 | import { runDashboardObserver } from './utils/dashboardObserver';
62 |
63 | library.add(
64 | faTrashAlt,
65 | faChartArea,
66 | faChartBar,
67 | faChartLine,
68 | faChartPie,
69 | faPlusCircle,
70 | faSave,
71 | faSearch,
72 | faTable,
73 | faParagraph,
74 | faImage,
75 | faCog,
76 | faClone,
77 | faSpinner,
78 | faShareAlt,
79 | faBars,
80 | faTimes,
81 | faAngleDoubleRight,
82 | faEdit,
83 | faEye,
84 | faMobileAlt,
85 | faTabletAlt,
86 | faLaptop,
87 | faFilter,
88 | faExternalLinkAlt,
89 | faCode,
90 | faCopy,
91 | faFileDownload,
92 | faInfoCircle,
93 | faChevronRight
94 | );
95 |
96 | import MainBuilder from './builder/components/Main';
97 | import Editor from './builder/components/Editor';
98 | import MainViewer from './viewer/components/Main';
99 | import Viewer from './viewer/components/Editor';
100 |
101 | JavascriptTimeAgo.locale(en);
102 |
103 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
104 |
105 | export let keenGlobals = undefined;
106 | if (typeof webpackKeenGlobals !== 'undefined') {
107 | keenGlobals = webpackKeenGlobals;
108 | }
109 |
110 | export class DashboardBuilder {
111 | constructor(config) {
112 | const { keenAnalysis, onRouteChange } = config;
113 |
114 | const client =
115 | keenAnalysis.instance || new KeenAnalysis(keenAnalysis.config);
116 | const keenWebHost = config.keenWebHost || window.location.host;
117 | let keenWebFetchOptions;
118 |
119 | if (!!config.keenWebHost) {
120 | keenWebFetchOptions = {
121 | mode: 'cors',
122 | credentials: 'include'
123 | };
124 | }
125 |
126 | const store = createStore(
127 | rootReducer,
128 | composeEnhancers(
129 | applyMiddleware(
130 | createOnlyUIMiddleware(keenWebHost),
131 | thunk.withExtraArgument({
132 | keenClient: client,
133 | keenWebHost,
134 | keenWebFetchOptions
135 | })
136 | )
137 | )
138 | );
139 |
140 | runDashboardObserver(store);
141 |
142 | ReactDOM.render(
143 |
144 |
145 |
146 | {
149 | onRouteChange();
150 | return null;
151 | }}
152 | />
153 | (
156 |
157 | )}
158 | exact
159 | />{' '}
160 | }
163 | />
164 | (
167 |
168 | )}
169 | exact
170 | />
171 | }
174 | />
175 |
176 |
177 | ,
178 | document.querySelector(config.container)
179 | );
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/lib/middleware/onlyUIMiddleware.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-expressions */
2 |
3 | const createOnlyUIMiddleware = keenWebHost => () => next => action => {
4 | const actionList = [
5 | 'ADD_DASHBOARD_ITEM',
6 | 'DELETE_DASHBOARD_ITEM',
7 | 'LOAD_DASHBOARDS',
8 | 'LOAD_DASHBOARD_INFO',
9 | 'SAVE_DASHBOARD',
10 | 'HIDE_SAVED_DASHBOARD_MESSAGE'
11 | ];
12 | keenWebHost === 'none'
13 | ? !actionList.includes(action.type) && next(action)
14 | : next(action);
15 | };
16 |
17 | export default createOnlyUIMiddleware;
18 |
--------------------------------------------------------------------------------
/lib/reducers/defaultDashboardInfo.js:
--------------------------------------------------------------------------------
1 | const defaultDashboardInfo = {
2 | id: 1,
3 | title: 'My great dashboard',
4 | last_modified_date: 'Mon, 02 Dec 2019 15:16:38 GMT',
5 | data: {
6 | items: [
7 | {
8 | type: 'bar',
9 | top: 20,
10 | left: 20,
11 | width: 500,
12 | height: 300,
13 | palette: '',
14 | colors: {},
15 | sparkline: true,
16 | savedQuery: {}
17 | },
18 | {
19 | type: 'metric',
20 | top: 20,
21 | left: 560,
22 | width: 500,
23 | height: 300,
24 | palette: '',
25 | colors: {},
26 | sparkline: true,
27 | savedQuery: {}
28 | },
29 | {
30 | type: 'area',
31 | top: 340,
32 | left: 20,
33 | width: 500,
34 | height: 300,
35 | palette: '',
36 | colors: {},
37 | sparkline: true,
38 | savedQuery: {}
39 | },
40 | {
41 | type: 'line',
42 | top: 340,
43 | left: 560,
44 | width: 500,
45 | height: 300,
46 | palette: '',
47 | colors: {},
48 | sparkline: true,
49 | savedQuery: {}
50 | }
51 | ]
52 | }
53 | };
54 | export default defaultDashboardInfo;
55 |
--------------------------------------------------------------------------------
/lib/reducers/rootReducer.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | import { combineReducers } from 'redux';
4 | import * as actionTypes from '../actions/actionTypes';
5 | import sortDashboardList from '../func/sortDashboardList';
6 | import defaultDashboardInfo from './defaultDashboardInfo.js';
7 | import ReactTooltip from 'react-tooltip';
8 | import merge from 'lodash/merge';
9 | import transformChart from '../func/transformChart';
10 | import { generateUniqueId } from '../utils/generateUniqueId';
11 | import { APP_REDUCER } from '../constants';
12 |
13 | const defaultPalette = { value: '', label: 'Default' };
14 | export const defaultData = {
15 | dashboardInfo: {
16 | id: '',
17 | title: '',
18 | data: {
19 | version: 2,
20 | items: []
21 | },
22 | settings: {
23 | dryRun: false,
24 | theme: {},
25 | layout: [],
26 | items: [],
27 | charts_theme: {},
28 | fonts: [],
29 | savedQueriesList: []
30 | }
31 | },
32 | isLoading: false,
33 | loadingSettings: false,
34 | draggedType: '',
35 | toolbarVisible: false,
36 | grid: 20,
37 | dashboardSaved: false,
38 | settingsVisible: false,
39 | savedQueries: '',
40 | isDashboardListLoaded: false,
41 | dashboardList: [],
42 | dashboardsMenu: '',
43 | searchInput: '',
44 | accessKey: '',
45 | newDashboardId: '',
46 | screenSize: 'desktop',
47 | isDashboardLoading: false,
48 | dashboardMenuFilter: '',
49 | sortingValue: { value: 'az', label: 'A - Z' }
50 | };
51 |
52 | const appReducer = (state = defaultData, action) => {
53 | const { dashboardInfo, grid } = state;
54 | switch (action.type) {
55 | case actionTypes.LOAD_DASHBOARDS:
56 | return {
57 | ...state,
58 | isDashboardListLoaded: true,
59 | dashboardList: action.dashboardList.sort((a, b) => {
60 | if (a.title.toLowerCase() < b.title.toLowerCase()) {
61 | return -1;
62 | }
63 | if (a.title.toLowerCase() > b.title.toLowerCase()) {
64 | return 1;
65 | }
66 | return 0;
67 | })
68 | };
69 | case actionTypes.ADD_DASHBOARD_ITEM:
70 | const addedDashboard = merge(
71 | {},
72 | defaultData.dashboardInfo,
73 | action.dashboardInfo
74 | );
75 | return {
76 | ...state,
77 | dashboardInfo: addedDashboard,
78 | newDashboardId: action.dashboardInfo.id
79 | };
80 | case actionTypes.HANDLE_SEARCH:
81 | return {
82 | ...state,
83 | searchInput: action.value
84 | };
85 | case actionTypes.DELETE_DASHBOARD_ITEM:
86 | const dashboardList = state.dashboardList.filter(
87 | el => el.id !== action.id
88 | );
89 | return {
90 | ...state,
91 | dashboardList: dashboardList.sort((a, b) => {
92 | if (a.title.toLowerCase() < b.title.toLowerCase()) {
93 | return -1;
94 | }
95 | if (a.title.toLowerCase() > b.title.toLowerCase()) {
96 | return 1;
97 | }
98 | return 0;
99 | })
100 | };
101 | case actionTypes.LOAD_DASHBOARD_INFO:
102 | const loadedDashboard = merge(
103 | {},
104 | defaultData.dashboardInfo,
105 | action.dashboardInfo
106 | );
107 | const { items } = loadedDashboard.settings;
108 | const formattedItems =
109 | (items && items.map(item => transformChart(item))) || [];
110 | const newDashboardInfo = {
111 | ...loadedDashboard,
112 | settings: {
113 | ...loadedDashboard.settings,
114 | items: formattedItems
115 | }
116 | };
117 | return {
118 | ...state,
119 | isDashboardLoading: action.isDashboardLoading,
120 | dashboardInfo: newDashboardInfo
121 | };
122 | case actionTypes.UPDATE_DASHBOARD_INFO:
123 | return {
124 | ...state,
125 | dashboardInfo: action.dashboardInfo
126 | };
127 | case actionTypes.CLEAR_DASHBOARD_INFO:
128 | return {
129 | ...state,
130 | dashboardInfo: {
131 | id: '',
132 | title: '',
133 | data: { version: 2, items: [] },
134 | settings: {
135 | palette: defaultPalette,
136 | colors: [],
137 | picker: {},
138 | dryRun: false,
139 | theme: {},
140 | layout: [],
141 | items: [],
142 | savedQueriesList: []
143 | }
144 | },
145 | dashboardSaved: false,
146 | settingsVisible: false
147 | };
148 | case actionTypes.SET_LOADING:
149 | return {
150 | ...state,
151 | isLoading: action.index
152 | };
153 | case actionTypes.SELECT_SAVED_QUERY:
154 | return {
155 | ...state,
156 | dashboardInfo: {
157 | ...dashboardInfo,
158 | settings: {
159 | ...dashboardInfo.settings,
160 | items: [
161 | ...dashboardInfo.settings.items.map(el =>
162 | el.i === action.index
163 | ? {
164 | ...el,
165 | savedQuery: Array.isArray(action.savedQueries)
166 | ? action.savedQueries
167 | : [action.savedQueries],
168 | error: false
169 | }
170 | : el
171 | )
172 | ]
173 | }
174 | }
175 | };
176 | case actionTypes.CHANGE_CHART_TYPE:
177 | return {
178 | ...state,
179 | dashboardInfo: {
180 | ...dashboardInfo,
181 | settings: {
182 | ...dashboardInfo.settings,
183 | items: [
184 | ...dashboardInfo.settings.items.map(el =>
185 | action.index === el.i
186 | ? {
187 | ...el,
188 | type: action.value
189 | }
190 | : el
191 | )
192 | ]
193 | }
194 | },
195 | isLoading: false
196 | };
197 | case actionTypes.SAVED_QUERY_ERROR:
198 | return {
199 | ...state,
200 | dashboardInfo: {
201 | ...dashboardInfo,
202 | settings: {
203 | ...dashboardInfo.settings,
204 | items: dashboardInfo.settings.items.map(el =>
205 | action.index === el.i
206 | ? {
207 | ...el,
208 | error: action.error
209 | }
210 | : el
211 | )
212 | }
213 | }
214 | };
215 | case actionTypes.GET_SAVED_QUERIES:
216 | const { savedQueries } = action;
217 | return {
218 | ...state,
219 | savedQueries
220 | };
221 | case actionTypes.LOAD_SAVED_QUERIES:
222 | return {
223 | ...state,
224 | dashboardInfo: {
225 | ...dashboardInfo,
226 | settings: {
227 | ...dashboardInfo.settings,
228 | items: dashboardInfo.settings.items.map(el =>
229 | action.index === el.i
230 | ? {
231 | ...el,
232 | sparkline: el.sparkline ? el.sparkline : false
233 | }
234 | : el
235 | )
236 | }
237 | }
238 | };
239 | case actionTypes.LOAD_SAVED_ERROR:
240 | return {
241 | ...state,
242 | dashboardInfo: {
243 | ...dashboardInfo,
244 | settings: {
245 | ...dashboardInfo.settings,
246 | items: dashboardInfo.settings.items.map(el =>
247 | action.index === el.i
248 | ? {
249 | ...el,
250 | error: action.error
251 | }
252 | : el
253 | )
254 | }
255 | }
256 | };
257 | case actionTypes.SAVE_DASHBOARD:
258 | return {
259 | ...state,
260 | dashboardSaved: true
261 | };
262 | case actionTypes.HIDE_SAVED_DASHBOARD_MESSAGE:
263 | return {
264 | ...state,
265 | dashboardSaved: false
266 | };
267 | case actionTypes.CHANGE_DASHBOARD_TITLE:
268 | return {
269 | ...state,
270 | dashboardInfo: {
271 | ...dashboardInfo,
272 | title: action.title
273 | }
274 | };
275 | case actionTypes.TOGGLE_DRY_RUN:
276 | return {
277 | ...state,
278 | dashboardInfo: {
279 | ...dashboardInfo,
280 | settings: {
281 | ...dashboardInfo.settings,
282 | dryRun: !dashboardInfo.settings.dryRun
283 | },
284 | theme: {
285 | ...dashboardInfo.theme
286 | }
287 | }
288 | };
289 | case actionTypes.TOGGLE_IS_PUBLIC:
290 | return {
291 | ...state,
292 | dashboardInfo: {
293 | ...dashboardInfo,
294 | is_public: !dashboardInfo.is_public
295 | }
296 | };
297 | case actionTypes.SET_THEME:
298 | return {
299 | ...state,
300 | dashboardInfo: {
301 | ...dashboardInfo,
302 | settings: {
303 | ...dashboardInfo.settings,
304 | theme: action.value
305 | }
306 | }
307 | };
308 | case actionTypes.SET_CHART_THEME:
309 | const charts_theme = dashboardInfo.settings.charts_theme
310 | ? { ...dashboardInfo.settings.charts_theme }
311 | : {};
312 | charts_theme[action.index] = action.value;
313 | return {
314 | ...state,
315 | dashboardInfo: {
316 | ...dashboardInfo,
317 | settings: {
318 | ...dashboardInfo.settings,
319 | charts_theme
320 | }
321 | }
322 | };
323 | case actionTypes.SET_LAYOUT:
324 | return {
325 | ...state,
326 | dashboardInfo: {
327 | ...state.dashboardInfo,
328 | settings: {
329 | ...state.dashboardInfo.settings,
330 | layout: action.layout
331 | }
332 | }
333 | };
334 | case actionTypes.SHOW_TOOLBAR:
335 | return {
336 | ...state,
337 | toolbarVisible: true
338 | };
339 | case actionTypes.CLOSE_TOOLBAR:
340 | return {
341 | ...state,
342 | toolbarVisible: false
343 | };
344 | case actionTypes.DRAG_START_HANDLER:
345 | return {
346 | ...state,
347 | draggedType: action.draggedType,
348 | toolbarVisible: false
349 | };
350 | case actionTypes.DROP_HANDLER:
351 | return {
352 | ...state,
353 | dashboardInfo: {
354 | ...state.dashboardInfo,
355 | settings: {
356 | ...state.dashboardInfo.settings,
357 | items: [...state.dashboardInfo.settings.items, action.element]
358 | }
359 | },
360 | draggedType: '',
361 | settingsVisible: action.id
362 | };
363 | case actionTypes.DELETE_CHART:
364 | return {
365 | ...state,
366 | settingsVisible: false,
367 | dashboardInfo: {
368 | ...dashboardInfo,
369 | settings: {
370 | ...dashboardInfo.settings,
371 | items: dashboardInfo.settings.items.filter(
372 | item => item.i !== action.index
373 | )
374 | }
375 | }
376 | };
377 | case actionTypes.CLOSE_SETTINGS:
378 | return {
379 | ...state,
380 | settingsVisible: false
381 | };
382 | case actionTypes.SHOW_SETTINGS:
383 | return {
384 | ...state,
385 | settingsVisible: action.index
386 | };
387 | case actionTypes.SET_SRC_FOR_IMG:
388 | return {
389 | ...state,
390 | dashboardInfo: {
391 | ...dashboardInfo,
392 | settings: {
393 | ...dashboardInfo.settings,
394 | items: [
395 | ...dashboardInfo.settings.items.map(el =>
396 | action.index === el.i
397 | ? {
398 | ...el,
399 | src: action.value
400 | }
401 | : el
402 | )
403 | ]
404 | }
405 | }
406 | };
407 | case actionTypes.SET_TEXT_FOR_PARAGRAPH:
408 | return action.source === 'user'
409 | ? {
410 | ...state,
411 | dashboardInfo: {
412 | ...dashboardInfo,
413 | settings: {
414 | ...dashboardInfo.settings,
415 | items: dashboardInfo.settings.items.map(el =>
416 | action.index === el.i
417 | ? {
418 | ...el,
419 | text: action.newValue
420 | }
421 | : el
422 | )
423 | }
424 | }
425 | }
426 | : { ...state };
427 | case actionTypes.CLEAR_ITEMS: // can be removed - for development only
428 | return {
429 | ...state,
430 | dashboardInfo: {
431 | ...dashboardInfo,
432 | settings: {
433 | ...dashboardInfo.settings,
434 | items: []
435 | }
436 | }
437 | };
438 | case actionTypes.CLONE_CHART:
439 | const id = `chart-${generateUniqueId()}`;
440 | const clonedElement = dashboardInfo.settings.items.find(
441 | item => item.i === action.index
442 | );
443 | const clonedElementLayout = dashboardInfo.settings.layout.find(
444 | item => item.i === action.index
445 | );
446 | const theme =
447 | (dashboardInfo.settings.charts_theme &&
448 | dashboardInfo.settings.charts_theme[action.index]) ||
449 | {};
450 | let newTheme = {};
451 | if (theme && theme.style) {
452 | const regex = new RegExp(`${action.index}`, 'g');
453 | newTheme = {
454 | ...theme,
455 | style: theme.style.replace(regex, id)
456 | };
457 | }
458 | const { x, y, w, h } = clonedElementLayout;
459 | const { type, savedQuery, options, error, text, src } = clonedElement;
460 | const newElement = {
461 | i: id,
462 | x,
463 | y: h + y,
464 | w,
465 | h,
466 | type,
467 | savedQuery,
468 | options,
469 | error,
470 | text,
471 | src
472 | };
473 | return {
474 | ...state,
475 | dashboardInfo: {
476 | ...dashboardInfo,
477 | settings: {
478 | ...dashboardInfo.settings,
479 | items: [...dashboardInfo.settings.items, newElement],
480 | charts_theme: {
481 | ...dashboardInfo.settings.charts_theme,
482 | [id]: newTheme
483 | }
484 | }
485 | }
486 | };
487 | case actionTypes.TOGGLE_DASHBOARDS_MENU:
488 | return {
489 | ...state,
490 | dashboardsMenu: action.value || ''
491 | };
492 | case actionTypes.SET_NEW_DASHBOARD_FOR_FOCUS:
493 | return {
494 | ...state,
495 | newDashboardId: action.value
496 | };
497 | case actionTypes.SET_ACCESS_KEY:
498 | return {
499 | ...state,
500 | accessKey: action.value
501 | };
502 | case actionTypes.CLEAR_ACCESS_KEY:
503 | return {
504 | ...state,
505 | accessKey: ''
506 | };
507 | case actionTypes.MAP_OLD_ITEMS:
508 | return {
509 | ...state,
510 | dashboardInfo: action.newDashboard
511 | };
512 | case actionTypes.LOADING_SINGLE_DASHBOARD:
513 | return {
514 | ...state,
515 | isDashboardLoading: true
516 | };
517 |
518 | case actionTypes.FILTER_DASHBOARDS_MENU:
519 | return {
520 | ...state,
521 | dashboardMenuFilter: action.value
522 | };
523 |
524 | case actionTypes.CHANGE_SORTING:
525 | return {
526 | ...state,
527 | sortingValue: action.sorting,
528 | dashboardList: sortDashboardList(
529 | action.sorting.value,
530 | state.dashboardList
531 | )
532 | };
533 |
534 | case actionTypes.CHANGE_SAVED_QUERY_LIST:
535 | const { savedQueriesList } = action;
536 | return {
537 | ...state,
538 | dashboardInfo: {
539 | ...dashboardInfo,
540 | settings: {
541 | ...dashboardInfo.settings,
542 | savedQueriesList
543 | }
544 | }
545 | };
546 |
547 | case actionTypes.LOAD_DUMMY_DASHBOARDS:
548 | return {
549 | ...state,
550 | dashboardList: [defaultDashboardInfo],
551 | dashboardInfo: {
552 | ...state.dashboardInfo,
553 | ...defaultDashboardInfo
554 | }
555 | };
556 |
557 | default:
558 | return state;
559 | }
560 | };
561 |
562 | export default combineReducers({
563 | [APP_REDUCER]: appReducer
564 | });
565 |
--------------------------------------------------------------------------------
/lib/selectors/app.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash';
2 |
3 | import { APP_REDUCER } from '../constants';
4 |
5 | export const getSavedQueriesList = state =>
6 | get(
7 | state,
8 | [APP_REDUCER, 'dashboardInfo', 'settings', 'savedQueriesList'],
9 | []
10 | );
11 |
12 | export const getDashboardInfo = state =>
13 | get(state, [APP_REDUCER, 'dashboardInfo']);
14 |
15 | export const getMovingState = state => get(state, [APP_REDUCER, 'isMoving']);
16 |
17 | export const getResizingState = state =>
18 | get(state, [APP_REDUCER, 'isResizing']);
19 |
--------------------------------------------------------------------------------
/lib/selectors/editor.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keen/dashboard-builder/175bf19e4cb8e4e3447c2f6cdd8affd6701aa0ce/lib/selectors/editor.js
--------------------------------------------------------------------------------
/lib/utils/dashboardObserver.js:
--------------------------------------------------------------------------------
1 | import { isEqual, omit } from 'lodash';
2 |
3 | import { saveDashboard } from '../actions/rootActions';
4 | import {
5 | getDashboardInfo,
6 | getMovingState,
7 | getResizingState
8 | } from '../selectors/app';
9 |
10 | const LAST_MODIFIED_DATE = 'last_modified_date';
11 |
12 | export const transformState = state => omit(state, [LAST_MODIFIED_DATE]);
13 |
14 | export const runDashboardObserver = store => {
15 | let currentDashboardState = {};
16 | let debounceTimer = null;
17 |
18 | const dispose = store.subscribe(() => {
19 | const state = store.getState();
20 |
21 | const previousDashboardState = currentDashboardState;
22 | const nextDashboardState = getDashboardInfo(state);
23 |
24 | // TODO: Remove as migration to React-Grid-Layout will be done
25 | if (getMovingState(state) || getResizingState(state)) return;
26 |
27 | if (
28 | !isEqual(
29 | transformState(previousDashboardState),
30 | transformState(nextDashboardState)
31 | )
32 | ) {
33 | if (previousDashboardState.id === nextDashboardState.id) {
34 | if (debounceTimer) {
35 | clearTimeout(debounceTimer);
36 | }
37 | debounceTimer = setTimeout(() => {
38 | store.dispatch(saveDashboard(nextDashboardState));
39 | debounceTimer = null;
40 | }, 10000);
41 | }
42 | currentDashboardState = nextDashboardState;
43 | }
44 | });
45 |
46 | return dispose;
47 | };
48 |
--------------------------------------------------------------------------------
/lib/utils/dashboardObserver.test.js:
--------------------------------------------------------------------------------
1 | import configureStore from 'redux-mock-store';
2 | import thunk from 'redux-thunk';
3 |
4 | import { runDashboardObserver } from './dashboardObserver';
5 |
6 | import { APP_REDUCER } from '../constants';
7 |
8 | describe('runDashboardObserver()', () => {
9 | const mockStore = configureStore([
10 | thunk.withExtraArgument({
11 | keenClient: {
12 | projectId: () => ''
13 | }
14 | })
15 | ]);
16 | let store;
17 |
18 | jest.useFakeTimers();
19 |
20 | beforeEach(() => {
21 | store = mockStore({});
22 | });
23 |
24 | it('should apply listener to store', () => {
25 | const store = {
26 | subscribe: jest.fn()
27 | };
28 | runDashboardObserver(store);
29 |
30 | expect(store.subscribe).toHaveBeenCalled();
31 | });
32 |
33 | it('should not dispatch dashboard save action', () => {
34 | store = mockStore({
35 | [APP_REDUCER]: {
36 | isMoving: true,
37 | isResizing: false
38 | }
39 | });
40 |
41 | runDashboardObserver(store);
42 | store.dispatch({ type: '@keen/action' });
43 |
44 | expect(store.getActions()).toMatchInlineSnapshot(`
45 | Array [
46 | Object {
47 | "type": "@keen/action",
48 | },
49 | ]
50 | `);
51 | });
52 |
53 | it('should not dispatch save action for same dashboard state', () => {
54 | store = mockStore({
55 | [APP_REDUCER]: {
56 | isMoving: true,
57 | isResizing: false
58 | }
59 | });
60 |
61 | runDashboardObserver(store);
62 | store.dispatch({ type: '@keen/action' });
63 | store.dispatch({ type: '@keen/action' });
64 |
65 | expect(store.getActions()).toMatchInlineSnapshot(`
66 | Array [
67 | Object {
68 | "type": "@keen/action",
69 | },
70 | Object {
71 | "type": "@keen/action",
72 | },
73 | ]
74 | `);
75 | });
76 |
77 | it('should dispatch save dashboard action', () => {
78 | const initialState = {
79 | [APP_REDUCER]: {
80 | isMoving: false,
81 | isResizing: false,
82 | dashboardInfo: {
83 | id: 1,
84 | value: 0
85 | }
86 | }
87 | };
88 |
89 | store = mockStore(actions => {
90 | if (actions.length === 1) {
91 | return {
92 | ...initialState,
93 | [APP_REDUCER]: {
94 | ...initialState[APP_REDUCER],
95 | dashboardInfo: {
96 | id: 1,
97 | value: 1
98 | }
99 | }
100 | };
101 | }
102 |
103 | return initialState;
104 | });
105 |
106 | runDashboardObserver(store);
107 |
108 | store.dispatch({ type: '@keen/action' });
109 | store.dispatch({ type: '@keen/action' });
110 |
111 | jest.runAllTimers();
112 |
113 | expect(store.getActions()).toMatchInlineSnapshot(`
114 | Array [
115 | Object {
116 | "type": "@keen/action",
117 | },
118 | Object {
119 | "type": "@keen/action",
120 | },
121 | Object {
122 | "type": "SAVE_DASHBOARD",
123 | },
124 | ]
125 | `);
126 | });
127 | });
128 |
--------------------------------------------------------------------------------
/lib/utils/generateUniqueId.js:
--------------------------------------------------------------------------------
1 | import { uuid } from 'uuidv4';
2 |
3 | export const generateUniqueId = () => {
4 | return uuid();
5 | };
6 |
--------------------------------------------------------------------------------
/lib/utils/generateUniqueId.test.js:
--------------------------------------------------------------------------------
1 | import { generateUniqueId } from './generateUniqueId';
2 |
3 | describe('generateUniqueId', () => {
4 | it('should return Id as string', () => {
5 | const id = generateUniqueId();
6 | expect(typeof id).toEqual('string');
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/lib/utils/loadFontsFromDashboard.js:
--------------------------------------------------------------------------------
1 | import WebFont from 'webfontloader';
2 |
3 | const getFontsList = dashboard => {
4 | const fonts = new Set();
5 | const { settings } = dashboard;
6 | if (settings.charts_theme && Object.keys(settings.charts_theme).length) {
7 | Object.keys(settings.charts_theme).forEach(item => {
8 | const theme =
9 | (settings.charts_theme[item] && settings.charts_theme[item].theme) ||
10 | {};
11 | Object.keys(theme).reduce((acc, key) => {
12 | if (key.includes('font_family') && theme[key]) {
13 | acc.add(theme[key]);
14 | }
15 | return acc;
16 | }, fonts);
17 | });
18 | }
19 |
20 | if (settings.theme && settings.theme.theme) {
21 | const { theme } = settings.theme;
22 | Object.keys(theme).reduce((acc, key) => {
23 | if (key.includes('font_family') && theme[key]) {
24 | acc.add(theme[key]);
25 | }
26 | return acc;
27 | }, fonts);
28 | }
29 | return Array.from(fonts);
30 | };
31 |
32 | const loadFontsFromDashboard = dashboard => {
33 | const fonts = getFontsList(dashboard);
34 | if (fonts.length) {
35 | WebFont.load({
36 | google: {
37 | families: fonts
38 | }
39 | });
40 | }
41 | };
42 |
43 | export default loadFontsFromDashboard;
44 |
--------------------------------------------------------------------------------
/lib/utils/loadFontsFromDashboard.test.js:
--------------------------------------------------------------------------------
1 | import WebFont from 'webfontloader';
2 | import loadFontsFromDashboard from './loadFontsFromDashboard';
3 |
4 | jest.mock('webfontloader', () => ({
5 | load: jest.fn()
6 | }));
7 |
8 | describe('loadFontsFromDashboard', () => {
9 | const dashboardWithDifferentFonts = {
10 | settings: {
11 | someProp: 1,
12 | charts_theme: {
13 | chart1: {
14 | options: {},
15 | theme: {
16 | background: 'yellow',
17 | base_font_family: 'Arial',
18 | base_font_size: '16px',
19 | title_font_family: 'Helvetica',
20 | title_font_size: '24px'
21 | }
22 | }
23 | },
24 | theme: {
25 | options: {},
26 | theme: {
27 | background: 'red',
28 | base_font_family: 'Times New Roman',
29 | base_font_size: '14px'
30 | }
31 | }
32 | }
33 | };
34 |
35 | const dashboardWithDuplicatedFonts = {
36 | settings: {
37 | someProp: 1,
38 | charts_theme: {
39 | chart1: {
40 | options: {},
41 | theme: {
42 | background: 'yellow',
43 | base_font_family: 'Arial',
44 | base_font_size: '16px',
45 | title_font_family: 'Arial',
46 | title_font_size: '24px'
47 | }
48 | }
49 | },
50 | theme: {
51 | options: {},
52 | theme: {
53 | background: 'red',
54 | base_font_family: 'Arial',
55 | base_font_size: '14px'
56 | }
57 | }
58 | }
59 | };
60 |
61 | it('should return a list of fonts', () => {
62 | loadFontsFromDashboard(dashboardWithDifferentFonts);
63 | expect(WebFont.load).toHaveBeenCalledWith({
64 | google: { families: ['Arial', 'Helvetica', 'Times New Roman'] }
65 | });
66 | });
67 |
68 | it('should not return duplicated fonts', () => {
69 | loadFontsFromDashboard(dashboardWithDuplicatedFonts);
70 | expect(WebFont.load).toHaveBeenCalledWith({
71 | google: { families: ['Arial'] }
72 | });
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/lib/viewer/components/Editor.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import isEmpty from 'lodash/isEmpty';
4 | import {
5 | loadDashboardInfo,
6 | updateDashboardInfo,
7 | clearDashboardInfo,
8 | loadDashboards
9 | } from '../../actions/rootActions';
10 | import EditorContainer from './EditorContainer';
11 |
12 | class Editor extends Component {
13 | constructor(props) {
14 | super(props);
15 | }
16 |
17 | componentDidMount() {
18 | const {
19 | dashboardInfo,
20 | loadDashboardInfo,
21 | updateDashboardInfo,
22 | loadDashboards,
23 | match
24 | } = this.props;
25 | const { id } = match.params;
26 | if (dashboardInfo && !isEmpty(dashboardInfo)) {
27 | updateDashboardInfo(dashboardInfo);
28 | } else {
29 | loadDashboardInfo(id);
30 | loadDashboards();
31 | }
32 | }
33 |
34 | componentWillUnmount() {
35 | this.props.keenWebHost !== 'none' && this.props.clearDashboardInfo();
36 | }
37 |
38 | render() {
39 | const { isDashboardPublic } = this.props;
40 | return (
41 |
42 |
46 |
47 | );
48 | }
49 | }
50 |
51 | const mapDispatchToProps = {
52 | loadDashboardInfo,
53 | updateDashboardInfo,
54 | clearDashboardInfo,
55 | loadDashboards
56 | };
57 |
58 | export default connect(
59 | null,
60 | mapDispatchToProps
61 | )(Editor);
62 |
--------------------------------------------------------------------------------
/lib/viewer/components/EditorContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { closeSettings } from '../../actions/rootActions';
4 | import EditorDashboardTopBar from '../../builder/components/EditorDashboardTopBar';
5 | import EditorTopToolbar from './EditorTopToolbar';
6 | import ShareDashboard from '../../builder/components/ShareDashboard';
7 | import EmbedDashboard from '../../builder/components/EmbedDashboard';
8 | import EditorDashboard from './EditorDashboard';
9 |
10 | import KeenAnalysisContext from '../../contexts/keenAnalysis';
11 |
12 | const EditorContainer = props => {
13 | const { version, dashboardsMenu, isDashboardPublic } = props;
14 | return (
15 |
23 | {dashboardsMenu === 'share' &&
}
24 | {dashboardsMenu === 'embed' &&
}
25 |
26 |
27 |
31 |
32 | {client => (
33 |
38 | )}
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | const mapStateTopProps = state => {
46 | const { screenSize, dashboardsMenu } = state.app;
47 | return {
48 | screenSize,
49 | dashboardsMenu
50 | };
51 | };
52 |
53 | const mapDispatchToProps = {
54 | closeSettings
55 | };
56 |
57 | export default connect(
58 | mapStateTopProps,
59 | mapDispatchToProps
60 | )(EditorContainer);
61 |
--------------------------------------------------------------------------------
/lib/viewer/components/EditorDashboard.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import React, { PureComponent } from 'react';
3 | import { connect } from 'react-redux';
4 | import { WidthProvider, Responsive } from 'react-grid-layout';
5 | import {
6 | dropHandler,
7 | showSettings,
8 | mapOldItems,
9 | setLayout,
10 | loadSavedQuery,
11 | savedQueryError,
12 | setLoading,
13 | clearDashboardInfo
14 | } from '../../actions/rootActions';
15 | import { generateUniqueId } from '../../utils/generateUniqueId';
16 | import Paragraph from '../../builder/components/Paragraph';
17 | import Image from '../../builder/components/Image';
18 | import ChartContainer from '../../builder/components/ChartContainer';
19 | import Buttons from '../../builder/components/Buttons';
20 | import WebFont from 'webfontloader';
21 | import ExplorerButton from './ExplorerButton';
22 | import KeenAnalysisContext from '../../contexts/keenAnalysis';
23 | import { getStyles } from 'keen-theme-builder';
24 | import css from 'styled-jsx/css';
25 |
26 | import '../../../node_modules/react-grid-layout/css/styles.css';
27 | import '../../../node_modules/react-resizable/css/styles.css';
28 |
29 | const ResponsiveReactGridLayout = WidthProvider(Responsive);
30 |
31 | class EditorDashboard extends PureComponent {
32 | constructor(props) {
33 | super(props);
34 |
35 | this.state = {
36 | editedWidget: false,
37 | loading: false,
38 | results: undefined
39 | };
40 | }
41 |
42 | componentDidUpdate(prevProps) {
43 | const { fonts: families } = this.props;
44 | if (families && families.length) {
45 | WebFont.load({
46 | google: {
47 | families
48 | }
49 | });
50 | }
51 | }
52 |
53 | componentWillUnmount() {
54 | this.props.clearDashboardInfo();
55 | }
56 |
57 | onElementClick = (e, i) => {
58 | const { version, settingsVisible, showSettings } = this.props;
59 | if (version === 'viewer') return;
60 | if (settingsVisible !== i) {
61 | showSettings(i);
62 | }
63 | };
64 |
65 | createElement = el => {
66 | const { i, w, h, type, savedQuery, src, text } = el;
67 | if (i === undefined || i === null) return;
68 | const {
69 | id,
70 | theme,
71 | charts_theme,
72 | version,
73 | layout,
74 | error,
75 | dryRun,
76 | settingsVisible,
77 | keenAnalysis
78 | } = this.props;
79 | const options =
80 | (charts_theme && charts_theme[i] && charts_theme[i].options) ||
81 | (theme && theme.options) ||
82 | {};
83 | const layoutEl = layout.find(item => item.i === el.i) || {};
84 | const dataGrid = { ...el, ...layoutEl };
85 | let opacity = 1;
86 | if (settingsVisible && settingsVisible !== i) {
87 | opacity = 0.3;
88 | }
89 | return (
90 | this.onElementClick(e, i)}
96 | onMouseDown={this.onMouseDown}
97 | >
98 | {type === 'image' ? (
99 |
100 | ) : type === 'paragraph' ? (
101 |
102 | ) : (
103 |
113 | )}
114 | {version === 'editor' && (
115 |
121 | )}
122 |
123 | );
124 | };
125 |
126 | onBreakpointChange = (breakpoint, cols) => {
127 | this.setState({
128 | breakpoint: breakpoint,
129 | cols: cols
130 | });
131 | };
132 |
133 | onLayoutChange = (layout, breakpoints) => {
134 | const { version, setLayout } = this.props;
135 | if (version === 'editor') {
136 | setLayout(layout);
137 | }
138 | };
139 |
140 | onDrop = elemParams => {
141 | const { x, y, w, h } = elemParams;
142 | const { draggedType, dropHandler } = this.props;
143 | const id = `chart-${generateUniqueId()}`;
144 | const newElement = {
145 | i: id,
146 | x,
147 | y,
148 | w,
149 | h,
150 | type: draggedType,
151 | savedQuery: [],
152 | options: {},
153 | src: '',
154 | text: '',
155 | error: false
156 | };
157 | dropHandler(newElement, id);
158 | };
159 |
160 | onDragStart = (layout, oldItem, newItem, placeholder, event, element) => {
161 | this.setState({ editedWidget: oldItem.i });
162 | };
163 |
164 | onDragStop = () => {
165 | this.setState({ editedWidget: false });
166 | };
167 |
168 | onResizeStart = (layout, oldItem, newItem, placeholder, event, element) => {
169 | this.setState({ editedWidget: oldItem.i });
170 | };
171 |
172 | onResizeStop = () => {
173 | this.setState({ editedWidget: false });
174 | };
175 |
176 | render() {
177 | const {
178 | version,
179 | screenSize,
180 | id,
181 | items,
182 | layout,
183 | layouts,
184 | theme,
185 | ...options
186 | } = this.props;
187 | const dashboardId = `dashboard-${id}`;
188 | return (
189 |
190 | {theme && theme.theme && (
191 |
196 | )}
197 |
217 | {items && items.map(item => this.createElement(item))}
218 |
219 |
220 | );
221 | }
222 | }
223 |
224 | const mapStateToProps = state => {
225 | const {
226 | dashboardInfo: {
227 | id,
228 | settings: {
229 | theme,
230 | charts_theme,
231 | layout,
232 | layouts,
233 | items,
234 | savedQuery,
235 | fonts
236 | }
237 | },
238 | dashboardInfo,
239 | draggedType,
240 | settingsVisible,
241 | screenSize
242 | } = state.app;
243 | return {
244 | id,
245 | dashboardInfo,
246 | draggedType,
247 | theme,
248 | charts_theme,
249 | layout,
250 | layouts,
251 | items,
252 | savedQuery,
253 | fonts,
254 | settingsVisible,
255 | screenSize
256 | };
257 | };
258 |
259 | const mapDispatchToProps = {
260 | dropHandler,
261 | showSettings,
262 | mapOldItems,
263 | setLayout,
264 | loadSavedQuery,
265 | savedQueryError,
266 | setLoading,
267 | clearDashboardInfo
268 | };
269 |
270 | export default connect(
271 | mapStateToProps,
272 | mapDispatchToProps
273 | )(EditorDashboard);
274 |
275 | EditorDashboard.defaultProps = {
276 | cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 },
277 | rowHeight: 100
278 | };
279 |
--------------------------------------------------------------------------------
/lib/viewer/components/EditorTopToolbar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import EditorTopToolbarTitle from './EditorTopToolbarTitle';
3 |
4 | const EditorTopToolbar = props => {
5 | const { version, isDashboardPublic } = props;
6 | return (
7 |
8 |
13 |
14 | );
15 | };
16 | export default EditorTopToolbar;
17 |
--------------------------------------------------------------------------------
/lib/viewer/components/EditorTopToolbarTitle.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Link } from 'react-router-dom';
4 | import {
5 | changeDashboardTitle,
6 | toggleDashboardsMenu,
7 | setNewDashboardForFocus
8 | } from '../../actions/rootActions';
9 | import SwitchDashboard from './SwitchDashboard';
10 | import EditorDashboardsSwitch from '../../builder/components/EditorDashboardsSwitch';
11 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
12 |
13 | class EditorTopToolbarTitle extends Component {
14 | constructor(props) {
15 | super(props);
16 | this.title = React.createRef();
17 | }
18 |
19 | componentDidMount() {
20 | if (this.props.version === 'editor') {
21 | if (this.props.id === this.props.newDashboardId) {
22 | this.handleFocus();
23 | this.props.setNewDashboardForFocus(false);
24 | }
25 | }
26 | }
27 |
28 | componentDidUpdate() {
29 | if (this.props.version === 'editor') {
30 | if (this.props.id === this.props.newDashboardId) {
31 | this.handleFocus();
32 | this.props.setNewDashboardForFocus(false);
33 | }
34 | }
35 | }
36 |
37 | handleFocus = () => {
38 | this.title.current.focus();
39 | this.title.current.select();
40 | };
41 |
42 | handleClick = () => {
43 | this.props.toggleDashboardsMenu('dashboard');
44 | };
45 |
46 | renderSwitcher() {
47 | const { title, switcherEnabled } = this.props;
48 | return switcherEnabled ? (
49 |
50 | ) : (
51 | {title}
52 | );
53 | }
54 |
55 | render() {
56 | const { id, version, title, dashboardsMenu, editable } = this.props;
57 | return (
58 |
59 | {version === 'editor' ? (
60 |
61 |
62 |
67 | {dashboardsMenu === 'dashboard' && }
68 |
69 | this.props.changeDashboardTitle(e.target.value)}
74 | placeholder="Enter your dashboard title..."
75 | />
76 |
77 | ) : (
78 |
79 | {this.renderSwitcher()}
80 | {editable && (
81 |
82 |
86 | Edit
87 |
88 |
89 | )}
90 |
91 | )}
92 |
93 | );
94 | }
95 | }
96 |
97 | const mapStateToProps = state => {
98 | const {
99 | dashboardInfo: { id, title },
100 | dashboardsMenu,
101 | newDashboardId
102 | } = state.app;
103 | return {
104 | id,
105 | title,
106 | dashboardsMenu,
107 | newDashboardId
108 | };
109 | };
110 |
111 | const mapDispatchToProps = {
112 | changeDashboardTitle,
113 | toggleDashboardsMenu,
114 | setNewDashboardForFocus
115 | };
116 |
117 | export default connect(
118 | mapStateToProps,
119 | mapDispatchToProps
120 | )(EditorTopToolbarTitle);
121 |
122 | EditorTopToolbarTitle.defaultProps = {
123 | switcherEnabled: true,
124 | editable: true
125 | };
126 |
--------------------------------------------------------------------------------
/lib/viewer/components/ExplorerButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import ReactTooltip from 'react-tooltip';
4 |
5 | const ExplorerButton = ({ savedQuery }) => {
6 | const { origin, pathname } = window.location;
7 | const openNewTab = () => {
8 | savedQuery.forEach(element => {
9 | window.open(
10 | `${origin}${pathname}explorer?saved_query=${element.value}`,
11 | '_blank'
12 | );
13 | });
14 | };
15 | return (
16 |
17 |
24 | dataTip}
30 | />
31 |
32 | );
33 | };
34 |
35 | export default ExplorerButton;
36 |
--------------------------------------------------------------------------------
/lib/viewer/components/Main.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { connect } from 'react-redux';
3 | import { loadDashboards } from '../../actions/rootActions';
4 | import MainContainer from './MainContainer';
5 |
6 | const Main = props => {
7 | useEffect(() => {
8 | props.loadDashboards();
9 | }, []);
10 | return ;
11 | };
12 |
13 | const mapDispatchToProps = {
14 | loadDashboards
15 | };
16 |
17 | export default connect(
18 | null,
19 | mapDispatchToProps
20 | )(Main);
21 |
--------------------------------------------------------------------------------
/lib/viewer/components/MainContainer.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { connect } from 'react-redux';
3 | import MainTopToolbar from './MainTopToolbar';
4 | import MainListItem from './MainListItem';
5 | import {
6 | addDashboardItem,
7 | loadDummyDashboards
8 | } from '../../actions/rootActions';
9 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
10 | import isEqual from 'lodash/isEqual';
11 |
12 | const MainContainer = ({
13 | dashboardList,
14 | isDashboardListLoaded,
15 | searchInput,
16 | version,
17 | addDashboardItem,
18 | keenWebHost,
19 | loadDummyDashboards
20 | }) => {
21 | useEffect(() => {
22 | if (keenWebHost === 'none') {
23 | loadDummyDashboards();
24 | }
25 | }, []);
26 | let list = dashboardList.filter(
27 | el =>
28 | searchInput === '' ||
29 | (el.title && el.title.toLowerCase().includes(searchInput.toLowerCase()))
30 | );
31 |
32 | useEffect(() => {
33 | if (isDashboardListLoaded && !list.length) {
34 | addDashboardItem('My first dashboard');
35 | }
36 | if (!isEqual(dashboardList, list)) {
37 | list = dashboardList.filter(
38 | el =>
39 | searchInput === '' ||
40 | (el.title &&
41 | el.title.toLowerCase().includes(searchInput.toLowerCase()))
42 | );
43 | }
44 | });
45 |
46 | return (
47 |
48 |
49 | {list.length ? (
50 | list.map(({ title, id, last_modified_date, is_public }, i) => {
51 | return (
52 |
61 | );
62 | })
63 | ) : (
64 |
No dashboards found...
65 | )}
66 | {!isDashboardListLoaded && keenWebHost !== 'none' && (
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | )}
76 |
77 | );
78 | };
79 |
80 | const mapDispatchToProps = {
81 | addDashboardItem,
82 | loadDummyDashboards
83 | };
84 |
85 | const mapStatetoProps = state => {
86 | const {
87 | dashboardList,
88 | isDashboardListLoaded,
89 | searchInput,
90 | sortingValue
91 | } = state.app;
92 |
93 | return {
94 | dashboardList,
95 | isDashboardListLoaded,
96 | searchInput,
97 | sortingValue
98 | };
99 | };
100 |
101 | export default connect(
102 | mapStatetoProps,
103 | mapDispatchToProps
104 | )(MainContainer);
105 |
--------------------------------------------------------------------------------
/lib/viewer/components/MainListItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import MainListItemButtons from './MainListItemButtons';
4 | import ReactTimeAgo from 'react-time-ago';
5 |
6 | const MainListItem = ({
7 | title,
8 | id,
9 | version,
10 | last_modified_date,
11 | is_public
12 | }) => {
13 | const link = `/viewer/${id}`;
14 | return (
15 |
16 |
17 |
18 | {title}
19 |
20 |
21 |
22 | {version === 'editor' && (
23 |
24 | )}
25 |
26 | );
27 | };
28 |
29 | export default MainListItem;
30 |
--------------------------------------------------------------------------------
/lib/viewer/components/MainListItemButtons.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { deleteDashboardItem } from '../../actions/rootActions';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 | import ReactTooltip from 'react-tooltip';
6 |
7 | const MainListItemButtons = ({ deleteDashboardItem, id, is_public }) => {
8 | const handleIconClick = () => {
9 | const approvalDelDash = confirm('Do You want to delete this dashboard?');
10 | if (approvalDelDash) {
11 | ReactTooltip.hide();
12 | deleteDashboardItem(id, is_public);
13 | }
14 | };
15 |
16 | return (
17 |
22 |
handleIconClick()}>
23 |
24 |
25 |
dataTip}
31 | />
32 |
33 | );
34 | };
35 |
36 | const mapDispatchToProps = {
37 | deleteDashboardItem
38 | };
39 |
40 | export default connect(
41 | null,
42 | mapDispatchToProps
43 | )(MainListItemButtons);
44 |
--------------------------------------------------------------------------------
/lib/viewer/components/MainTopToolbar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { handleSearch, changeSorting } from '../../actions/rootActions';
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 | import NewDashboardButton from '../../builder/components/NewDashboardButton';
6 | import Select from 'react-select';
7 |
8 | const MainTopToolbar = props => {
9 | const { version, sortingValue } = props;
10 |
11 | const sortOptions = [
12 | { value: 'az', label: 'A - Z' },
13 | { value: 'za', label: 'Z - A' },
14 | { value: 'latest', label: 'Latest first' },
15 | { value: 'oldest', label: 'Oldest first' }
16 | ];
17 |
18 | return (
19 |
20 | {version === 'editor' &&
}
21 |
22 |
23 | props.handleSearch(e.target.value)}
27 | />
28 |
29 |
30 |
36 |
37 | );
38 | };
39 |
40 | const mapStateToProps = state => {
41 | const { sortingValue } = state.app;
42 | return {
43 | sortingValue
44 | };
45 | };
46 |
47 | const mapDispatchToProps = {
48 | handleSearch,
49 | changeSorting
50 | };
51 |
52 | export default connect(
53 | mapStateToProps,
54 | mapDispatchToProps
55 | )(MainTopToolbar);
56 |
--------------------------------------------------------------------------------
/lib/viewer/components/SwitchDashboard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { withRouter } from 'react-router-dom';
4 | import { loadDashboardInfo } from '../../actions/rootActions';
5 | import Select from 'react-select';
6 |
7 | const SwitchDashboard = props => {
8 | const dashboardOptions = props.dashboardList.map(el => ({
9 | value: el.id,
10 | label: el.title
11 | }));
12 |
13 | const changeDashboardView = id => {
14 | props.history.push(id);
15 | props.loadDashboardInfo(id);
16 | };
17 |
18 | return (
19 |
20 |
26 | );
27 | };
28 |
29 | const mapStateToProps = state => {
30 | const {
31 | dashboardInfo: { id, title },
32 | dashboardList
33 | } = state.app;
34 | return {
35 | id,
36 | title,
37 | dashboardList
38 | };
39 | };
40 |
41 | const mapDispatchToProps = {
42 | loadDashboardInfo
43 | };
44 |
45 | export default withRouter(
46 | connect(
47 | mapStateToProps,
48 | mapDispatchToProps
49 | )(SwitchDashboard)
50 | );
51 |
--------------------------------------------------------------------------------
/lib/viewer/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import { HashRouter as Router, Route } from 'react-router-dom';
6 | import { createStore, applyMiddleware, compose } from 'redux';
7 | import { Provider } from 'react-redux';
8 | import thunk from 'redux-thunk';
9 | import PropTypes from 'prop-types';
10 | import isEmpty from 'lodash/isEmpty';
11 | import KeenAnalysis from 'keen-analysis';
12 | import rootReducer from '../reducers/rootReducer';
13 | import Main from './components/Main';
14 | import Editor from './components/Editor';
15 | import 'keen-dataviz/dist/keen-dataviz.css';
16 | import '../../styles/style.css';
17 | import { library } from '@fortawesome/fontawesome-svg-core';
18 | import {
19 | faParagraph,
20 | faImage,
21 | faSearch,
22 | faSpinner,
23 | faEdit,
24 | faMobileAlt,
25 | faTabletAlt,
26 | faLaptop,
27 | faExternalLinkAlt,
28 | faFileDownload,
29 | faArrowsAltH,
30 | faTimes
31 | } from '@fortawesome/free-solid-svg-icons';
32 |
33 | library.add(
34 | faParagraph,
35 | faImage,
36 | faSearch,
37 | faSpinner,
38 | faEdit,
39 | faMobileAlt,
40 | faTabletAlt,
41 | faLaptop,
42 | faExternalLinkAlt,
43 | faFileDownload,
44 | faArrowsAltH,
45 | faTimes
46 | );
47 | import JavascriptTimeAgo from 'javascript-time-ago';
48 | import en from 'javascript-time-ago/locale/en';
49 |
50 | import KeenAnalysisContext from '../contexts/keenAnalysis';
51 |
52 | JavascriptTimeAgo.locale(en);
53 |
54 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
55 |
56 | export let keenGlobals = undefined;
57 | if (typeof webpackKeenGlobals !== 'undefined') {
58 | keenGlobals = webpackKeenGlobals;
59 | }
60 |
61 | export class DashboardViewer {
62 | constructor(props) {
63 | const { keenAnalysis, dashboardInfo } = props;
64 | const shouldRenderViewer = dashboardInfo && !isEmpty(dashboardInfo);
65 | const client =
66 | keenAnalysis.instance || new KeenAnalysis(keenAnalysis.config);
67 | const keenWebHost = props.keenWebHost || window.location.host;
68 | let keenWebFetchOptions;
69 |
70 | if (!!props.keenWebHost) {
71 | keenWebFetchOptions = {
72 | mode: 'cors',
73 | credentials: 'include'
74 | };
75 | }
76 |
77 | const store = createStore(
78 | rootReducer,
79 | composeEnhancers(
80 | applyMiddleware(
81 | thunk.withExtraArgument({
82 | keenClient: client,
83 | keenWebHost,
84 | keenWebFetchOptions
85 | })
86 | )
87 | )
88 | );
89 |
90 | ReactDOM.render(
91 |
92 |
93 |
94 | {shouldRenderViewer ? (
95 | }
98 | exact
99 | />
100 | ) : (
101 |
102 | )}
103 | }
106 | />
107 |
108 |
109 | ,
110 | document.querySelector(props.container)
111 | );
112 | }
113 | }
114 |
115 | DashboardViewer.propTypes = {
116 | dashboardInfo: PropTypes.shape({
117 | created_date: PropTypes.string,
118 | data: PropTypes.shape({
119 | version: PropTypes.number,
120 | items: PropTypes.arrayOf(
121 | PropTypes.shape({
122 | height: PropTypes.number,
123 | width: PropTypes.number,
124 | top: PropTypes.number,
125 | left: PropTypes.number,
126 | colors: PropTypes.array,
127 | palette: PropTypes.string,
128 | picker: PropTypes.object,
129 | legend: PropTypes.shape({
130 | value: PropTypes.string,
131 | label: PropTypes.string
132 | }),
133 | sparkline: PropTypes.shape({
134 | value: PropTypes.bool,
135 | label: PropTypes.string
136 | }),
137 | stacking: PropTypes.shape({
138 | value: PropTypes.string,
139 | label: PropTypes.string
140 | }),
141 | savedQuery: PropTypes.shape({
142 | value: PropTypes.string,
143 | label: PropTypes.string
144 | })
145 | })
146 | )
147 | }),
148 | id: PropTypes.string,
149 | is_public: PropTypes.bool,
150 | last_modified_date: PropTypes.string,
151 | project_id: PropTypes.string,
152 | rows: PropTypes.arrayOf(
153 | PropTypes.shape({
154 | height: PropTypes.number,
155 | tiles: PropTypes.arrayOf(
156 | PropTypes.shape({
157 | column_width: PropTypes.number,
158 | query_name: PropTypes.string
159 | })
160 | )
161 | })
162 | ),
163 | settings: PropTypes.shape({
164 | dryRun: PropTypes.bool,
165 | is_public: PropTypes.bool,
166 | colors: PropTypes.array,
167 | palette: PropTypes.string,
168 | picker: PropTypes.object
169 | }),
170 | title: PropTypes.string
171 | })
172 | };
173 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "keen-dashboard-builder",
3 | "description": "Dashboard builder for Keen.io",
4 | "license": "MIT",
5 | "version": "2.0.9",
6 | "main": "dist/main.min.js",
7 | "scripts": {
8 | "start": "concurrently --kill-others \"NODE_ENV=development webpack-dev-server\"",
9 | "build": "npm run build:viewer && npm run build:builder && npm run build:css && npm run build:css:min",
10 | "build:builder": "NODE_ENV=production OPTIMIZE_MINIMIZE=1 component=builder webpack -p",
11 | "build:viewer": "NODE_ENV=production OPTIMIZE_MINIMIZE=1 component=viewer webpack -p",
12 | "build:css": "node_modules/postcss-cli/bin/postcss styles/style.css -o dist/style.css --config postcss.config.js",
13 | "build:css:min": "OPTIMIZE_MINIMIZE=1 node_modules/postcss-cli/bin/postcss styles/style.css -o dist/style.min.css --config postcss.config.js",
14 | "build:gh-pages": "NODE_ENV=development webpack --mode development",
15 | "lint": "eslint lib/",
16 | "prettier": "prettier --write 'lib/**/*.{js,jsx,json}'",
17 | "version": "npm run build && git add .",
18 | "postversion": "git push && git push --tags && npm publish",
19 | "builder": "concurrently --kill-others \"NODE_ENV=development component=builder webpack-dev-server\"",
20 | "viewer": "concurrently --kill-others \"NODE_ENV=development component=viewer webpack-dev-server\"",
21 | "commit": "npx git-cz",
22 | "circular": "madge --circular ./lib/**/*",
23 | "test": "NODE_ENV=test jest",
24 | "test:cov": "NODE_ENV=test jest --coverage",
25 | "test:watch": "NODE_ENV=test jest --watch",
26 | "predeploy": "npm run build:gh-pages",
27 | "deploy": "gh-pages -d dist"
28 | },
29 | "jest": {
30 | "snapshotSerializers": [
31 | "enzyme-to-json/serializer"
32 | ],
33 | "setupFiles": [
34 | "/jestSetup.js"
35 | ]
36 | },
37 | "config": {
38 | "commitizen": {
39 | "path": "./node_modules/cz-conventional-changelog"
40 | }
41 | },
42 | "husky": {
43 | "hooks": {
44 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
45 | "pre-commit": "lint-staged"
46 | }
47 | },
48 | "lint-staged": {
49 | "*.{js,jsx}": [
50 | "npm run prettier",
51 | "git add *"
52 | ]
53 | },
54 | "repository": {
55 | "type": "git",
56 | "url": "https://github.com/keen/dashboard-builder.git"
57 | },
58 | "bugs": "https://github.com/keen/dashboard-builder/issues",
59 | "author": "Keen.IO (https://keen.io/)",
60 | "contributors": [
61 | "Dariusz Łacheta (https://github.com/dariuszlacheta)"
62 | ],
63 | "homepage": "https://keen.github.io/dashboard-builder/",
64 | "keywords": [
65 | "React Charts",
66 | "d3",
67 | "c3",
68 | "Analytics",
69 | "Stats",
70 | "Statistics",
71 | "Visualization",
72 | "Visualizations",
73 | "Data Visualization",
74 | "Chart",
75 | "Charts",
76 | "Charting",
77 | "Svg",
78 | "Dataviz",
79 | "Plots",
80 | "Graphs",
81 | "Funnels"
82 | ],
83 | "dependencies": {
84 | "@fortawesome/fontawesome-svg-core": "^1.2.17",
85 | "@fortawesome/free-solid-svg-icons": "^5.8.1",
86 | "@fortawesome/react-fontawesome": "^0.1.4",
87 | "babel-jest": "^24.9.0",
88 | "babel-plugin-prismjs": "^1.1.1",
89 | "dom-to-image": "^2.6.0",
90 | "file-saver": "^2.0.2",
91 | "highlight.js": "^9.15.8",
92 | "javascript-time-ago": "^2.0.4",
93 | "keen-analysis": "^3.4.0",
94 | "keen-dataviz": "^3.13.8",
95 | "keen-explorer": "^6.0.19",
96 | "keen-theme-builder": "^1.0.28",
97 | "lodash": "^4.17.11",
98 | "prettier": "^1.18.2",
99 | "prismjs": "^1.17.1",
100 | "prop-types": "^15.6.2",
101 | "react": "^16.4.2",
102 | "react-dom": "^16.4.2",
103 | "react-grid-layout": "^0.17.1",
104 | "react-html-parser": "^2.0.2",
105 | "react-quill": "^1.3.3",
106 | "react-redux": "^7.1.0",
107 | "react-router-dom": "^5.0.0",
108 | "react-select": "^2.4.3",
109 | "react-tabs": "^3.0.0",
110 | "react-time-ago": "^5.0.4",
111 | "react-tooltip": "^3.10.0",
112 | "redux": "^4.0.1",
113 | "redux-thunk": "^2.3.0",
114 | "styled-jsx": "^3.2.1",
115 | "uuidv4": "^6.0.0",
116 | "webfontloader": "^1.6.28"
117 | },
118 | "devDependencies": {
119 | "@babel/cli": "^7.2.3",
120 | "@babel/core": "^7.4.0",
121 | "@babel/plugin-proposal-object-rest-spread": "^7.4.0",
122 | "@babel/plugin-transform-arrow-functions": "^7.2.0",
123 | "@babel/plugin-transform-runtime": "^7.4.4",
124 | "@babel/preset-env": "^7.4.5",
125 | "@babel/preset-react": "^7.0.0",
126 | "@babel/runtime": "^7.4.5",
127 | "@commitlint/cli": "^8.2.0",
128 | "@commitlint/config-conventional": "^8.2.0",
129 | "autoprefixer": "^8.2.0",
130 | "babel-eslint": "^10.0.3",
131 | "babel-jest": "^24.9.0",
132 | "babel-loader": "^8.0.5",
133 | "babel-plugin-syntax-class-properties": "^6.13.0",
134 | "babel-plugin-transform-class-properties": "^6.24.1",
135 | "babel-plugin-transform-es2015-arrow-functions": "^6.22.0",
136 | "babel-plugin-transform-object-assign": "^6.22.0",
137 | "commitizen": "^4.0.3",
138 | "concurrently": "^3.5.1",
139 | "css-loader": "^1.0.0",
140 | "cssnano": "^3.10.0",
141 | "enzyme": "^3.7.0",
142 | "enzyme-adapter-react-16": "^1.7.2",
143 | "enzyme-to-json": "^3.4.3",
144 | "eslint": "^4.19.1",
145 | "eslint-config-airbnb": "^16.1.0",
146 | "eslint-config-prettier": "^6.7.0",
147 | "eslint-loader": "^2.0.0",
148 | "eslint-plugin-import": "^2.11.0",
149 | "eslint-plugin-jest": "^23.1.1",
150 | "eslint-plugin-jsx-a11y": "^6.0.3",
151 | "eslint-plugin-prettier": "^3.1.1",
152 | "eslint-plugin-react": "^7.7.0",
153 | "eslint-plugin-react-hooks": "^2.3.0",
154 | "gh-pages": "^2.0.1",
155 | "git-cz": "^3.3.0",
156 | "html-loader": "^0.5.5",
157 | "html-webpack-plugin": "^3.2.0",
158 | "husky": "^3.1.0",
159 | "jest": "^24.9.0",
160 | "jest-environment-jsdom-c3": "^2.0.0",
161 | "jest-fetch-mock": "^2.1.2",
162 | "lint-staged": "^9.5.0",
163 | "madge": "^3.6.0",
164 | "nock": "^9.2.6",
165 | "postcss": "^6.0.21",
166 | "postcss-cli": "^5.0.0",
167 | "postcss-color-function": "^4.0.1",
168 | "postcss-css-variables": "^0.8.1",
169 | "postcss-cssnext": "^2.4.0",
170 | "postcss-import": "^8.0.2",
171 | "postcss-loader": "^2.1.3",
172 | "precss": "^3.1.2",
173 | "redux-mock-store": "^1.5.3",
174 | "regenerator-runtime": "^0.11.1",
175 | "replace-in-file": "^3.4.0",
176 | "style-loader": "^0.20.3",
177 | "webpack": "^4.5.0",
178 | "webpack-bundle-analyzer": "^3.3.2",
179 | "webpack-cli": "^3.3.4",
180 | "webpack-dev-server": "^3.7.1",
181 | "xhr-mock": "^2.3.2"
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | const autoprefixer = require('autoprefixer');
2 |
3 | module.exports = {
4 | plugins: [
5 | require('precss'),
6 | require('postcss-css-variables'),
7 | require('postcss-color-function'),
8 | process.env.OPTIMIZE_MINIMIZE ? require('cssnano')({
9 | preset: 'default',
10 | }) : null,
11 | autoprefixer({
12 | browsers: [
13 | '>1%',
14 | 'last 4 versions',
15 | 'Firefox ESR',
16 | 'not ie < 9', // React doesn't support IE8 anyway
17 | ],
18 | flexbox: 'no-2009',
19 | }),
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/test/demo/index-viewer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Keen dashboard builder
8 |
9 |
10 |
11 |
12 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/test/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Keen dashboard builder
8 |
9 |
10 |
11 |
12 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/setupTests.js:
--------------------------------------------------------------------------------
1 | import Enzyme from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 |
4 | Enzyme.configure({ adapter: new Adapter() });
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const HtmlWebPackPlugin = require('html-webpack-plugin');
4 |
5 | const extendedPath = path.resolve(__dirname, 'dist');
6 | let alias = {
7 | Client: path.resolve(__dirname, 'lib/')
8 | };
9 |
10 | let definePluginVars = {};
11 |
12 | if (process.env.NODE_ENV === 'development') {
13 | const demoConfig = require('../demo-config');
14 | definePluginVars = {
15 | webpackKeenGlobals: JSON.stringify({ demoConfig }),
16 | KEEN_DASHBOARD_BUILDER_VERSION: JSON.stringify(
17 | require('./package.json').version
18 | )
19 | };
20 | }
21 |
22 | if (process.env.NODE_ENV === 'production') {
23 | definePluginVars = {
24 | KEEN_DASHBOARD_BUILDER_VERSION: JSON.stringify(
25 | require('./package.json').version
26 | )
27 | };
28 | }
29 |
30 | switch (process.env.NODE_ENV) {
31 | case 'production':
32 | switch (process.env.component) {
33 | case 'builder':
34 | entry = {
35 | main: './lib/index.js'
36 | };
37 | alias = {
38 | Client: path.resolve(__dirname, 'lib/')
39 | };
40 | name = 'main';
41 | break;
42 | case 'viewer':
43 | entry = {
44 | viewer: './lib/viewer/index.js'
45 | };
46 | alias = {
47 | Client: path.resolve(__dirname, 'lib/viewer/')
48 | };
49 | name = 'viewer';
50 | break;
51 | default:
52 | break;
53 | }
54 | break;
55 |
56 | case 'development':
57 | switch (process.env.component) {
58 | case 'builder':
59 | entry = './lib/builder/index.js';
60 | alias = {
61 | Client: path.resolve(__dirname, 'lib/builder/')
62 | };
63 | name = 'main';
64 | break;
65 | case 'viewer':
66 | entry = './lib/viewer/index.js';
67 | alias = {
68 | Client: path.resolve(__dirname, 'lib/viewer/')
69 | };
70 | name = 'viewer';
71 | break;
72 |
73 | default:
74 | entry = './lib/index.js';
75 | alias = {
76 | Client: path.resolve(__dirname, 'lib/')
77 | };
78 | name = 'main';
79 | break;
80 | }
81 | break;
82 |
83 | default:
84 | break;
85 | }
86 |
87 | module.exports = {
88 | entry,
89 |
90 | target: 'web',
91 |
92 | output: {
93 | path: extendedPath,
94 | filename: `${name}${process.env.OPTIMIZE_MINIMIZE ? '.min' : ''}.js`,
95 | library: `${!process.env.LIBRARY ? '' : process.env.LIBRARY}`,
96 | libraryTarget: 'umd'
97 | },
98 |
99 | module: {
100 | rules: [
101 | {
102 | test: /\.js?$/,
103 | include: [path.resolve(__dirname, '')],
104 | exclude: [path.resolve(__dirname, 'node_modules')],
105 | loader: 'babel-loader'
106 | },
107 | {
108 | test: /\.html$/,
109 | loader: 'html-loader'
110 | },
111 | {
112 | test: /\.css$/,
113 | use: [
114 | 'style-loader',
115 | 'css-loader',
116 | {
117 | loader: 'postcss-loader',
118 | options: {
119 | config: {
120 | path: __dirname + '/postcss.config.js'
121 | }
122 | }
123 | }
124 | ]
125 | }
126 | ]
127 | },
128 |
129 | plugins: [
130 | new HtmlWebPackPlugin({
131 | template:
132 | process.env.component === 'viewer'
133 | ? './test/demo/index-viewer.html'
134 | : './test/demo/index.html',
135 | filename: './index.html',
136 | title: 'Dashboard Builder'
137 | }),
138 | new webpack.DefinePlugin(definePluginVars)
139 | ],
140 |
141 | resolve: {
142 | modules: ['node_modules'],
143 | extensions: ['.js', '.json', '.jsx'],
144 | alias
145 | },
146 |
147 | optimization: {
148 | minimize: !!process.env.OPTIMIZE_MINIMIZE
149 | },
150 |
151 | //devtool: 'source-map',
152 |
153 | context: __dirname,
154 |
155 | //stats: 'verbose',
156 |
157 | mode: process.env.NODE_ENV,
158 |
159 | devServer: {
160 | contentBase: path.join(__dirname, 'test/demo'),
161 | publicPath: '/',
162 | open: true,
163 | watchContentBase: true,
164 | historyApiFallback: true
165 | }
166 | };
167 |
--------------------------------------------------------------------------------