├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── bin └── iot-dashboard.js ├── codecov.yml ├── contributors.txt ├── deploy-github.sh ├── docs ├── index.md ├── pluginDevelopment.md └── security.md ├── gulpfile.js ├── license.txt ├── package.json ├── plugins ├── DigimondoGpsDatasource.js ├── GoogleMapsWidget.js ├── TestDatasourcePlugin.js ├── TestWidgetPlugin.js └── TestWidgetPlugin2.js ├── src ├── actionNames.js ├── app.css ├── app.ts ├── appState.ts ├── browser-tests.js ├── config.json ├── config.ts ├── dashboard │ ├── dashboard.js │ ├── dashboardMenuEntry.ui.js │ ├── import.js │ └── importExportDialog.ui.js ├── datasource │ ├── datasource.js │ ├── datasourceConfigDialog.ui.js │ ├── datasourceNavItem.ui.js │ ├── datasourcePlugin.js │ ├── datasourcePlugins.js │ ├── datasourcePlugins.test.js │ ├── datasourceWorker.js │ └── plugins │ │ ├── randomDatasource.js │ │ ├── randomDatasource.test.js │ │ └── timeDatasource.js ├── index.html ├── layouts │ ├── layouts.js │ └── layouts.ui.js ├── modal │ ├── modalDialog.js │ ├── modalDialog.ui.js │ └── modalDialogIds.js ├── pageLayout.tsx ├── persistence.js ├── pluginApi │ ├── freeboardDatasource.js │ ├── freeboardPluginApi.js │ ├── pluginApi.js │ ├── pluginCache.js │ ├── pluginNavItem.ui.js │ ├── pluginRegistry.js │ ├── plugins.js │ ├── pluginsDialog.ui.js │ └── uri.test.js ├── renderer.js ├── semanticUiUtil.js ├── serverRenderer.test.ts ├── serverRenderer.tsx ├── store.ts ├── tests.html ├── tests.ts ├── typings │ ├── index.d.ts │ ├── loadjs │ │ └── index.d.ts │ └── react-dom-server │ │ └── index.d.ts ├── ui │ ├── elements.ui.js │ └── settingsForm.ui.js ├── util │ ├── collection.js │ ├── collection.test.js │ ├── formSerializer.js │ ├── reducer.js │ └── uuid.js └── widgets │ ├── plugins │ ├── chartWidget.js │ └── textWidget.js │ ├── widgetConfig.js │ ├── widgetConfigDialog.ui.js │ ├── widgetFrame.ui.js │ ├── widgetGrid.ui.js │ ├── widgetPlugin.js │ ├── widgetPlugins.js │ ├── widgetPlugins.test.js │ ├── widgets.test.ts │ ├── widgets.ts │ ├── widgetsNavItem.ui.js │ └── widthProvider.ui.js ├── tsconfig.json ├── tslint.json ├── typings.json ├── typings ├── globals │ ├── chai │ │ ├── index.d.ts │ │ └── typings.json │ ├── es6-promise │ │ ├── index.d.ts │ │ └── typings.json │ ├── lodash │ │ ├── index.d.ts │ │ └── typings.json │ ├── mocha │ │ ├── index.d.ts │ │ └── typings.json │ ├── object-assign │ │ ├── index.d.ts │ │ └── typings.json │ ├── react-dom │ │ ├── index.d.ts │ │ └── typings.json │ ├── react-redux │ │ ├── index.d.ts │ │ └── typings.json │ ├── react │ │ ├── index.d.ts │ │ └── typings.json │ ├── redux-form │ │ ├── index.d.ts │ │ └── typings.json │ ├── redux-logger │ │ ├── index.d.ts │ │ └── typings.json │ ├── redux-thunk │ │ ├── index.d.ts │ │ └── typings.json │ ├── redux │ │ ├── index.d.ts │ │ └── typings.json │ ├── urijs │ │ ├── index.d.ts │ │ └── typings.json │ └── webpack-env │ │ ├── index.d.ts │ │ └── typings.json └── index.d.ts ├── vendor ├── c3 │ ├── LICENSE │ ├── c3.css │ ├── c3.js │ ├── c3.min.css │ ├── c3.min.js │ └── package.json ├── d3 │ ├── LICENSE │ ├── d3.js │ └── d3.min.js └── sandie.js ├── webpack.browser-tests.js ├── webpack.client.js ├── webpack.config.js └── webpack.tests.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015" 5 | ], 6 | "plugins": [ 7 | "transform-object-rest-spread", 8 | "transform-flow-comments" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | 8 | [{*.js,*.ts}] 9 | indent_style = space 10 | indent_size = 4 11 | 12 | [{*.json,*.yml}] 13 | indent_style = space 14 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "parser": "babel-eslint", 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "experimentalObjectRestSpread": true, 11 | "jsx": true 12 | }, 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "react", 17 | "flowtype" 18 | ], 19 | "rules": { 20 | "indent": [ 21 | "off", 22 | 4, 23 | { 24 | "SwitchCase": 1 25 | } 26 | ], 27 | "linebreak-style": [ 28 | "off", 29 | "windows" 30 | ], 31 | "no-case-declarations": "off", 32 | "one-var": ["error", { 33 | "initialized": "never" 34 | }], 35 | "no-var": "error", 36 | "prefer-const": "error", 37 | // To be considdered: 38 | "no-unused-vars": "off", 39 | "no-console": "off", 40 | "no-undef": "off", 41 | // -- END To be considdered 42 | "quotes": [ 43 | "off", 44 | "double" 45 | ], 46 | "semi": [ 47 | "off", 48 | "always" 49 | ], 50 | "react/prop-types": 2 51 | } 52 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /lib 3 | /node_modules 4 | /coverage 5 | 6 | # IntelliJ Project Files 7 | /.idea 8 | *.iml 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /plugins/ 3 | /sandbox/ 4 | /src/ 5 | /typings/ 6 | /vendor/ 7 | 8 | /.babelrc 9 | /.editorconfig 10 | /.eslintrc.json 11 | /.gitignore 12 | /.travis.yml 13 | /deploy-github.sh 14 | /gulpfile.js 15 | /tsconfig.json 16 | /tslint.json 17 | /typings.json 18 | /webpack.*.js 19 | /iot-dashboard-*.tgz 20 | # IntelliJ Project Files 21 | /.idea/ 22 | *.iml 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | 5 | 6 | # before_script: 7 | 8 | script: 9 | - npm run build --production 10 | 11 | after_success: 12 | - bash deploy-github.sh 13 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Master:** [![Build Status](https://travis-ci.org/Niondir/iot-dashboard.svg?branch=master)](https://travis-ci.org/Niondir/iot-dashboard) [![codecov](https://codecov.io/gh/Niondir/iot-dashboard/branch/master/graph/badge.svg)](https://codecov.io/gh/Niondir/iot-dashboard) [![Dependencies](https://david-dm.org/niondir/iot-dashboard.svg)](https://david-dm.org/niondir/iot-dashboard) [![Dev-Dependencies](https://david-dm.org/niondir/iot-dashboard/dev-status.svg)](https://david-dm.org/niondir/iot-dashboard#info=devDependencies) 2 | 3 | **Dev:** [![Build Status](https://travis-ci.org/Niondir/iot-dashboard.svg?branch=dev)](https://travis-ci.org/Niondir/iot-dashboard) 4 | 5 | For help, questions, feedback, ... try: [![Gitter](https://badges.gitter.im/Niondir/iot-dashboard.svg)](https://gitter.im/Niondir/iot-dashboard?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge) 6 | 7 | # Individual Open Technology - Dashboard 8 | Free Dashboard for your Data 9 | 10 | A generic dashboard application based on JavaScript, HTML and CSS that runs in modern browsers. 11 | Allows to arrange and configure widgets to display data from any datasource. 12 | A Plugin API that allows easy widget and datasource development to keep the dashboard as extensible as possible. 13 | 14 | Can be used as free alternative to [geckoboard](https://www.geckoboard.com), [kibana](https://www.elastic.co/products/kibana), or [freeboard](https://freeboard.io/). 15 | And of course for all other IoT, M2M, Industry 4.0, BigData, whatever dashboards you have to pay for out there. 16 | 17 | --- 18 | 19 | **Not Done Yet** 20 | This Project is still in Development and can not be used for production. Subscribe to get updates. 21 | 22 | The **latest stable version** is on the `master` branch. 23 | The **latest development snapshot** is on the `dev` branch. 24 | 25 | ## Demo ## 26 | 27 | Online: 28 | 29 | * [Live Demo Stable](http://demo.iot-dashboard.org/) of the `master` branch. 30 | * [Live Demo Dev](http://demo.iot-dashboard.org/branch/dev/) of the `dev` branch. 31 | 32 | ## Motivation ## 33 | Why just another Dashboard? 34 | 35 | I was looking for a Dashboard with the following properties: 36 | 37 | - OpenSource, royalty free, with code that I can understand and extend for full customization 38 | - Easy to setup, maintain and extend - even for unusual datasources and widgets 39 | - A Reasonable set of default widgets, to be used out of the box 40 | - Simple API and development setup to write custom widgets and datasources, as a solid base for community driven development and extensions 41 | - Running locally/offline without the need of any server, keeping the server optional until I really need one 42 | - Having a community that extends the Dashboard for their own needs 43 | 44 | If you find something that comes close to the above requirements, please let me know! 45 | 46 | ## Setup ## 47 | 48 | Prerequisite: Download & install [NodeJs](https://nodejs.org) 49 | 50 | ### Install from npm ### 51 | 52 | Install the Dashboard 53 | 54 | npm install -g iot-dashboard 55 | 56 | Start the dashboard server 57 | 58 | iot-dashboard 59 | 60 | Open your browser at http://localhost:8081 61 | 62 | ### Run the Dashboard locally from source ### 63 | 64 | npm install 65 | npm run compile 66 | npm start 67 | 68 | * Dashboard: http://localhost:8081/ 69 | * Tests: http://localhost:8081/tests.html 70 | * Testcoverage: http://localhost:8081/coverage/ 71 | 72 | ### Development ### 73 | 74 | To keep everything simple all important tasks are based on scripts in package.json. Use `npm run ` to run any of them. 75 | 76 | For preparation run 77 | 78 | npm install 79 | 80 | Run the Webpack Server with live-reload and hot module replacement 81 | 82 | npm run dev 83 | 84 | Open your browser at: `http://localhost:8080` and for developing tests: `http://localhost:8080/webpack-dev-server/tests.html` 85 | 86 | Run a second watch task to keep some other files up to date (optional) 87 | See `gulpfile.js` -> `watch` task for details. 88 | 89 | npm run watch 90 | 91 | To make sure all you changes will survive the CI build 92 | 93 | npm run build 94 | 95 | To just run the tests (not enough to survive the CI build!) 96 | 97 | npm test 98 | 99 | Find the coverage report in `dist/coverage` or while the server is running at `http://localhost:8080/coverage/` 100 | 101 | ## Documentation ## 102 | 103 | Check out the [Documentation](https://github.com/Niondir/iot-dashboard/blob/master/docs/index.md) in `/docs` 104 | 105 | ## License ## 106 | The code is available under [Mozilla Public License 2.0](https://www.mozilla.org/en-US/MPL/) (MPL 2.0) 107 | For more information you might want to read the [FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/). 108 | 109 | Contributors have to add a [License Header](https://www.mozilla.org/en-US/MPL/headers/) to new sourcecode files. 110 | 111 | This means you can use and modify the code for private propose (personal or inside your organisation) 112 | Outside of your Organisation you must make modified MPLed code available to your users and comply with all other requirements of the MPL 2.0. 113 | 114 | If you need some of the code available under another license, do not hesitate to **contact me**. 115 | -------------------------------------------------------------------------------- /bin/iot-dashboard.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | var connect = require('connect'); 5 | var serveStatic = require('serve-static'); 6 | var open = require("open"); 7 | var path = require("path"); 8 | 9 | var contentDir = path.join(__dirname, "../dist"); 10 | 11 | connect().use(serveStatic(contentDir)).listen(8081, function(){ 12 | console.log('Serving ' + contentDir); 13 | console.log('Server running on 8081 ...'); 14 | open("http://localhost:8081"); 15 | }); -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: "60...100" 5 | -------------------------------------------------------------------------------- /contributors.txt: -------------------------------------------------------------------------------- 1 | Tobias Kaupat 2 | -------------------------------------------------------------------------------- /deploy-github.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o nounset 3 | 4 | CNAME="demo.iot-dashboard.org" 5 | #BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) 6 | BRANCH_NAME=${TRAVIS_BRANCH} 7 | REV=$(git rev-parse --short HEAD) 8 | 9 | rm -rf _deployment 10 | mkdir _deployment 11 | cd _deployment 12 | 13 | echo "Initializing and configuring git.." 14 | git init 15 | git config user.name "Travis CI" 16 | git config user.email "noreply@iot-dashboard.org" 17 | 18 | echo "Setting upstream and branch..." 19 | git remote add upstream "https://$GH_TOKEN@github.com/niondir/iot-dashboard" 20 | git fetch upstream 21 | git checkout upstream/gh-pages 22 | 23 | echo ${CNAME} > CNAME 24 | 25 | touch . 26 | 27 | echo "Updating ${BRANCH_NAME} folder" 28 | 29 | TARGET_DIR="./branch/${BRANCH_NAME}/" 30 | 31 | git rm -rf --ignore-unmatch ${TARGET_DIR} 32 | 33 | mkdir -p ${TARGET_DIR} 34 | cp -r ../dist/* ${TARGET_DIR} 35 | 36 | echo "Git add, commit and pushing..." 37 | 38 | git add -A . 39 | git commit -m "Deploying branch ${BRANCH_NAME} @ ${REV} to GitHub pages" 40 | git push -q upstream HEAD:gh-pages 41 | 42 | echo "Deployed on http://${CNAME}/branch/${BRANCH_NAME}/" -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # iot-dashboard Documentation 2 | 3 | **Content:** 4 | 5 | * Hosting the iot-dashboard 6 | * Contributing to the dashboard core codebase (this git repo) 7 | * Basic Concepts & Architecture 8 | * Coding Guidelines 9 | * [Plugin Development](pluginDevelopment.md) 10 | * [Security](security.md) related topics 11 | 12 | # Hosting the iot-Dashboard 13 | If you plan to host an own instance of the iot-dashboard please have a look into our [Security](Security) page. A documentation on how to setup your own instance on a server might follow in future and will require some additional work on the Dashboard before. 14 | 15 | # Contributing 16 | All contributions to the core and plugins are very welcome. 17 | 18 | To get started with writing code for the Dashboard core you need a good understanding of the Basic Concepts (see below) and follow the Coding Guidelines (see blow). 19 | If you want to provide another datasource or visualization in form of a new widget checkout the [Plugin Development](pluginDevelopment.md) page. 20 | 21 | # Basic Concepts & Architecture 22 | A basic overview of the concepts and ideas behind the Dashboard. 23 | 24 | Not all Concepts are implemented yet. Not implemented concepts might change in future. 25 | 26 | * **Dashboard:** A `Dashboard` defines `Datasources`, `Widgets` and `Layouts` and can be imported and exported. 27 | * **Layout:** A `Layout` belongs to one `Dashboard` and defines how `Widgets` are arranged. 28 | * **Widget:** A `Widget` can be arranged inside the `Layout` and renders content based on the `WidgetType`, `WidgetProps` and `Datasources`. 29 | * There are several predefined, more and less generic `Widgets` that can be configured and saved as `Widget Blueprints`. 30 | * **Widget Blueprints:** A `Dashboard` can define `Widget Blueprints` which provides an easy way to compose complex layouts with less widget configuration effort. 31 | * **Plugins:** Plugins provide the implementations for `Datasources` and `Widgets`. 32 | * **Datasource:** A `Datasource` provides data for `Widgets` on request. 33 | * **Datasource Type:** A `Datasource Type` defines how a `Datasource` can fetch data, 34 | * e.g. a simple REST datasource, or a more sophisticated for specific services like dweet.io, google docs, etc. 35 | 36 | ## Datasources 37 | 38 | * **DatasourcePlugin:** Can be written by anybody to provide logic that fetches data from anywhere 39 | * **DatasourceInstance:** Can be created by the user based on any `DatasourcePlugin`. Executes the actual data fetching. 40 | * **DatasourceState:** Contains properties defined by the user when a `DatasourceInstance` is created 41 | and is updated regularly with data from the `DatasourceInstance` 42 | * **DatasourceWorker:** Managing the actual updating of the `DatasourceState` based on the `DatasourceInstance` and the current `DatasourceState` 43 | 44 | The following needs way more documentation in future, just a quick start: 45 | 46 | A `DatasourcePlugin` can provide 2 functions: 47 | * `fetchNewValues(): [{value}]` 48 | * `fetchPastValues(since): [{value}]` 49 | * Implementation is optional 50 | * `Value` can be any kind of JSON object. 51 | * Widgets can verify if they are able to display given values 52 | 53 | And a `TYPE_INFO` constant. 54 | 55 | # Coding Guidelines 56 | 57 | Eslint and tslint is in place and must be followed to get successful CI builds. 58 | New code must be tested, unittests can be provided next to the code folder as `.test.ts`, they will be executed automatically during build. 59 | 60 | ## Folder Structure 61 | 62 | * Folders should reflect the business domain not Framework structures 63 | * `root` - Globally used stuff & new stuff that can not be sorted in yet 64 | * `ui` - generic, reusable UI components 65 | * `util` - generic, reusable functions that helps in certain situations 66 | * `typings` - contains custom typings in case the ones managed by the `typings.json` are not sufficient 67 | * *Everything else* - should match to the Basic Concepts (see ab ove) 68 | 69 | ## File Naming 70 | 71 | * `.js` / `.ts` - Business Logic: Actions & Reducers 72 | * `.ui.js` / `.tsx` - React components 73 | * `.test.js` / `.test.ts` - Tests, automatically loaded by gulp inject 74 | 75 | # Webpack Analysis 76 | 77 | Useful to check problems in the Webpack build. 78 | 79 | - Execute: `npm run webpack-profile` 80 | - Goto: https://webpack.github.io/analyse/ 81 | - Load the generated stats.json -------------------------------------------------------------------------------- /docs/security.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | Security should be considered from the very beginning. We like to address some threads and solutions here and explain how they are implemented in the Dashboard. 4 | 5 | **In case you have any security concerns feel free to contact us by creating a Github issue.** 6 | 7 | ## Client side Security 8 | Some security aspects are already considered in the client code of the Dashboard. 9 | 10 | ### Encoding / Escaping 11 | **Thread:** Having a wrong output encoding might lead to XSS, i.e. an attacker could insert a ` 10 | 11 | 12 | Dashboard 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/layouts/layouts.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import * as Widgets from '../widgets/widgets' 6 | import {generate as generateUuid} from '../util/uuid' 7 | import {genCrudReducer} from '../util/reducer' 8 | import {ADD_LAYOUT, LOAD_LAYOUT, UPDATE_LAYOUT, DELETE_LAYOUT, SET_CURRENT_LAYOUT} from '../actionNames' 9 | 10 | const initialLayouts = { 11 | "default": { 12 | id: "default", 13 | name: "Default Layout", 14 | widgets: Widgets.initialWidgets 15 | } 16 | }; 17 | 18 | export function addLayout(name, widgets) { 19 | return (dispatch) => { 20 | 21 | 22 | const addLayout = dispatch({ 23 | type: ADD_LAYOUT, 24 | id: generateUuid(), 25 | name, 26 | widgets 27 | }); 28 | 29 | dispatch(setCurrentLayout(addLayout.id)); 30 | } 31 | 32 | } 33 | 34 | export function updateLayout(id, widgets) { 35 | return { 36 | type: UPDATE_LAYOUT, 37 | id, 38 | widgets 39 | } 40 | } 41 | 42 | 43 | export function deleteLayout(id) { 44 | return { 45 | type: DELETE_LAYOUT, 46 | id 47 | } 48 | } 49 | 50 | export function setCurrentLayout(id) { 51 | return { 52 | type: SET_CURRENT_LAYOUT, 53 | id 54 | } 55 | } 56 | 57 | export function loadEmptyLayout() { 58 | return { 59 | type: LOAD_LAYOUT, 60 | layout: { 61 | id: "empty", 62 | widgets: {} 63 | } 64 | }; 65 | } 66 | 67 | export function loadLayout(id) { 68 | return (dispatch, getState) => { 69 | const state = getState(); 70 | 71 | const layout = state.layouts[id]; 72 | // Bad hack to force the grid layout to update correctly 73 | dispatch(loadEmptyLayout()); 74 | 75 | if (!layout) { 76 | return; 77 | } 78 | setTimeout(()=> { 79 | dispatch(setCurrentLayout(layout.id)); 80 | dispatch({ 81 | type: LOAD_LAYOUT, 82 | layout 83 | }); 84 | }, 0); 85 | } 86 | } 87 | 88 | const layoutCrudReducer = genCrudReducer([ADD_LAYOUT, DELETE_LAYOUT], layout); 89 | export function layouts(state = initialLayouts, action) { 90 | state = layoutCrudReducer(state, action); 91 | switch (action.type) { 92 | default: 93 | return state; 94 | } 95 | } 96 | 97 | export function layout(state, action) { 98 | switch (action.type) { 99 | case ADD_LAYOUT: 100 | return { 101 | id: action.id, 102 | name: action.name, 103 | widgets: action.widgets 104 | }; 105 | case UPDATE_LAYOUT: 106 | return Object.assign({}, state, { 107 | widgets: action.widgets 108 | }); 109 | default: 110 | return state; 111 | } 112 | } 113 | 114 | export function currentLayout(state = {}, action) { 115 | switch (action.type) { 116 | case SET_CURRENT_LAYOUT: 117 | return Object.assign({}, state, { 118 | id: action.id 119 | }); 120 | case DELETE_LAYOUT: 121 | if (action.id == state.id) { 122 | return Object.assign({}, state, { 123 | id: undefined 124 | }) 125 | } 126 | return state; 127 | default: 128 | return state; 129 | } 130 | } -------------------------------------------------------------------------------- /src/layouts/layouts.ui.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import * as React from 'react'; 6 | import {connect} from 'react-redux' 7 | import * as _ from 'lodash' 8 | import * as Layouts from './layouts' 9 | import * as ui from '../ui/elements.ui' 10 | import {PropTypes as Prop} from "react"; 11 | 12 | 13 | /*TODO: Add remove button next to each loadable layout 14 | * - Connect with Actions 15 | * */ 16 | const LayoutsTopNavItem = (props) => { 17 | return
18 | Layout 19 | 20 |
21 | 22 | 23 | 24 |
25 |
Load Layout
26 | 27 | {props.layouts.map(layout => { 28 | return 30 | })} 31 | 32 |
33 |
34 | }; 35 | 36 | LayoutsTopNavItem.propTypes = { 37 | layouts: Prop.arrayOf( 38 | Prop.shape({ 39 | name: Prop.string 40 | }) 41 | ), 42 | widgets: Prop.object, 43 | currentLayout: Prop.object 44 | }; 45 | 46 | export default connect((state) => { 47 | return { 48 | layouts: _.valuesIn(state.layouts), 49 | currentLayout: state.currentLayout, 50 | widgets: state.widgets 51 | } 52 | }, 53 | (dispatch)=> { 54 | return { 55 | } 56 | })(LayoutsTopNavItem); 57 | 58 | class SaveInput extends React.Component { 59 | onEnter(e) { 60 | if (e.key === 'Enter') { 61 | this.props.onEnter(this.refs.input.value, this.props); 62 | this.refs.input.value = ''; 63 | } 64 | } 65 | 66 | render() { 67 | return
68 |
69 | 70 | 71 |
72 |
73 | } 74 | } 75 | 76 | SaveInput.propTypes = { 77 | onEnter: Prop.func, 78 | widgets: Prop.object 79 | }; 80 | 81 | const SaveLayout = connect((state) => { 82 | return { 83 | layouts: _.valuesIn(state.layouts), 84 | widgets: state.widgets 85 | } 86 | }, 87 | (dispatch, props)=> { 88 | return { 89 | onEnter: (name, props) => { 90 | dispatch(Layouts.addLayout(name, props.widgets)) 91 | } 92 | } 93 | } 94 | )(SaveInput); 95 | 96 | class MyLayoutItem extends React.Component { 97 | render() { 98 | const props = this.props; 99 | 100 | let indexIconClass = null; 101 | if (props.currentLayout.id == props.layout.id) { 102 | indexIconClass = "tiny selected radio icon"; 103 | } 104 | else { 105 | indexIconClass = "tiny radio icon"; 106 | } 107 | 108 | return props.onClick(props)}> 109 | 110 | { 111 | props.deleteLayout(props); 112 | e.stopPropagation(); 113 | }}/> {props.text} 114 | ; 115 | } 116 | } 117 | 118 | MyLayoutItem.propTypes = { 119 | deleteLayout: Prop.func.isRequired, 120 | onClick: Prop.func.isRequired, 121 | layout: Prop.object.isRequired, 122 | currentLayout: Prop.object 123 | }; 124 | 125 | const LayoutItem = connect( 126 | (state) => { 127 | return { 128 | currentLayout: state.currentLayout 129 | } 130 | }, 131 | (dispatch, props)=> { 132 | return { 133 | deleteLayout: (props) => dispatch(Layouts.deleteLayout(props.layout.id)), 134 | onClick: (props) => dispatch(Layouts.loadLayout(props.layout.id)) 135 | } 136 | } 137 | )(MyLayoutItem); 138 | 139 | /* 140 | const ResetLayoutButtonc = (props) => { 141 | return 143 | };*/ 144 | 145 | 146 | const ResetLayoutButton = connect( 147 | (state) => { 148 | return { 149 | id: state.currentLayout.id, 150 | disabled: !state.currentLayout.id 151 | } 152 | }, 153 | (dispatch, props)=> { 154 | return { 155 | onClick: (props) => dispatch(Layouts.loadLayout(props.id)) 156 | } 157 | } 158 | )(ui.LinkItem); 159 | 160 | const SaveLayoutButton = connect( 161 | (state) => { 162 | return { 163 | id: state.currentLayout.id, 164 | widgets: state.widgets, 165 | disabled: !state.currentLayout.id 166 | } 167 | }, 168 | (dispatch)=> { 169 | return { 170 | onClick: (props) => dispatch(Layouts.updateLayout(props.id, props.widgets)) 171 | } 172 | } 173 | )(ui.LinkItem); 174 | 175 | -------------------------------------------------------------------------------- /src/modal/modalDialog.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import * as Action from '../actionNames' 6 | import ModalDialog from './modalDialog.ui.js' 7 | 8 | const initialState = { 9 | dialogId: null, 10 | isVisible: false, 11 | data: {} 12 | }; 13 | 14 | function showModalSideeffect(id) { 15 | const $modal = $('.ui.modal.' + id); 16 | 17 | if (!$modal.length) { 18 | throw new Error("Can not find Modal with id", id, $modal); 19 | } 20 | 21 | $modal.modal('show'); 22 | 23 | // This is to update the Browser Scrollbar, at least needed in WebKit 24 | if (typeof document !== 'undefined') { 25 | const n = document.createTextNode(' '); 26 | $modal.append(n); 27 | setTimeout(function () { 28 | n.parentNode.removeChild(n) 29 | }, 0); 30 | } 31 | } 32 | 33 | function closeModalSideeffect(id) { 34 | $('.ui.modal.' + id).modal('hide'); 35 | } 36 | 37 | function updateModalVisibility(stateAfter, stateBefore) { 38 | const dialogBefore = stateBefore.modalDialog; 39 | const dialogAfter = stateAfter.modalDialog; 40 | 41 | if (dialogBefore.isVisible !== dialogAfter.isVisible) { 42 | if (stateAfter.modalDialog.isVisible) { 43 | showModalSideeffect(dialogAfter.dialogId); 44 | } 45 | else { 46 | closeModalSideeffect(dialogBefore.dialogId); 47 | } 48 | } 49 | else if (dialogBefore.dialogId && dialogAfter.dialogId && dialogBefore.dialogId !== dialogAfter.dialogId) { 50 | closeModalSideeffect(dialogBefore.dialogId); 51 | showModalSideeffect(dialogAfter.dialogId); 52 | } 53 | } 54 | 55 | 56 | export function showModal(id, data = {}) { 57 | return (dispatch, getState) => { 58 | const stateBefore = getState(); 59 | dispatch({ 60 | type: Action.SHOW_MODAL, 61 | dialogId: id, 62 | data 63 | }); 64 | 65 | const stateAfter = getState(); 66 | updateModalVisibility(stateAfter, stateBefore); 67 | } 68 | } 69 | 70 | export function closeModal() { 71 | return (dispatch, getState) => { 72 | const stateBefore = getState(); 73 | dispatch({ 74 | type: Action.HIDE_MODAL 75 | }); 76 | 77 | const stateAfter = getState(); 78 | updateModalVisibility(stateAfter, stateBefore); 79 | } 80 | } 81 | 82 | export function modalDialog(state = initialState, action) { 83 | switch (action.type) { 84 | case Action.SHOW_MODAL: 85 | return Object.assign({}, state, { 86 | dialogId: action.dialogId, 87 | data: action.data, 88 | isVisible: true 89 | }); 90 | case Action.HIDE_MODAL: 91 | return Object.assign({}, state, { 92 | dialogId: null, 93 | data: null, 94 | isVisible: false 95 | }); 96 | default: 97 | return state; 98 | } 99 | } -------------------------------------------------------------------------------- /src/modal/modalDialog.ui.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import * as React from 'react' 6 | import {connect} from 'react-redux' 7 | import * as Modal from './modalDialog' 8 | import * as ui from '../ui/elements.ui.js' 9 | import {PropTypes as Prop} from "react"; 10 | 11 | 12 | class ModalDialog extends React.Component { 13 | 14 | componentDidMount() { 15 | $('.ui.modal.' + this.props.id) 16 | .modal({ 17 | detachable: false, 18 | closable: false, 19 | observeChanges: true, 20 | onApprove: ($element) => false, 21 | onDeny: ($element) => false 22 | }) 23 | } 24 | 25 | onClick(e, action) { 26 | if (action.onClick(e)) { 27 | // Closing is done externally (by redux) 28 | this.props.closeDialog(); 29 | //ModalDialog.closeModal(this.props.id); 30 | } 31 | } 32 | 33 | 34 | render() { 35 | let key = 0; 36 | const actions = this.props.actions.map(action => { 37 | return
this.onClick(e, action)}> 38 | {action.label} 39 | {action.iconClass ? : null} 40 |
41 | }); 42 | 43 | const props = this.props; 44 | return
45 |
46 | {props.title} 47 |
48 |
49 | {props.children} 50 |
51 |
52 | {actions} 53 |
54 |
55 | } 56 | } 57 | 58 | ModalDialog.propTypes = { 59 | children: React.PropTypes.element.isRequired, 60 | title: Prop.string.isRequired, 61 | id: Prop.string.isRequired, 62 | actions: Prop.arrayOf( 63 | Prop.shape({ 64 | className: Prop.string.isRequired, 65 | iconClass: Prop.string, 66 | label: Prop.string.isRequired, 67 | onClick: Prop.func.isRequired 68 | }) 69 | ).isRequired, 70 | handlePositive: Prop.func, 71 | handleDeny: Prop.func, 72 | closeDialog: Prop.func 73 | }; 74 | 75 | export default connect( 76 | (state) => { 77 | return {} 78 | }, 79 | (dispatch) => { 80 | return { 81 | closeDialog: () => dispatch(Modal.closeModal()) 82 | } 83 | } 84 | )(ModalDialog) -------------------------------------------------------------------------------- /src/modal/modalDialogIds.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | 6 | 7 | export const DASHBOARD_IMPORT_EXPORT = "dashboard-import-export-dialog"; 8 | export const DATASOURCE_CONFIG = "datasource-config-dialog"; 9 | export const WIDGET_CONFIG = "widget-config-dialog"; 10 | export const PLUGINS = "plugins-dialog"; -------------------------------------------------------------------------------- /src/pageLayout.tsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import * as React from "react"; 6 | import {Component, KeyboardEvent} from "react"; 7 | import * as ReactDOM from "react-dom"; 8 | import {connect} from "react-redux"; 9 | import * as Dashboard from "./dashboard/dashboard.js"; 10 | import WidgetGrid from "./widgets/widgetGrid.ui.js"; 11 | import LayoutsNavItem from "./layouts/layouts.ui.js"; 12 | import WidgetConfigDialog from "./widgets/widgetConfigDialog.ui.js"; 13 | import DashboardMenuEntry from "./dashboard/dashboardMenuEntry.ui.js"; 14 | import ImportExportDialog from "./dashboard/importExportDialog.ui.js"; 15 | import DatasourceConfigDialog from "./datasource/datasourceConfigDialog.ui.js"; 16 | import DatasourceNavItem from "./datasource/datasourceNavItem.ui.js"; 17 | import WidgetsNavItem from "./widgets/widgetsNavItem.ui.js"; 18 | import PluginNavItem from "./pluginApi/pluginNavItem.ui.js"; 19 | import PluginsDialog from "./pluginApi/pluginsDialog.ui.js"; 20 | import * as Persistence from "./persistence.js"; 21 | import {IConfigState} from "./config"; 22 | 23 | interface LayoutProps { 24 | setReadOnly(readOnly: boolean): void; 25 | isReadOnly: boolean; 26 | config: IConfigState; 27 | } 28 | 29 | interface LayoutState { 30 | hover: boolean; 31 | } 32 | 33 | export class Layout extends Component { 34 | 35 | constructor(props: LayoutProps) { 36 | super(props); 37 | this.state = {hover: false}; 38 | } 39 | 40 | onReadOnlyModeKeyPress(e: KeyboardEvent) { 41 | //console.log("key pressed", event.keyCode); 42 | const intKey = (window.event) ? e.which : e.keyCode; 43 | if (intKey === 27) { 44 | this.props.setReadOnly(!this.props.isReadOnly); 45 | } 46 | } 47 | 48 | componentDidMount() { 49 | this.onReadOnlyModeKeyPress = this.onReadOnlyModeKeyPress.bind(this); 50 | 51 | ReactDOM.findDOMNode(this) 52 | .offsetParent 53 | .addEventListener('keydown', this.onReadOnlyModeKeyPress); 54 | } 55 | 56 | render() { 57 | const props = this.props; 58 | 59 | const showMenu = !props.isReadOnly || this.state.hover; 60 | 61 | return
this.onReadOnlyModeKeyPress(event)}> 62 |
63 | 64 | 65 | 66 | 67 |
68 |
69 |
{ this.setState({hover:true})}} 71 | onMouseEnter={()=> {this.setState({hover:true})}} 72 | 73 | >
74 |
{ this.setState({hover:true})}} 76 | onMouseLeave={()=> {this.setState({hover:false})}} 77 | > 78 |
79 | 80 | {/**/} 81 | Dashboard 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | Persistence.clearData()}> 90 | 91 | Reset Everything! 92 | 93 | props.setReadOnly(!props.isReadOnly)}> 94 | {/*expand*/} 95 | 96 |
97 | v{this.props.config.version} {this.props.config.revisionShort} 98 |
99 |
100 | 101 |
102 | 103 | {/* TODO: Use custom classes for everything inside the Grid to make it customizable without breaking semantic-ui */} 104 |
105 | 106 |
107 |
108 |
109 | } 110 | 111 | } 112 | 113 | export default connect( 114 | state => { 115 | return { 116 | isReadOnly: state.dashboard.isReadOnly, 117 | config: state.config 118 | }; 119 | }, 120 | dispatch => { 121 | return { 122 | setReadOnly: (isReadOnly: boolean) => dispatch(Dashboard.setReadOnly(isReadOnly)) 123 | }; 124 | } 125 | )(Layout); -------------------------------------------------------------------------------- /src/persistence.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | 6 | let lastSave = new Date(); 7 | 8 | export function clearData() { 9 | lastSave = new Date(); 10 | if (window.confirm("Wipe app data and reload page?")) { 11 | window.localStorage.setItem("appState", undefined); 12 | location.reload(); 13 | } 14 | } 15 | 16 | export function persistenceMiddleware({getState}) { 17 | return (next) => (action) => { 18 | 19 | const nextState = next(action); 20 | 21 | const now = new Date(); 22 | if (now.getTime() - lastSave.getTime() < 10000) { 23 | return nextState; 24 | } 25 | 26 | saveToLocalStorage(getState()); 27 | console.log('Saved state ...'); 28 | lastSave = new Date(); 29 | return nextState; 30 | } 31 | } 32 | 33 | export function saveToLocalStorage(state) { 34 | if (typeof window === 'undefined') { 35 | console.warn("Can not save to local storage in current environment."); 36 | return; 37 | } 38 | 39 | const savableState = Object.assign({}, state); 40 | 41 | delete savableState.form; 42 | delete savableState.modalDialog; 43 | window.localStorage.setItem("appState", JSON.stringify(savableState)); 44 | } 45 | 46 | 47 | export function loadFromLocalStorage() { 48 | if (typeof window === 'undefined') { 49 | console.warn("Can not load from local storage in current environment."); 50 | return undefined; 51 | } 52 | 53 | const stateString = window.localStorage.getItem("appState"); 54 | let state = undefined; 55 | try { 56 | if (stateString !== undefined && stateString !== "undefined") { 57 | state = JSON.parse(stateString); 58 | } 59 | } 60 | catch (e) { 61 | console.error("Failed to load state from local storage. Data:", stateString, e.message); 62 | } 63 | console.log("Loaded state:", state); 64 | return state !== null ? state : undefined; 65 | } -------------------------------------------------------------------------------- /src/pluginApi/freeboardDatasource.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import loadjs from 'loadjs'; 6 | import * as _ from 'lodash' 7 | 8 | // **newInstance(settings, newInstanceCallback, updateCallback)** (required) : A function that will be called when a new instance of this plugin is requested. 9 | // * **settings** : A javascript object with the initial settings set by the user. The names of the properties in the object will correspond to the setting names defined above. 10 | // * **newInstanceCallback** : A callback function that you'll call when the new instance of the plugin is ready. This function expects a single argument, which is the new instance of your plugin object. 11 | // * **updateCallback** : A callback function that you'll call if and when your datasource has an update for freeboard to recalculate. This function expects a single parameter which is a javascript object with the new, updated data. You should hold on to this reference and call it when needed. 12 | 13 | 14 | export function create(newInstance, TYPE_INFO) { 15 | 16 | return function FreeboardDatasource(newInstance, props = {}, history = []) { 17 | this.instance = null; 18 | this.data = history; 19 | this.getValues = function () { 20 | if (_.isArray(this.data)) { 21 | return this.data; 22 | } 23 | return [this.data]; 24 | }.bind(this); 25 | 26 | this.updateProps = function (newProps) { 27 | console.log("Updating Datasource props"); 28 | this.instance.onSettingsChanged(newProps) 29 | }.bind(this); 30 | 31 | const newInstanceCallback = function (instance) { 32 | this.instance = instance; 33 | instance.updateNow(); 34 | }.bind(this); 35 | 36 | const updateCallback = function (newData) { 37 | this.data = newData; 38 | }.bind(this); 39 | 40 | // TODO: Maybe no needed anymore when we take care of dependencies elsewhere 41 | if (TYPE_INFO.dependencies) { 42 | loadjs([...TYPE_INFO.dependencies], {success: createNewInstance}); 43 | } 44 | else { 45 | createNewInstance(); 46 | } 47 | 48 | function createNewInstance() { 49 | newInstance(props, newInstanceCallback, updateCallback); 50 | } 51 | 52 | }.bind(this, newInstance) 53 | } 54 | -------------------------------------------------------------------------------- /src/pluginApi/freeboardPluginApi.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import * as FreeboardDatasource from './freeboardDatasource' 6 | import * as Plugins from '../pluginApi/plugins' 7 | import * as PluginCache from './pluginCache' 8 | import store from '../store' 9 | 10 | function mapSettings(settings) { 11 | return settings.map(setting => { 12 | return { 13 | id: setting["name"], 14 | name: setting["display_name"], 15 | description: setting["description"], 16 | type: setting["type"], 17 | defaultValue: setting["default_value"], 18 | required: setting["required"] 19 | } 20 | }) 21 | } 22 | 23 | const freeboardPluginApi = { 24 | 25 | /** 26 | * Method to register a DatasourcePlugin as you would with the IoT-Dashboard API 27 | * But supporting the Freeboard syntax 28 | * @param plugin A Freeboard Datasource Plugin. 29 | * See: https://freeboard.github.io/freeboard/docs/plugin_example.html 30 | */ 31 | loadDatasourcePlugin(plugin) { 32 | console.log("Loading freeboard Plugin: ", plugin); 33 | 34 | const typeName = plugin["type_name"]; 35 | const displayName = plugin["display_name"]; 36 | const description = plugin["description"]; 37 | const externalScripts = plugin["external_scripts"]; 38 | const settings = plugin["settings"]; 39 | const newInstance = plugin["newInstance"]; 40 | 41 | const TYPE_INFO = { 42 | type: typeName, 43 | name: displayName, 44 | description: description, 45 | dependencies: externalScripts, 46 | settings: mapSettings(settings) 47 | }; 48 | 49 | const dsPlugin = { 50 | TYPE_INFO, 51 | Datasource: FreeboardDatasource.create(newInstance, TYPE_INFO) 52 | }; 53 | 54 | PluginCache.registerDatasourcePlugin(dsPlugin.TYPE_INFO, dsPlugin.Datasource); 55 | } 56 | 57 | 58 | }; 59 | 60 | window.freeboard = freeboardPluginApi; 61 | 62 | export default freeboardPluginApi; -------------------------------------------------------------------------------- /src/pluginApi/pluginApi.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import * as PluginCache from './pluginCache' 6 | 7 | window.iotDashboardApi = { 8 | registerDatasourcePlugin: PluginCache.registerDatasourcePlugin, 9 | registerWidgetPlugin: PluginCache.registerWidgetPlugin 10 | }; -------------------------------------------------------------------------------- /src/pluginApi/pluginCache.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /** 6 | * When a Plugin is loaded via the UI, an action is called to do so. 7 | * The action will load an external script, containing the plugin code, which calles one of the API methods here. 8 | * By calling the API method the plugin is put to the pluginCache where it can be fetched by the application to initialize it 9 | * 10 | * The application can not call the Plugin since it could (and should) be wrapped into a module. 11 | * @type {null} 12 | */ 13 | 14 | let pluginCache = null; 15 | 16 | 17 | export function popLoadedPlugin() { 18 | const plugin = pluginCache; 19 | pluginCache = null; 20 | return plugin; 21 | } 22 | 23 | export function hasPlugin() { 24 | return pluginCache !== null; 25 | } 26 | 27 | export function registerDatasourcePlugin(typeInfo, Datasource) { 28 | console.assert(!hasPlugin(), "Plugin must be finished loading before another can be registered", pluginCache); 29 | pluginCache = ({ 30 | TYPE_INFO: typeInfo, 31 | Datasource 32 | }); 33 | } 34 | 35 | export function registerWidgetPlugin(typeInfo, Widget) { 36 | console.assert(!hasPlugin(), "Plugin must be finished loading before another can be registered", pluginCache); 37 | pluginCache = ({ 38 | TYPE_INFO: typeInfo, 39 | Widget 40 | }); 41 | } -------------------------------------------------------------------------------- /src/pluginApi/pluginNavItem.ui.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import * as React from 'react' 6 | import {connect} from 'react-redux' 7 | import {reset} from "redux-form"; 8 | import * as ModalIds from '../modal/modalDialogIds' 9 | import * as Modal from '../modal/modalDialog' 10 | import {PropTypes as Prop} from "react"; 11 | 12 | 13 | const PluginsTopNavItem = (props) => { 14 | return props.showPluginsDialog()}> 15 | Plugins 16 | 17 | }; 18 | 19 | PluginsTopNavItem.propTypes = { 20 | showPluginsDialog: Prop.func.isRequired 21 | }; 22 | 23 | 24 | export default connect( 25 | (state) => { 26 | return {} 27 | }, 28 | (dispatch) => { 29 | return { 30 | showPluginsDialog: () => { 31 | dispatch(Modal.showModal(ModalIds.PLUGINS)) 32 | } 33 | } 34 | } 35 | )(PluginsTopNavItem); -------------------------------------------------------------------------------- /src/pluginApi/pluginRegistry.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import * as DsPlugin from '../datasource/datasourcePlugin' 6 | 7 | 8 | export default class PluginRegistry { 9 | 10 | constructor() { 11 | this.plugins = {}; 12 | } 13 | 14 | set store(store) { 15 | this._store = store; 16 | } 17 | 18 | get store() { 19 | return this._store; 20 | } 21 | 22 | register(module) { 23 | if (!this._store === undefined) { 24 | throw new Error("PluginRegistry has no store. Set the store property before registering modules!"); 25 | } 26 | 27 | console.assert(module.TYPE_INFO, "Missing TYPE_INFO on plugin module. Every module must export TYPE_INFO"); 28 | console.assert(module.TYPE_INFO.type, "Missing TYPE_INFO.type on plugin TYPE_INFO."); 29 | 30 | console.log("registering plugin: ", module); 31 | this.plugins[module.TYPE_INFO.type] = this.createPluginFromModule(module); 32 | } 33 | 34 | createPluginFromModule(module) { 35 | throw new Error("PluginRegistry must implement createPluginFromModule"); 36 | } 37 | 38 | getPlugin(type) { 39 | return this.plugins[type]; 40 | } 41 | 42 | 43 | getPlugins() { 44 | return Object.assign({}, this.plugins); 45 | } 46 | } -------------------------------------------------------------------------------- /src/pluginApi/plugins.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import * as Action from "../actionNames"; 6 | import * as DatasourcePlugins from "../datasource/datasourcePlugins"; 7 | import * as WidgetPlugins from "../widgets/widgetPlugins"; 8 | import loadjs from "loadjs"; 9 | import * as PluginCache from "./pluginCache"; 10 | import * as _ from "lodash"; 11 | import URI from "urijs"; 12 | 13 | export function loadPlugin(plugin) { 14 | return addPlugin(plugin); 15 | } 16 | 17 | 18 | export function loadPluginFromUrl(url) { 19 | return function (dispatch) { 20 | loadjs([url], {success: () => onScriptLoaded(url, dispatch)}); 21 | }; 22 | } 23 | 24 | function onScriptLoaded(url, dispatch) { 25 | if (PluginCache.hasPlugin()) { 26 | const plugin = PluginCache.popLoadedPlugin(); 27 | 28 | const dependencies = plugin.TYPE_INFO.dependencies; 29 | if (_.isArray(dependencies) && dependencies.length !== 0) { 30 | 31 | const paths = dependencies.map(dependency => { 32 | return URI(dependency).absoluteTo(url).toString(); 33 | }); 34 | 35 | console.log("Loading Dependencies for Plugin", paths); 36 | 37 | // TODO: Load Plugins into a sandbox / iframe, and pass as "deps" object 38 | // Let's wait for the dependency hell before introducing this. 39 | // Until then we can try to just provide shared libs by the Dashboard, e.g. jQuery, d3, etc. 40 | // That should avoid that people add too many custom libs. 41 | /*sandie([dependencies], 42 | function (deps) { 43 | plugin.deps = deps; 44 | console.log("deps loaded", deps); 45 | dispatch(addPlugin(plugin, url)); 46 | } 47 | ); */ 48 | 49 | 50 | loadjs(paths, { 51 | success: () => { 52 | dispatch(addPlugin(plugin, url)); 53 | } 54 | }); 55 | } 56 | else { 57 | dispatch(addPlugin(plugin, url)); 58 | } 59 | } 60 | else { 61 | console.error("Failed to load Plugin. Make sure it called window.iotDashboardApi.register***Plugin from url " + url); 62 | } 63 | } 64 | 65 | 66 | export function initializeExternalPlugins() { 67 | return (dispatch, getState) => { 68 | const state = getState(); 69 | const plugins = _.valuesIn(state.plugins); 70 | 71 | plugins.filter(pluginState => !_.isEmpty(pluginState.url)).forEach(plugin => { 72 | dispatch(loadPluginFromUrl(plugin.url)); 73 | }) 74 | } 75 | } 76 | 77 | function registerPlugin(plugin) { 78 | const type = plugin.TYPE_INFO.type; 79 | if (plugin.Datasource) { 80 | const dsPlugin = DatasourcePlugins.pluginRegistry.getPlugin(type); 81 | if (!dsPlugin) { 82 | DatasourcePlugins.pluginRegistry.register(plugin); 83 | } 84 | else { 85 | console.warn("Plugin of type " + type + " already loaded:", dsPlugin, ". Tried to load: ", plugin); 86 | } 87 | } 88 | else if (plugin.Widget) { 89 | const widgetPlugin = WidgetPlugins.pluginRegistry.getPlugin(type); 90 | if (!widgetPlugin) { 91 | WidgetPlugins.pluginRegistry.register(plugin); 92 | } 93 | else { 94 | console.warn("Plugin of type " + type + " already loaded:", widgetPlugin, ". Tried to load: ", plugin); 95 | } 96 | } 97 | else { 98 | throw new Error("Plugin neither defines a Datasource nor a Widget.", plugin); 99 | } 100 | } 101 | 102 | // Add plugin to store and register it in the PluginRegistry 103 | export function addPlugin(plugin, url = null) { 104 | console.log("Adding plugin from " + url, plugin); 105 | 106 | return function (dispatch, getState) { 107 | const state = getState(); 108 | const plugins = state.plugins; 109 | 110 | const existentPluginState = _.valuesIn(plugins).find(pluginState => { 111 | return plugin.TYPE_INFO.type === pluginState.pluginType; 112 | }); 113 | 114 | if (existentPluginState) { 115 | registerPlugin(plugin); 116 | return; 117 | } 118 | 119 | let actionType = "unknown-add-widget-action"; 120 | if (plugin.Datasource !== undefined) { 121 | actionType = Action.ADD_DATASOURCE_PLUGIN; 122 | } 123 | if (plugin.Widget !== undefined) { 124 | actionType = Action.ADD_WIDGET_PLUGIN; 125 | } 126 | 127 | // TODO: Just put the raw plugin + url here and let the reducer do the logic 128 | dispatch({ 129 | type: actionType, 130 | id: plugin.TYPE_INFO.type, // needed for crud reducer 131 | typeInfo: plugin.TYPE_INFO, 132 | url 133 | }); 134 | // TODO: Maybe use redux sideeffect and move this call to the reducer 135 | registerPlugin(plugin); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/pluginApi/uri.test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import {assert} from 'chai' 6 | import URI from 'urijs' 7 | 8 | 9 | describe('Uri Tests', function () { 10 | describe('Resolve URIs for plugin loading', function () { 11 | it("Check different uri's on absolute base", function () { 12 | const uri = URI("https://www.domain.de/folder/file.min.js"); 13 | 14 | const dotRelative = "./a/b.js"; 15 | const relative = "a/b.js"; 16 | const absolute = "/a/b.js"; 17 | 18 | assert.equal(uri.toString(), "https://www.domain.de/folder/file.min.js"); 19 | assert.equal(URI(dotRelative).absoluteTo(uri).toString(), "https://www.domain.de/folder/a/b.js"); 20 | assert.equal(URI(relative).absoluteTo(uri).toString(), "https://www.domain.de/folder/a/b.js"); 21 | 22 | 23 | assert.equal(URI(absolute).absoluteTo(uri).toString(), "https://www.domain.de/a/b.js"); 24 | 25 | }); 26 | 27 | it("Check different uri's on relative base", function () { 28 | const uri = "/folder/file.min.js"; 29 | 30 | const dotRelative = "./a/b.js"; 31 | const absolute = "/a/b.js"; 32 | 33 | assert.equal(uri.toString(), "/folder/file.min.js"); 34 | assert.equal(URI(dotRelative).absoluteTo(uri).toString(), "/folder/a/b.js"); 35 | 36 | 37 | assert.equal(URI(absolute).absoluteTo(uri).toString(), "/a/b.js"); 38 | 39 | }); 40 | }); 41 | }); 42 | 43 | -------------------------------------------------------------------------------- /src/renderer.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import * as ReactDOM from 'react-dom' 6 | import {Provider} from "react-redux" 7 | import Layout from "./pageLayout" 8 | 9 | export function render(element, store) { 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | element); 15 | } -------------------------------------------------------------------------------- /src/semanticUiUtil.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const NumbersToString = { 6 | 1: "one", 7 | 2: "two", 8 | 3: "three", 9 | 4: "four", 10 | 5: "five", 11 | 6: "six", 12 | 7: "seven", 13 | 8: "eight", 14 | 9: "nine", 15 | 10: "ten", 16 | 11: "eleven", 17 | 12: "twelve" 18 | }; 19 | 20 | export function numberToClass(number) { 21 | if (!NumbersToString[number]) { 22 | throw new Error("Can not convert number " + number + " to class name."); 23 | } 24 | return NumbersToString[number]; 25 | } -------------------------------------------------------------------------------- /src/serverRenderer.test.ts: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import * as ServerRenderer from './serverRenderer' 6 | import {assert} from 'chai' 7 | import * as store from './store' 8 | 9 | describe('Render Serverside', function () { 10 | describe('render initial state', function () { 11 | it('should return some proper html', function () { 12 | const html = ServerRenderer.render(store.createDefault({log: false})); 13 | 14 | assert.isString(html, "The rendered HTML needs to be a string"); 15 | assert.include(html, '', 'rendered HTML contains at least an closed div'); 17 | 18 | // TODO: Make assumptions about the state in store.getState(); 19 | }); 20 | }); 21 | }); -------------------------------------------------------------------------------- /src/serverRenderer.tsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import {renderToString} from 'react-dom-server' 6 | import {Provider} from 'react-redux' 7 | import Layout from './pageLayout' 8 | import * as React from 'react' //TSC needs a reference to react 9 | import * as Store from './store' 10 | 11 | // Render the component as string 12 | export function render(store: Store.DashboardStore) { 13 | return renderToString( 14 | 15 | 16 | 17 | ) 18 | } -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import * as Redux from "redux"; 6 | import thunk from "redux-thunk"; 7 | import * as createLogger from "redux-logger"; 8 | import * as Widgets from "./widgets/widgets"; 9 | import * as WidgetConfig from "./widgets/widgetConfig.js"; 10 | import * as Layouts from "./layouts/layouts.js"; 11 | import * as Datasource from "./datasource/datasource.js"; 12 | import * as Dashboard from "./dashboard/dashboard.js"; 13 | import * as Import from "./dashboard/import.js"; 14 | import * as Modal from "./modal/modalDialog.js"; 15 | import * as Persist from "./persistence.js"; 16 | import {reducer as formReducer} from "redux-form"; 17 | import * as Action from "./actionNames.js"; 18 | import * as WidgetPlugins from "./widgets/widgetPlugins.js"; 19 | import * as DatasourcePlugins from "./datasource/datasourcePlugins.js"; 20 | import * as AppState from "./appState.ts"; 21 | import * as Config from "./config"; 22 | 23 | export interface DashboardStore extends Redux.Store { 24 | 25 | } 26 | 27 | 28 | const appReducer: AppState.Reducer = Redux.combineReducers({ 29 | config: Config.config, 30 | widgets: Widgets.widgets, 31 | widgetConfig: WidgetConfig.widgetConfigDialog, // TODO: Still used or replaced by modalDialog 32 | layouts: Layouts.layouts, 33 | currentLayout: Layouts.currentLayout, 34 | datasources: Datasource.datasources, 35 | form: formReducer, 36 | modalDialog: Modal.modalDialog, 37 | widgetPlugins: WidgetPlugins.widgetPlugins, 38 | datasourcePlugins: DatasourcePlugins.datasourcePlugins, 39 | dashboard: Dashboard.dashboard 40 | }); 41 | 42 | const reducer: AppState.Reducer = (state: AppState.State, action: Redux.Action) => { 43 | if (action.type === Action.CLEAR_STATE) { 44 | state = undefined 45 | } 46 | 47 | state = Import.importReducer(state, action); 48 | 49 | return appReducer(state, action); 50 | }; 51 | 52 | 53 | const logger = createLogger({ 54 | duration: false, // Print the duration of each action? 55 | timestamp: true, // Print the timestamp with each action? 56 | logErrors: true, // Should the logger catch, log, and re-throw errors? 57 | predicate: (getState, action) => { 58 | if (action.type.startsWith("redux-form")) { 59 | return false; 60 | } 61 | 62 | return !action.doNotLog; 63 | 64 | } 65 | }); 66 | 67 | let globalStore: DashboardStore; 68 | 69 | export function setGlobalStore(store: DashboardStore) { 70 | globalStore = store; 71 | } 72 | 73 | export function get() { 74 | if (!globalStore) { 75 | throw new Error("No global store created. Call setGlobalStore(store) before!"); 76 | } 77 | 78 | return globalStore; 79 | } 80 | 81 | /** 82 | * Create a store as empty as possible 83 | */ 84 | export function createEmpty(options: any = {log: true}) { 85 | return create({ 86 | config: null, 87 | widgets: {}, 88 | datasources: {} 89 | }, options); 90 | } 91 | 92 | /** 93 | * Create a store with default values 94 | */ 95 | export function createDefault(options: any = {log: true}) { 96 | return create(undefined, options); 97 | } 98 | 99 | export function create(initialState: AppState.State, options: any = {log: true}): DashboardStore { 100 | const middleware: Redux.Middleware[] = []; 101 | middleware.push(thunk); 102 | middleware.push(Persist.persistenceMiddleware); 103 | if (options.log) { 104 | middleware.push(logger); // must be last 105 | } 106 | 107 | const store = Redux.createStore( 108 | reducer, 109 | initialState, 110 | Redux.applyMiddleware(...middleware) 111 | ); 112 | 113 | DatasourcePlugins.pluginRegistry.store = store; 114 | WidgetPlugins.pluginRegistry.store = store; 115 | 116 | return store; 117 | } 118 | 119 | export function clearState(): Redux.Action { 120 | return { 121 | type: Action.CLEAR_STATE 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/tests.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | Mocha Tests 9 | 10 | 11 | 12 | 13 |

