├── .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 | Slack 8 | 9 | 10 | 11 | ## Build status 12 | 13 | [![CircleCI](https://circleci.com/gh/keen/dashboard-builder/tree/develop.svg?style=svg)](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 | Keen dashboard builder
-------------------------------------------------------------------------------- /jestSetup.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | global.fetch = require('jest-fetch-mock'); 5 | 6 | Enzyme.configure({ adapter: new Adapter() }); 7 | -------------------------------------------------------------------------------- /lib/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_DASHBOARD_ITEM = 'ADD_DASHBOARD_ITEM'; 2 | 3 | export const HANDLE_SEARCH = 'HANDLE_SEARCH'; 4 | 5 | export const DELETE_DASHBOARD_ITEM = 'DELETE_DASHBOARD_ITEM'; 6 | 7 | export const GET_SAVED_QUERIES = 'GET_SAVED_QUERIES'; 8 | 9 | export const LOAD_SAVED_QUERIES = 'LOAD_SAVED_QUERIES'; 10 | 11 | export const LOAD_SAVED_ERROR = 'LOAD_SAVED_ERROR'; 12 | 13 | export const LOAD_DASHBOARDS = 'LOAD_DASHBOARDS'; 14 | 15 | export const LOAD_DASHBOARD_INFO = 'LOAD_DASHBOARD_INFO'; 16 | 17 | export const UPDATE_DASHBOARD_INFO = 'UPDATE_DASHBOARD_INFO'; 18 | 19 | export const CLEAR_DASHBOARD_INFO = 'CLEAR_DASHBOARD_INFO'; 20 | 21 | export const MAP_OLD_ITEMS = 'MAP_OLD_ITEMS'; 22 | 23 | export const SAVE_DASHBOARD = 'SAVE_DASHBOARD'; 24 | 25 | export const HIDE_SAVED_DASHBOARD_MESSAGE = 'HIDE_SAVED_DASHBOARD_MESSAGE'; 26 | 27 | export const TOGGLE_DRY_RUN = 'TOGGLE_DRY_RUN'; 28 | 29 | export const TOGGLE_IS_PUBLIC = 'TOGGLE_IS_PUBLIC'; 30 | 31 | export const CHANGE_DASHBOARD_TITLE = 'CHANGE_DASHBOARD_TITLE'; 32 | 33 | export const SHOW_TOOLBAR = 'SHOW_TOOLBAR'; 34 | 35 | export const CLOSE_TOOLBAR = 'CLOSE_TOOLBAR'; 36 | 37 | export const DRAG_START_HANDLER = 'DRAG_START_HANDLER'; 38 | 39 | export const DROP_HANDLER = 'DROP_HANDLER'; 40 | 41 | export const SET_LOADING = 'SET_LOADING'; 42 | 43 | export const SELECT_SAVED_QUERY = 'SELECT_SAVED_QUERY'; 44 | 45 | export const SAVED_QUERY_ERROR = 'SAVED_QUERY_ERROR'; 46 | 47 | export const CHANGE_CHART_TYPE = 'CHANGE_CHART_TYPE'; 48 | 49 | export const DELETE_CHART = 'DELETE_CHART'; 50 | 51 | export const CLOSE_SETTINGS = 'CLOSE_SETTINGS'; 52 | 53 | export const SHOW_SETTINGS = 'SHOW_SETTINGS'; 54 | 55 | export const SET_SRC_FOR_IMG = 'SET_SRC_FOR_IMG'; 56 | 57 | export const SET_TEXT_FOR_PARAGRAPH = 'SET_TEXT_FOR_PARAGRAPH'; 58 | 59 | export const CLONE_CHART = 'CLONE_CHART'; 60 | 61 | export const TOGGLE_DASHBOARDS_MENU = 'TOGGLE_DASHBOARDS_MENU'; 62 | 63 | export const SET_NEW_DASHBOARD_FOR_FOCUS = 'SET_NEW_DASHBOARD_FOR_FOCUS'; 64 | 65 | export const SET_ACCESS_KEY = 'SET_ACCESS_KEY'; 66 | 67 | export const CLEAR_ACCESS_KEY = 'CLEAR_ACCESS_KEY'; 68 | 69 | export const LOADING_SINGLE_DASHBOARD = 'LOADING_SINGLE_DASHBOARD'; 70 | 71 | export const FILTER_DASHBOARDS_MENU = 'FILTER_DASHBOARDS_MENU'; 72 | 73 | export const CHANGE_SORTING = 'CHANGE_SORTING'; 74 | 75 | export const SET_THEME = 'SET_THEME'; 76 | 77 | export const SET_CHART_THEME = 'SET_CHART_THEME'; 78 | 79 | export const SET_LAYOUT = 'SET_LAYOUT'; 80 | 81 | export const CHANGE_SAVED_QUERY_LIST = 'CHANGE_SAVED_QUERY_LIST'; 82 | 83 | export const LOAD_DUMMY_DASHBOARDS = 'LOAD_DUMMY_DASHBOARDS'; 84 | 85 | export const CLEAR_ITEMS = 'CLEAR_ITEMS'; 86 | -------------------------------------------------------------------------------- /lib/actions/constants.js: -------------------------------------------------------------------------------- 1 | export const CLIENTS_ACTION_PREFIX = '@clients'; 2 | -------------------------------------------------------------------------------- /lib/actions/rootActions.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import * as actions from './actionTypes'; 4 | import { getDashboardInfo, getSavedQueriesList } from '../selectors/app'; 5 | import updateAPIKey from '../func/updateApiKey'; 6 | import classicDashboardDataParse from '../func/classicDashboardDataParse'; 7 | import newGridDataParse from '../func/newGridDataParse'; 8 | import { generateUniqueId } from '../utils/generateUniqueId'; 9 | import loadFontsFromDashboard from '../utils/loadFontsFromDashboard'; 10 | import ReactTooltip from 'react-tooltip'; 11 | 12 | export const addDashboardItem = (title = 'Untitled') => ( 13 | dispatch, 14 | getState, 15 | { keenClient, keenWebHost, keenWebFetchOptions } 16 | ) => { 17 | fetch( 18 | `https://${keenWebHost}/projects/${keenClient.projectId()}/dashboards/`, 19 | { 20 | method: 'post', 21 | body: JSON.stringify({ 22 | title, 23 | data: { version: 2, items: [] }, 24 | settings: { dryRun: false } 25 | }), 26 | ...keenWebFetchOptions 27 | } 28 | ) 29 | .then(res => 30 | res.json().then(data => { 31 | dispatch({ 32 | type: actions.ADD_DASHBOARD_ITEM, 33 | dashboardInfo: data 34 | }); 35 | }) 36 | ) 37 | .catch(err => console.error(err)); 38 | }; 39 | 40 | export const handleSearch = value => ({ 41 | type: actions.HANDLE_SEARCH, 42 | value 43 | }); 44 | 45 | export const deleteDashboardItem = (id, is_public) => ( 46 | dispatch, 47 | getState, 48 | { keenClient, keenWebHost, keenWebFetchOptions } 49 | ) => { 50 | const keyName = `public-dashboard: ${id}`; 51 | 52 | fetch( 53 | `https://${keenWebHost}/projects/${keenClient.projectId()}/dashboards/${id}`, 54 | { 55 | method: 'delete', 56 | ...keenWebFetchOptions 57 | } 58 | ) 59 | .then(res => { 60 | if (res.status === 204) { 61 | if (is_public) dispatch(deleteAccessKey(keyName)); 62 | dispatch({ 63 | type: actions.DELETE_DASHBOARD_ITEM, 64 | id 65 | }); 66 | } 67 | }) 68 | .catch(err => console.error(err)); 69 | }; 70 | 71 | export const deleteAccessKey = id => ( 72 | dispatch, 73 | getState, 74 | { keenClient, keenWebHost, keenWebFetchOptions } 75 | ) => { 76 | keenClient 77 | .get({ 78 | url: keenClient.url('projectId', `keys?name=${id}`), 79 | api_key: keenClient.masterKey() 80 | }) 81 | .then(res => { 82 | const customKey = res[0].key; 83 | fetch( 84 | `https://api.keen.io/3.0/projects/${keenClient.projectId()}/keys/${customKey}`, 85 | { 86 | method: 'delete', 87 | headers: { 88 | Authorization: keenClient.masterKey() 89 | } 90 | } 91 | ); 92 | }) 93 | .catch(err => console.error(err)); 94 | }; 95 | 96 | export const getSavedQueries = () => ( 97 | dispatch, 98 | getState, 99 | { keenClient, keenWebHost, keenWebFetchOptions } 100 | ) => { 101 | keenClient 102 | .get({ 103 | url: keenClient.url('queries', 'saved'), 104 | api_key: keenClient.masterKey() 105 | }) 106 | .then(res => 107 | dispatch({ 108 | type: actions.GET_SAVED_QUERIES, 109 | savedQueries: res 110 | }) 111 | ) 112 | .catch(err => { 113 | console.error(err); 114 | }); 115 | }; 116 | 117 | export const savedQueryError = (error, index) => ({ 118 | type: actions.SAVED_QUERY_ERROR, 119 | error, 120 | index 121 | }); 122 | 123 | export const loadDashboards = () => ( 124 | dispatch, 125 | getState, 126 | { keenClient, keenWebHost, keenWebFetchOptions } 127 | ) => { 128 | fetch( 129 | `https://${keenWebHost}/projects/${keenClient.projectId()}/dashboards`, 130 | { 131 | ...keenWebFetchOptions 132 | } 133 | ) 134 | .then(res => 135 | res.json().then(data => 136 | dispatch({ 137 | type: actions.LOAD_DASHBOARDS, 138 | dashboardList: data 139 | }) 140 | ) 141 | ) 142 | .catch(err => console.error(err)); 143 | }; 144 | 145 | export const loadDashboardInfo = id => ( 146 | dispatch, 147 | getState, 148 | { keenClient, keenWebHost, keenWebFetchOptions } 149 | ) => { 150 | fetch( 151 | `https://${keenWebHost}/projects/${keenClient.projectId()}/dashboards/${id}`, 152 | { 153 | ...keenWebFetchOptions 154 | } 155 | ) 156 | .then(res => 157 | res.json().then(data => { 158 | const parsingCheck = parseData => { 159 | if ( 160 | !parseData.settings.items || 161 | (parseData.settings.items && !parseData.settings.items.length) 162 | ) { 163 | if ( 164 | !parseData.data.items || 165 | (parseData.data.items && !parseData.data.version) 166 | ) 167 | return classicDashboardDataParse(parseData, generateUniqueId); 168 | return newGridDataParse(parseData, generateUniqueId); 169 | } 170 | return parseData; 171 | }; 172 | loadFontsFromDashboard(data); 173 | dispatch({ 174 | type: actions.LOAD_DASHBOARD_INFO, 175 | dashboardInfo: parsingCheck(data), 176 | id, 177 | isDashboardLoading: false 178 | }); 179 | }) 180 | ) 181 | .catch(err => console.error(err)); 182 | }; 183 | 184 | export const updateDashboardInfo = data => ({ 185 | type: actions.UPDATE_DASHBOARD_INFO, 186 | dashboardInfo: data 187 | }); 188 | 189 | export const clearDashboardInfo = () => ({ 190 | type: actions.CLEAR_DASHBOARD_INFO 191 | }); 192 | 193 | export const saveDashboard = dashboard => ( 194 | dispatch, 195 | getState, 196 | { keenClient, keenWebHost, keenWebFetchOptions } 197 | ) => { 198 | dispatch({ 199 | type: actions.SAVE_DASHBOARD 200 | }); 201 | fetch( 202 | `https://${keenWebHost}/projects/${keenClient.projectId()}/dashboards/${ 203 | dashboard.id 204 | }`, 205 | { 206 | method: 'put', 207 | body: JSON.stringify(dashboard), 208 | ...keenWebFetchOptions 209 | } 210 | ) 211 | .then(res => { 212 | if (res.status === 200) { 213 | dispatch({ 214 | type: actions.HIDE_SAVED_DASHBOARD_MESSAGE 215 | }); 216 | } 217 | }) 218 | .catch(err => console.error(err)); 219 | }; 220 | 221 | export const makeDashboardPublicAndSave = dashboard => (dispatch, getState) => { 222 | let newDashboard = dashboard; 223 | if (!dashboard.is_public) { 224 | dispatch(toggleIsPublic()); 225 | newDashboard = getState().dashboardInfo; 226 | } 227 | dispatch(saveDashboard(newDashboard)); 228 | }; 229 | 230 | export const hideSavedDashboardMessage = () => ({ 231 | type: actions.HIDE_SAVED_DASHBOARD_MESSAGE 232 | }); 233 | 234 | export const toggleDryRun = () => ({ 235 | type: actions.TOGGLE_DRY_RUN 236 | }); 237 | 238 | export const toggleIsPublic = () => ({ 239 | type: actions.TOGGLE_IS_PUBLIC 240 | }); 241 | 242 | export const setTheme = value => { 243 | return { 244 | type: actions.SET_THEME, 245 | value 246 | }; 247 | }; 248 | 249 | export const setChartTheme = (index, value) => { 250 | return { 251 | type: actions.SET_CHART_THEME, 252 | index, 253 | value 254 | }; 255 | }; 256 | 257 | export const setLayout = layout => { 258 | return { 259 | type: actions.SET_LAYOUT, 260 | layout 261 | }; 262 | }; 263 | 264 | export const changeDashboardTitle = title => ({ 265 | type: actions.CHANGE_DASHBOARD_TITLE, 266 | title 267 | }); 268 | 269 | export const showToolbar = () => ({ 270 | type: actions.SHOW_TOOLBAR 271 | }); 272 | 273 | export const closeToolbar = () => ({ 274 | type: actions.CLOSE_TOOLBAR 275 | }); 276 | 277 | export const dragStartHandler = draggedType => ({ 278 | type: actions.DRAG_START_HANDLER, 279 | draggedType 280 | }); 281 | 282 | export const dropHandler = (element, id) => ({ 283 | type: actions.DROP_HANDLER, 284 | element, 285 | id 286 | }); 287 | 288 | export const setLoading = index => ({ 289 | type: actions.SET_LOADING, 290 | index 291 | }); 292 | 293 | export const selectSavedQuery = (queryNames, index) => ( 294 | dispatch, 295 | getState, 296 | { keenClient } 297 | ) => { 298 | const store = getState(); 299 | 300 | const dashboardInfo = getDashboardInfo(store); 301 | const item = dashboardInfo.settings.items.find(el => el.i === index); 302 | const delValue = item.savedQuery; 303 | const savedQueriesList = getSavedQueriesList(store); 304 | const savedQueries = Array.isArray(queryNames) ? queryNames : [queryNames]; 305 | dispatch(changeSavedQueryList(delValue, savedQueries)); 306 | const newStore = getState(); 307 | const newSavedQueriesList = getSavedQueriesList(newStore); 308 | const isPublic = dashboardInfo.is_public; 309 | if (isPublic && savedQueries.length) { 310 | const isNewQueryAdded = savedQueries.some( 311 | query => !savedQueriesList.includes(query.value) 312 | ); 313 | 314 | if (isNewQueryAdded) { 315 | updateAPIKey(newSavedQueriesList, dashboardInfo.id, keenClient); 316 | } 317 | } 318 | dispatch({ 319 | type: actions.SELECT_SAVED_QUERY, 320 | savedQueries, 321 | index 322 | }); 323 | }; 324 | 325 | export const loadSavedQuery = index => ({ 326 | type: actions.LOAD_SAVED_QUERIES, 327 | index 328 | }); 329 | 330 | export const changeChartType = (value, index) => ({ 331 | type: actions.CHANGE_CHART_TYPE, 332 | index, 333 | value 334 | }); 335 | 336 | export const deleteChart = index => (dispatch, getState, { keenClient }) => { 337 | const approvalDelChart = confirm('Do You want to delete this chart?'); 338 | if (approvalDelChart) { 339 | ReactTooltip.hide(); 340 | const store = getState(); 341 | const dashboardInfo = getDashboardInfo(store); 342 | const item = dashboardInfo.settings.items.find(el => el.i === index); 343 | const delValue = item.savedQuery; 344 | dispatch(changeSavedQueryList(delValue, [])); 345 | const newStore = getState(); 346 | const savedQueriesList = getSavedQueriesList(newStore); 347 | const isPublic = dashboardInfo.is_public; 348 | if (isPublic && delValue.length) { 349 | const isNewQueryAdded = delValue.some( 350 | query => !savedQueriesList.includes(query.value) 351 | ); 352 | if (isNewQueryAdded) { 353 | updateAPIKey(savedQueriesList, dashboardInfo.id, keenClient); 354 | } 355 | } 356 | dispatch({ 357 | type: actions.DELETE_CHART, 358 | index 359 | }); 360 | } 361 | }; 362 | 363 | export const closeSettings = () => ({ 364 | type: actions.CLOSE_SETTINGS 365 | }); 366 | 367 | export const showSettings = index => ({ 368 | type: actions.SHOW_SETTINGS, 369 | index 370 | }); 371 | 372 | export const setSrcForImg = (value, index) => ({ 373 | type: actions.SET_SRC_FOR_IMG, 374 | value, 375 | index 376 | }); 377 | 378 | export const setTextForParagraph = (newValue, source, index) => ({ 379 | type: actions.SET_TEXT_FOR_PARAGRAPH, 380 | newValue, 381 | source, 382 | index 383 | }); 384 | 385 | export const cloneChart = index => ({ 386 | type: actions.CLONE_CHART, 387 | index 388 | }); 389 | 390 | export const toggleDashboardsMenu = value => ({ 391 | type: actions.TOGGLE_DASHBOARDS_MENU, 392 | value 393 | }); 394 | 395 | export const setNewDashboardForFocus = value => ({ 396 | type: actions.SET_NEW_DASHBOARD_FOR_FOCUS, 397 | value 398 | }); 399 | 400 | export const setAccessKey = value => ({ 401 | type: actions.SET_ACCESS_KEY, 402 | value 403 | }); 404 | 405 | export const clearAccessKey = () => ({ 406 | type: actions.CLEAR_ACCESS_KEY 407 | }); 408 | 409 | export const mapOldItems = newDashboard => ({ 410 | type: actions.MAP_OLD_ITEMS, 411 | newDashboard 412 | }); 413 | 414 | export const loadingSingleDashboard = () => ({ 415 | type: actions.LOADING_SINGLE_DASHBOARD 416 | }); 417 | 418 | export const filterDashboardsMenu = value => ({ 419 | type: actions.FILTER_DASHBOARDS_MENU, 420 | value 421 | }); 422 | 423 | export const changeSorting = sorting => ({ 424 | type: actions.CHANGE_SORTING, 425 | sorting 426 | }); 427 | 428 | export const changeSavedQueryList = (delValue, addValue) => ( 429 | dispatch, 430 | getState 431 | ) => { 432 | const store = getState(); 433 | const dashboardInfo = getDashboardInfo(store); 434 | const savedQueriesList = dashboardInfo.settings.savedQueriesList 435 | ? [...dashboardInfo.settings.savedQueriesList] 436 | : []; 437 | savedQueriesList.length && 438 | delValue && 439 | delValue.forEach(el => { 440 | let delIndex = savedQueriesList.indexOf(el.value); 441 | delIndex >= 0 && savedQueriesList.splice(delIndex, 1); 442 | }); 443 | addValue.length && 444 | addValue.forEach(el => { 445 | savedQueriesList.push(el.value); 446 | }); 447 | 448 | dispatch({ 449 | type: actions.CHANGE_SAVED_QUERY_LIST, 450 | savedQueriesList 451 | }); 452 | }; 453 | 454 | export const loadDummyDashboards = () => ({ 455 | type: actions.LOAD_DUMMY_DASHBOARDS 456 | }); 457 | -------------------------------------------------------------------------------- /lib/actions/rootActions.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | selectSavedQuery, 3 | deleteChart, 4 | changeSavedQueryList 5 | } from './rootActions'; 6 | 7 | import { defaultData as initialState } from '../reducers/rootReducer'; 8 | 9 | import { 10 | SELECT_SAVED_QUERY, 11 | DELETE_CHART, 12 | CHANGE_SAVED_QUERY_LIST 13 | } from './actionTypes'; 14 | import { APP_REDUCER } from '../constants'; 15 | 16 | describe('Root Actions', () => { 17 | describe('selectSavedQuery()', () => { 18 | let dispatch; 19 | let keenClient; 20 | 21 | const query = { 22 | value: 'queryValue' 23 | }; 24 | 25 | beforeEach(() => { 26 | dispatch = jest.fn(); 27 | keenClient = { 28 | get: jest 29 | .fn() 30 | .mockImplementationOnce(() => 31 | Promise.resolve([{ key: 'access_key' }]) 32 | ), 33 | post: jest.fn(), 34 | url: jest.fn(), 35 | masterKey: jest.fn() 36 | }; 37 | }); 38 | 39 | it('should not update access key for non empty saved queries', () => { 40 | const state = { 41 | [APP_REDUCER]: { 42 | ...initialState, 43 | dashboardInfo: { 44 | is_public: true, 45 | settings: { 46 | items: [ 47 | { 48 | i: 0, 49 | savedQuery: [{ value: 'initialQueryValue' }] 50 | } 51 | ] 52 | }, 53 | data: { 54 | items: [ 55 | { 56 | savedQuery: [{ value: 'initialQueryValue' }] 57 | } 58 | ] 59 | } 60 | } 61 | } 62 | }; 63 | const getState = jest.fn().mockImplementationOnce(() => state); 64 | const thunkAction = selectSavedQuery([], 0); 65 | 66 | thunkAction(dispatch, getState, { keenClient }); 67 | 68 | expect(keenClient.get).not.toHaveBeenCalled(); 69 | expect(dispatch).toHaveBeenCalledTimes(2); 70 | }); 71 | 72 | it('should not update access key for non public dashboard', () => { 73 | const state = { 74 | [APP_REDUCER]: { 75 | ...initialState, 76 | dashboardInfo: { 77 | is_public: false, 78 | settings: { 79 | items: [ 80 | { 81 | i: 0, 82 | savedQuery: [{ value: 'initialQueryValue' }] 83 | } 84 | ] 85 | }, 86 | data: { 87 | items: [ 88 | { 89 | savedQuery: [{ value: 'initialQueryValue' }] 90 | } 91 | ] 92 | } 93 | } 94 | } 95 | }; 96 | 97 | const getState = jest.fn().mockImplementationOnce(() => state); 98 | const thunkAction = selectSavedQuery(query, 0); 99 | 100 | thunkAction(dispatch, getState, { keenClient }); 101 | 102 | expect(keenClient.get).not.toHaveBeenCalled(); 103 | expect(dispatch).toHaveBeenCalledTimes(2); 104 | }); 105 | 106 | it('should not update access key for already existing saved query', () => { 107 | const state = { 108 | [APP_REDUCER]: { 109 | ...initialState, 110 | dashboardInfo: { 111 | is_public: true, 112 | settings: { 113 | savedQueriesList: [query.value], 114 | items: [ 115 | { 116 | i: 0, 117 | savedQuery: [{ value: 'initialQueryValue' }] 118 | } 119 | ] 120 | }, 121 | data: { 122 | items: [ 123 | { 124 | savedQuery: [{ value: 'initialQueryValue' }] 125 | } 126 | ] 127 | } 128 | } 129 | } 130 | }; 131 | 132 | const getState = jest.fn().mockImplementationOnce(() => state); 133 | const thunkAction = selectSavedQuery(query, 0); 134 | 135 | thunkAction(dispatch, getState, { keenClient }); 136 | 137 | expect(keenClient.get).not.toHaveBeenCalled(); 138 | expect(dispatch).toHaveBeenCalledTimes(2); 139 | }); 140 | 141 | it('should update access key', () => { 142 | const state = { 143 | [APP_REDUCER]: { 144 | ...initialState, 145 | dashboardInfo: { 146 | is_public: true, 147 | settings: { 148 | items: [ 149 | { 150 | i: 0, 151 | savedQuery: [{ value: 'initialQueryValue' }] 152 | } 153 | ] 154 | }, 155 | data: { 156 | items: [ 157 | { 158 | savedQuery: [{ value: 'initialQueryValue' }] 159 | } 160 | ] 161 | } 162 | } 163 | } 164 | }; 165 | 166 | const getState = jest.fn().mockImplementationOnce(() => state); 167 | const thunkAction = selectSavedQuery(query, 0); 168 | 169 | thunkAction(dispatch, getState, { keenClient }); 170 | 171 | expect(keenClient.get).toHaveBeenCalled(); 172 | expect(dispatch).toHaveBeenCalledTimes(2); 173 | expect(dispatch).toHaveBeenLastCalledWith({ 174 | type: SELECT_SAVED_QUERY, 175 | index: 0, 176 | savedQueries: [query] 177 | }); 178 | }); 179 | }); 180 | 181 | describe('delChart()', () => { 182 | let dispatch; 183 | let keenClient; 184 | 185 | beforeEach(() => { 186 | dispatch = jest.fn(); 187 | keenClient = { 188 | get: jest 189 | .fn() 190 | .mockImplementationOnce(() => 191 | Promise.resolve([{ key: 'access_key' }]) 192 | ), 193 | post: jest.fn(), 194 | url: jest.fn(), 195 | masterKey: jest.fn() 196 | }; 197 | }); 198 | 199 | it('should not delete chart and not update access key when confirm is false', () => { 200 | const state = { 201 | [APP_REDUCER]: { 202 | ...initialState, 203 | dashboardInfo: { 204 | is_public: true, 205 | settings: { 206 | items: [ 207 | { 208 | i: 0, 209 | savedQuery: [{ value: 'initialQueryValue' }] 210 | } 211 | ] 212 | }, 213 | data: { 214 | items: [ 215 | { 216 | savedQuery: [{ value: 'initialQueryValue' }] 217 | } 218 | ] 219 | } 220 | } 221 | } 222 | }; 223 | window.confirm = jest.fn().mockImplementationOnce(() => false); 224 | 225 | const getState = jest.fn().mockImplementationOnce(() => state); 226 | const thunkAction = deleteChart(0); 227 | 228 | thunkAction(dispatch, getState, { keenClient }); 229 | 230 | expect(window.confirm).toHaveBeenCalled(); 231 | expect(keenClient.get).not.toHaveBeenCalled(); 232 | expect(dispatch).not.toHaveBeenCalled(); 233 | }); 234 | 235 | it('should delete chart and not update access key when confirm is true but dashboard is not public', () => { 236 | const state = { 237 | [APP_REDUCER]: { 238 | ...initialState, 239 | dashboardInfo: { 240 | is_public: false, 241 | settings: { 242 | items: [ 243 | { 244 | i: 0, 245 | savedQuery: [{ value: 'initialQueryValue' }] 246 | } 247 | ] 248 | }, 249 | data: { 250 | items: [ 251 | { 252 | savedQuery: [{ value: 'initialQueryValue' }] 253 | } 254 | ] 255 | } 256 | } 257 | } 258 | }; 259 | window.confirm = jest.fn().mockImplementationOnce(() => true); 260 | 261 | const getState = jest.fn().mockImplementationOnce(() => state); 262 | const thunkAction = deleteChart(0); 263 | 264 | thunkAction(dispatch, getState, { keenClient }); 265 | 266 | expect(window.confirm).toHaveBeenCalled(); 267 | expect(keenClient.get).not.toHaveBeenCalled(); 268 | expect(dispatch).toHaveBeenLastCalledWith({ 269 | type: DELETE_CHART, 270 | index: 0 271 | }); 272 | }); 273 | 274 | it('should delete chart and not update access key when saved query is not assigned', () => { 275 | const state = { 276 | [APP_REDUCER]: { 277 | ...initialState, 278 | dashboardInfo: { 279 | is_public: true, 280 | settings: { 281 | items: [ 282 | { 283 | i: 0, 284 | savedQuery: [] 285 | } 286 | ] 287 | }, 288 | data: { 289 | items: [ 290 | { 291 | savedQuery: [] 292 | } 293 | ] 294 | } 295 | } 296 | } 297 | }; 298 | window.confirm = jest.fn().mockImplementationOnce(() => true); 299 | 300 | const getState = jest.fn().mockImplementationOnce(() => state); 301 | const thunkAction = deleteChart(0); 302 | 303 | thunkAction(dispatch, getState, { keenClient }); 304 | 305 | expect(window.confirm).toHaveBeenCalled(); 306 | expect(keenClient.get).not.toHaveBeenCalled(); 307 | expect(dispatch).toHaveBeenLastCalledWith({ 308 | type: DELETE_CHART, 309 | index: 0 310 | }); 311 | }); 312 | 313 | it('should delete chart and update access key', () => { 314 | const state = { 315 | [APP_REDUCER]: { 316 | ...initialState, 317 | dashboardInfo: { 318 | is_public: true, 319 | settings: { 320 | items: [ 321 | { 322 | i: 0, 323 | savedQuery: [{ value: 'initialQueryValue' }] 324 | } 325 | ] 326 | }, 327 | data: { 328 | items: [ 329 | { 330 | savedQuery: [{ value: 'initialQueryValue' }] 331 | } 332 | ] 333 | } 334 | } 335 | } 336 | }; 337 | window.confirm = jest.fn().mockImplementationOnce(() => true); 338 | 339 | const getState = jest 340 | .fn() 341 | .mockImplementationOnce(() => state) 342 | .mockImplementationOnce(() => state); 343 | const thunkAction = deleteChart(0); 344 | 345 | thunkAction(dispatch, getState, { keenClient }); 346 | 347 | expect(window.confirm).toHaveBeenCalled(); 348 | expect(keenClient.get).toHaveBeenCalled(); 349 | expect(dispatch).toHaveBeenLastCalledWith({ 350 | type: DELETE_CHART, 351 | index: 0 352 | }); 353 | }); 354 | }); 355 | 356 | describe('change', () => { 357 | const dispatch = jest.fn(); 358 | 359 | it('should dispatch CHANGE_SAVED_QUERY_LIST action', () => { 360 | const state = { 361 | [APP_REDUCER]: { 362 | ...initialState, 363 | dashboardInfo: { 364 | settings: { 365 | savedQueriesList: ['queryName', 'queryName1'] 366 | } 367 | } 368 | } 369 | }; 370 | 371 | const savedQueriesList = ['queryName1', 'queryName2']; 372 | 373 | const getState = jest.fn().mockImplementationOnce(() => state); 374 | const thunkAction = changeSavedQueryList( 375 | [{ value: 'queryName' }], 376 | [{ value: 'queryName2' }] 377 | ); 378 | 379 | thunkAction(dispatch, getState); 380 | 381 | expect(dispatch).toHaveBeenLastCalledWith({ 382 | type: CHANGE_SAVED_QUERY_LIST, 383 | savedQueriesList 384 | }); 385 | }); 386 | }); 387 | }); 388 | -------------------------------------------------------------------------------- /lib/builder/components/Buttons.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { 4 | deleteChart, 5 | showSettings, 6 | closeSettings, 7 | cloneChart 8 | } from '../../actions/rootActions'; 9 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 10 | import ReactTooltip from 'react-tooltip'; 11 | 12 | const ChartButtons = props => { 13 | const { index, editedWidget, savedQuery } = props; 14 | const showSettings = e => { 15 | e.stopPropagation(); 16 | props.showSettings(index); 17 | }; 18 | const { origin, pathname } = window.location; 19 | const openNewTab = () => { 20 | savedQuery.forEach(element => { 21 | window.open( 22 | `${origin}${pathname}explorer?saved_query=${element.value}`, 23 | '_blank' 24 | ); 25 | }); 26 | }; 27 | let opacity; 28 | if (editedWidget === index) { 29 | opacity = '0'; 30 | } 31 | return ( 32 | 33 |
34 |
showSettings(e)} 38 | > 39 | 40 |
41 |
props.cloneChart(index)} 45 | > 46 | 47 |
48 | {savedQuery && savedQuery.length > 0 && ( 49 |
54 | 55 |
56 | )} 57 |
{ 61 | e.stopPropagation(); 62 | props.deleteChart(index); 63 | }} 64 | > 65 | 66 |
67 |
68 | dataTip} 74 | /> 75 | dataTip} 81 | /> 82 | dataTip} 88 | /> 89 | dataTip} 95 | /> 96 |
97 | ); 98 | }; 99 | 100 | const mapStateToProps = state => { 101 | const { isMoving, isResizing } = state.app; 102 | return { 103 | isMoving, 104 | isResizing 105 | }; 106 | }; 107 | 108 | const mapDispatchTopProps = { 109 | deleteChart, 110 | showSettings, 111 | closeSettings, 112 | cloneChart 113 | }; 114 | 115 | export default connect( 116 | mapStateToProps, 117 | mapDispatchTopProps 118 | )(ChartButtons); 119 | -------------------------------------------------------------------------------- /lib/builder/components/Chart.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import React, { Component, PureComponent } from 'react'; 4 | import { connect } from 'react-redux'; 5 | import { savedQueryError } from '../../actions/rootActions'; 6 | import PropTypes from 'prop-types'; 7 | import KeenDataviz from 'keen-dataviz'; 8 | import isEqual from 'lodash/isEqual'; 9 | 10 | class Chart extends Component { 11 | constructor(props) { 12 | super(props); 13 | } 14 | 15 | componentDidUpdate() { 16 | this.keenDataviz.destroy(); 17 | try { 18 | this.createKeenDataviz(); 19 | } catch (error) { 20 | this.props.savedQueryError(error, this.props.index); 21 | } 22 | } 23 | 24 | componentWillUnmount() { 25 | this.keenDataviz.destroy(); 26 | } 27 | 28 | shouldComponentUpdate(nextProps) { 29 | if ( 30 | this.props.width !== nextProps.width || 31 | this.props.height !== nextProps.height || 32 | this.props.palette !== nextProps.palette || 33 | this.props.type !== nextProps.type || 34 | this.props.legend.position !== nextProps.legend.position || 35 | this.props.sparkline !== nextProps.sparkline || 36 | this.props.stacking !== nextProps.stacking || 37 | this.props.savedQuery !== nextProps.savedQuery || 38 | !isEqual(this.props.results, nextProps.results) || 39 | !isEqual(this.props.colors, nextProps.colors) || 40 | this.props.screenSize !== nextProps.screenSize || 41 | this.props.prefix !== nextProps.prefix || 42 | this.props.suffix !== nextProps.suffix || 43 | !isEqual(this.props.point, nextProps.point) || 44 | !isEqual(this.props.choropleth, nextProps.choropleth) || 45 | !isEqual(this.props.heatmap, nextProps.heatmap) || 46 | !isEqual(this.props.funnel, nextProps.funnel) || 47 | !isEqual(this.props.table || nextProps.table) || 48 | !_.isEqual(this.props.axis, nextProps.axis) || 49 | this.props.w !== nextProps.w || 50 | this.props.h !== nextProps.h || 51 | this.props.title !== nextProps.title || 52 | this.props.subtitle !== nextProps.subtitle 53 | ) { 54 | return true; 55 | } 56 | return false; 57 | } 58 | 59 | createKeenDataviz() { 60 | const chartProps = { 61 | ...this.props, 62 | ...this.props.options, 63 | axis: { 64 | x: { 65 | label: this.props.axis && this.props.axis.x && this.props.axis.x.label 66 | }, 67 | y: { 68 | label: this.props.axis && this.props.axis.y && this.props.axis.y.label 69 | } 70 | }, 71 | palette: 72 | this.props.colors && this.props.colors.length ? '' : this.props.palette, 73 | results: 74 | this.props.type && 75 | this.props.type.includes('funnel') && 76 | isEqual(this.props.results.result, [200, 300, 100, 400, 250]) 77 | ? { result: [430, 300, 220, 150, 80] } 78 | : this.props.results 79 | }; 80 | 81 | this.keenDataviz = new KeenDataviz({ 82 | container: this.el, 83 | react: true, 84 | ...chartProps 85 | }); 86 | } 87 | 88 | handleRef = el => { 89 | if (el) { 90 | this.el = el; 91 | this.createKeenDataviz(); 92 | } 93 | }; 94 | 95 | render() { 96 | return
; 97 | } 98 | } 99 | 100 | const mapStateToProps = state => { 101 | const { 102 | dashboardInfo: { id }, 103 | screenSize 104 | } = state.app; 105 | return { 106 | id, 107 | screenSize 108 | }; 109 | }; 110 | 111 | const mapDispatchToProps = { 112 | savedQueryError 113 | }; 114 | 115 | export default connect( 116 | mapStateToProps, 117 | mapDispatchToProps 118 | )(Chart); 119 | 120 | Chart.propTypes = { 121 | type: PropTypes.string, 122 | showDeprecationWarnings: PropTypes.bool, 123 | showLoadingSpinner: PropTypes.bool, 124 | theme: PropTypes.string, 125 | dateFormat: PropTypes.string, 126 | title: PropTypes.oneOfType([ 127 | PropTypes.string, 128 | PropTypes.bool, 129 | PropTypes.number 130 | ]), 131 | legend: PropTypes.shape({ 132 | show: PropTypes.bool, 133 | position: PropTypes.string, 134 | label: PropTypes.shape({ 135 | textMaxLength: PropTypes.number 136 | }), 137 | pagination: PropTypes.shape({ 138 | offset: PropTypes.number, 139 | limit: PropTypes.number 140 | }), 141 | tooltip: PropTypes.shape({ 142 | show: PropTypes.bool, 143 | pointer: PropTypes.bool 144 | }), 145 | sort: PropTypes.string 146 | }), 147 | colors: PropTypes.arrayOf(PropTypes.string), 148 | colorMapping: PropTypes.objectOf(PropTypes.string), 149 | labelMapping: PropTypes.objectOf(PropTypes.string), 150 | labelMappingRegExp: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), 151 | labelMappingDimension: PropTypes.string, 152 | errorMapping: PropTypes.objectOf(PropTypes.string), 153 | showErrorMessages: PropTypes.bool, 154 | labels: PropTypes.arrayOf(PropTypes.string), 155 | sortGroups: PropTypes.string, 156 | sortIntervals: PropTypes.string, 157 | stacking: PropTypes.string, 158 | table: PropTypes.shape({ 159 | columns: PropTypes.arrayOf(PropTypes.string), 160 | pagination: PropTypes.shape({ 161 | limit: PropTypes.number 162 | }), 163 | mapValues: PropTypes.func 164 | }), 165 | renderOnVisible: PropTypes.bool, 166 | results: PropTypes.any, 167 | previousResults: PropTypes.shape({ 168 | result: PropTypes.number 169 | }), 170 | funnel: PropTypes.shape({ 171 | lines: PropTypes.bool, 172 | resultValues: PropTypes.bool, 173 | percents: PropTypes.shape({ 174 | show: PropTypes.bool, 175 | countingMethod: PropTypes.string, 176 | decimals: PropTypes.number 177 | }), 178 | hover: PropTypes.bool, 179 | marginBetweenSteps: PropTypes.bool, 180 | effect3d: PropTypes.string 181 | }), 182 | stacked: PropTypes.string, 183 | indexBy: PropTypes.string, 184 | library: PropTypes.string, 185 | timezone: PropTypes.string, 186 | padding: PropTypes.shape({ 187 | top: PropTypes.number, 188 | right: PropTypes.number, 189 | bottom: PropTypes.number, 190 | left: PropTypes.number 191 | }), 192 | tooltip: PropTypes.shape({ 193 | show: PropTypes.bool, 194 | grouped: PropTypes.bool, 195 | format: PropTypes.shape({ 196 | title: PropTypes.func, 197 | name: PropTypes.func, 198 | value: PropTypes.func 199 | }), 200 | position: PropTypes.func, 201 | contenss: PropTypes.func 202 | }), 203 | partialIntervalIndicator: PropTypes.shape({ 204 | show: PropTypes.bool, 205 | className: PropTypes.string 206 | }), 207 | showTitle: PropTypes.bool, 208 | notes: PropTypes.string, 209 | axis: PropTypes.object, 210 | color: PropTypes.any, 211 | point: PropTypes.any, 212 | transition: PropTypes.any, 213 | data: PropTypes.any, 214 | grid: PropTypes.any 215 | }; 216 | 217 | Chart.defaultProps = { 218 | theme: 'keen-dataviz', 219 | results: { result: [200, 300, 100, 400, 250] }, 220 | title: false, 221 | type: 'bar', 222 | choropleth: { 223 | map: 'world', 224 | borders: { 225 | show: true, 226 | size: 0.5, 227 | color: '#000' 228 | }, 229 | showSlider: false 230 | }, 231 | heatmap: { 232 | showSlider: false, 233 | simpleTooltip: false 234 | }, 235 | point: { 236 | show: true, 237 | r: 2 238 | }, 239 | legend: { 240 | show: false, 241 | position: 'top', 242 | alignment: 'left', 243 | label: { 244 | textMaxLength: 12 245 | }, 246 | pagination: { 247 | offset: 0, 248 | limit: 5 249 | }, 250 | tooltip: { 251 | show: true, 252 | pointer: true 253 | } 254 | } 255 | }; 256 | -------------------------------------------------------------------------------- /lib/builder/components/ChartContainer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import React, { Component } from 'react'; 4 | import { connect } from 'react-redux'; 5 | import { 6 | loadSavedQuery, 7 | savedQueryError, 8 | setLoading 9 | } from '../../actions/rootActions'; 10 | import Chart from './Chart'; 11 | import CustomChartTheme from './CustomChartTheme'; 12 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 13 | import ChartTypeUtils from '../../func/ChartType'; 14 | import isEqual from 'lodash/isEqual'; 15 | 16 | class ChartContainer extends Component { 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | loading: false, 21 | results: undefined 22 | }; 23 | } 24 | 25 | runSingleSavedQuery = element => { 26 | this.setState({ 27 | loading: true 28 | }); 29 | this.props.keenAnalysis 30 | .query('saved', element.value) 31 | .then(results => { 32 | const newType = ChartTypeUtils.getChartTypeOptions(results.query); 33 | const type = this.props.type ? this.props.type : newType[0]; 34 | this.props.loadSavedQuery(this.props.index); 35 | this.setState({ 36 | results: 37 | this.state.results === undefined 38 | ? { ...results } 39 | : Array.isArray(this.state.results) 40 | ? [...this.state.results, { ...results }] 41 | : type && (type.includes('area') || type.includes('line')) 42 | ? [{ ...this.state.results }, { ...results }] 43 | : { ...results }, 44 | savedQuery: { 45 | value: results.query_name, 46 | label: results.metadata.display_name 47 | }, 48 | type, 49 | loading: false 50 | }); 51 | this.props.setLoading(false); 52 | }) 53 | .catch(err => { 54 | this.setState({ 55 | error: err.body, 56 | loading: false 57 | }); 58 | this.props.setLoading(false); 59 | }); 60 | }; 61 | 62 | runMultiSavedQuery = props => { 63 | this.setState({ 64 | loading: true 65 | }); 66 | const promises = []; 67 | props.savedQuery.map(el => { 68 | promises.push(this.props.keenAnalysis.query('saved', el.value)); 69 | }); 70 | 71 | Promise.all(promises) 72 | .then(results => { 73 | for (let i = 0; i < results.length; i++) { 74 | for (let j = 0; j < results.length; j++) { 75 | if (i === j) { 76 | continue; 77 | } 78 | if ( 79 | results[i].query.analysis_type === 80 | results[j].query.analysis_type && 81 | results[i].query.event_collection === 82 | results[j].query.event_collection 83 | ) { 84 | results[j].query.analysis_type += j; 85 | } 86 | } 87 | } 88 | const newType = ChartTypeUtils.getChartTypeOptions(results[0].query); 89 | const type = newType[0]; 90 | props.loadSavedQuery(props.index); 91 | this.setState({ 92 | results, 93 | type: props.type ? props.type : type, 94 | loading: false 95 | }); 96 | props.setLoading(false); 97 | }) 98 | .catch(err => { 99 | this.setState({ 100 | error: err.body, 101 | loading: false 102 | }); 103 | props.setLoading(false); 104 | }); 105 | }; 106 | 107 | componentDidMount() { 108 | if ( 109 | this.props.savedQuery.length && 110 | (this.props.dryRun === false || this.props.version === 'viewer') 111 | ) { 112 | this.setState({ 113 | loading: true 114 | }); 115 | if (this.props.savedQuery.length > 1) { 116 | this.runMultiSavedQuery(this.props); 117 | } else { 118 | this.runSingleSavedQuery(this.props.savedQuery[0]); 119 | } 120 | } 121 | } 122 | 123 | UNSAFE_componentWillReceiveProps(nextProps) { 124 | if (this.props.id !== nextProps.id) { 125 | this.setState({ 126 | results: undefined, 127 | type: undefined 128 | }); 129 | } 130 | this.props.dryRun !== nextProps.dryRun && 131 | this.props.id === nextProps.id && 132 | this.setState({ results: undefined }); 133 | this.props.id !== nextProps.id && this.setState({ results: undefined }); 134 | isEqual(nextProps.savedQuery, this.props.savedQuery) === false || 135 | (this.props.id !== nextProps.id && this.setState({ results: undefined })); 136 | if ( 137 | this.props.id !== nextProps.id || 138 | (this.props.savedQuery && 139 | nextProps.dryRun === false && 140 | isEqual(nextProps.savedQuery, this.props.savedQuery) === false) || 141 | (this.props.savedQuery === false && 142 | nextProps.savedQuery && 143 | this.props.type !== nextProps.type) || 144 | (this.props.dryRun !== nextProps.dryRun && 145 | nextProps.dryRun === false && 146 | this.props.id === nextProps.id) 147 | ) { 148 | if (nextProps.version === 'viewer' || nextProps.dryRun === false) { 149 | if (nextProps.savedQuery.length > 1) { 150 | this.runMultiSavedQuery(nextProps); 151 | } else { 152 | nextProps.savedQuery.map(el => { 153 | this.runSingleSavedQuery(el); 154 | }); 155 | } 156 | } 157 | } 158 | if (this.props.savedQuery.length !== nextProps.savedQuery.length) { 159 | this.setState({ 160 | results: undefined 161 | }); 162 | } 163 | } 164 | 165 | render() { 166 | const { 167 | index, 168 | isLoading, 169 | dryRun, 170 | version, 171 | error, 172 | template, 173 | id, 174 | charts_theme 175 | } = this.props; 176 | const { loading, results, type, error: stateError } = this.state; 177 | const chartOptions = 178 | charts_theme && 179 | charts_theme[index] !== undefined && 180 | charts_theme[index] !== null 181 | ? charts_theme[index] 182 | : {}; 183 | 184 | const errorMessage = error || stateError; 185 | const stepLabels = 186 | results && 187 | results.metadata && 188 | results.metadata.visualization && 189 | results.metadata.visualization.step_labels; 190 | 191 | return ( 192 | 193 | {Object.keys(chartOptions).length ? ( 194 | 198 | 207 | 208 | ) : ( 209 | 218 | )} 219 | {errorMessage && 220 | typeof errorMessage === 'string' && 221 | dryRun === false && 222 | loading === false && ( 223 |
224 |
{errorMessage}
225 |
226 | )} 227 | {dryRun && version === 'editor' && ( 228 |
{dryRun && 'Dry run'}
229 | )} 230 | {(loading || isLoading === index) && !error && ( 231 |
232 | 233 | 234 | 235 |
236 | )} 237 |
238 | ); 239 | } 240 | } 241 | 242 | const mapStateToProps = state => { 243 | const { 244 | isLoading, 245 | dashboardInfo: { 246 | id, 247 | settings: { dryRun, theme: template = {}, charts_theme = {} } 248 | } 249 | } = state.app; 250 | return { 251 | isLoading, 252 | dryRun, 253 | id, 254 | template, 255 | charts_theme 256 | }; 257 | }; 258 | 259 | const mapDispatchToProps = { 260 | loadSavedQuery, 261 | savedQueryError, 262 | setLoading 263 | }; 264 | 265 | export default connect( 266 | mapStateToProps, 267 | mapDispatchToProps 268 | )(ChartContainer); 269 | -------------------------------------------------------------------------------- /lib/builder/components/CustomChartTheme.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { getStyles } from 'keen-theme-builder'; 3 | 4 | const CustomChartTheme = props => { 5 | const { theme, containerId } = props; 6 | return ( 7 | <> 8 | 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 | changeDashboardView(id.value)} 23 | options={dashboardOptions} 24 | /> 25 |
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 | --------------------------------------------------------------------------------