Mocha Tests

14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/tests.ts: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | 6 | /* inject:tests */ 7 | import './datasource/datasourcePlugins.test.js' 8 | import './datasource/plugins/randomDatasource.test.js' 9 | import './pluginApi/uri.test.js' 10 | import './serverRenderer.test.ts' 11 | import './util/collection.test.js' 12 | import './widgets/widgetPlugins.test.js' 13 | import './widgets/widgets.test.ts' 14 | /* endinject */ 15 | 16 | // TODO: instead of inject we could use require.context 17 | // const testsContext = require.context(".", true, /_test$/); 18 | // testsContext.keys().forEach(testsContext); 19 | -------------------------------------------------------------------------------- /src/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | -------------------------------------------------------------------------------- /src/typings/loadjs/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "loadjs" { 2 | interface Options { 3 | success: () => void; 4 | } 5 | function loadjs(paths: Array, options: Options): void; 6 | 7 | export default loadjs; 8 | } -------------------------------------------------------------------------------- /src/typings/react-dom-server/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-dom-server" { 2 | import DOMServer = __React.__DOMServer; 3 | export = DOMServer; 4 | } -------------------------------------------------------------------------------- /src/ui/elements.ui.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import * as React from 'react'; 6 | import {PropTypes as Prop} from "react"; 7 | 8 | /** 9 | * This module contains generic UI Elements reuse in the app 10 | */ 11 | 12 | export const LinkItem = (props) => { 13 | let icon; 14 | if (props.icon) { 15 | icon = ; 16 | } 17 | 18 | return { 20 | e.stopPropagation(); 21 | e.preventDefault(); 22 | props.onClick(props); 23 | }}>{icon} {props.children} {props.text}; 24 | }; 25 | 26 | LinkItem.propTypes = { 27 | onClick: Prop.func.isRequired, 28 | text: Prop.string, 29 | icon: Prop.string, 30 | disabled: Prop.bool, 31 | children: Prop.any 32 | }; 33 | 34 | export const Icon = (props) => { 35 | const classes = []; 36 | classes.push(props.type); 37 | if (props.align === 'right') { 38 | classes.push('right floated'); 39 | } 40 | if (props.size !== "normal") { 41 | classes.push(props.size); 42 | } 43 | classes.push('icon'); 44 | return 45 | }; 46 | 47 | Icon.propTypes = { 48 | type: Prop.string.isRequired, 49 | onClick: Prop.func, 50 | align: Prop.oneOf(["left", "right"]), 51 | size: Prop.oneOf(["mini", "tiny", "small", "normal", "large", "huge", "massive"]) 52 | }; 53 | 54 | 55 | export const Divider = (props) => { 56 | return
57 | }; -------------------------------------------------------------------------------- /src/ui/settingsForm.ui.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import * as React from 'react' 6 | import {connect} from 'react-redux' 7 | import * as ui from './elements.ui' 8 | import {reduxForm, reset} from 'redux-form'; 9 | import {chunk} from '../util/collection' 10 | import * as _ from 'lodash' 11 | import {PropTypes as Prop} from "react"; 12 | 13 | class SettingsForm extends React.Component { 14 | 15 | componentDidMount() { 16 | this._initSemanticUi(); 17 | } 18 | 19 | componentDidUpdate() { 20 | this._initSemanticUi(); 21 | } 22 | 23 | _initSemanticUi() { 24 | $('.icon.help.circle') 25 | .popup({ 26 | position: "top left", 27 | offset: -10 28 | }); 29 | $('.ui.checkbox') 30 | .checkbox(); 31 | } 32 | 33 | render() { 34 | const props = this.props; 35 | const fields = props.fields; 36 | 37 | return
38 | {/*className="two fields" with chunk size of 2*/} 39 | { 40 | chunk(this.props.settings, 1).map(chunk => { 41 | return
42 | {chunk.map(setting => { 43 | return ; 44 | })} 45 |
46 | }) 47 | } 48 | 49 |
50 | } 51 | } 52 | 53 | SettingsForm.propTypes = { 54 | settings: Prop.arrayOf(Prop.shape({ 55 | id: Prop.string.isRequired, 56 | type: Prop.string.isRequired, 57 | name: Prop.string.isRequired 58 | } 59 | )).isRequired 60 | }; 61 | 62 | export default reduxForm({})(SettingsForm); 63 | 64 | function Field(props) { 65 | return
66 | 70 | 71 |
72 | } 73 | 74 | Field.propTypes = { 75 | field: Prop.object.isRequired, // redux-form field info 76 | name: Prop.string.isRequired, 77 | type: Prop.string.isRequired, 78 | description: Prop.string 79 | }; 80 | 81 | 82 | function SettingsInput(props) { 83 | switch (props.type) { 84 | case "text": 85 | return