├── .codeclimate.yml ├── .eslintrc ├── .gitignore ├── .prettierrc.js ├── GNUmakefile ├── LICENSE ├── README.md ├── appveyor.yml ├── build ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── dist ├── LICENSE ├── README.md ├── config │ └── template.html ├── datasource │ ├── module.js │ ├── module.js.map │ ├── partials │ │ ├── add_model.html │ │ ├── config.html │ │ ├── edit_model.html │ │ └── query_ctrl.html │ └── plugin.json ├── img │ ├── logo.png │ ├── loudml_grafana_datasource.png │ └── loudml_grafana_panel.png ├── index.html ├── module.js ├── module.js.map ├── panel │ ├── module.js │ ├── module.js.map │ └── plugin.json └── plugin.json ├── docs ├── README.md ├── _config.yml ├── loudml_annotations.png ├── loudml_grafana_app.png ├── loudml_grafana_datasource.png ├── loudml_grafana_panel.png └── loudml_props.png ├── grafana-loudml-app-1.1.0.zip ├── grafana-loudml-app-1.2.0.zip ├── grafana-loudml-app-1.3.0.zip ├── jest.config.js ├── loudml-grafana-app-1.4.0-grafana7.zip ├── loudml-grafana-app-1.4.0.zip ├── loudml-grafana-app-1.6.0-grafana6.zip ├── loudml-grafana-app-1.7.0dev-grafana6.zip ├── loudml-grafana-app-1.7.1.zip ├── loudml-grafana-app-1.7.2.zip ├── package.json ├── src ├── config │ ├── config_ctrl.ts │ └── template.html ├── datasource │ ├── config_ctrl.ts │ ├── datasource.ts │ ├── loudml_api.ts │ ├── module.ts │ ├── partials │ │ ├── add_job.html │ │ ├── add_model.html │ │ ├── config.html │ │ ├── edit_model.html │ │ └── query_ctrl.html │ ├── plugin.json │ ├── query_ctrl.ts │ └── types.ts ├── img │ ├── logo.png │ ├── loudml_grafana_datasource.png │ └── loudml_grafana_panel.png ├── module.ts ├── module_raw.d.ts ├── panel │ ├── Graph2.tsx │ ├── GraphLegendEditor.tsx │ ├── GraphPanel.tsx │ ├── GraphPanelController.tsx │ ├── GraphPanelEditor.tsx │ ├── GraphWithLegend2.tsx │ ├── extractors.ts │ ├── getGraphSeriesModel.ts │ ├── module.test.ts │ ├── module.ts │ ├── plugin.json │ └── types.ts ├── plugin.json └── utils.ts ├── tsconfig.json └── yarn.lock /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | exclude_patterns: 2 | - "dist" 3 | - "**/node_modules/" 4 | - "**/*.d.ts" 5 | - "src/libs/" 6 | - "src/images/" 7 | - "src/img/" 8 | - "src/screenshots/" 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@grafana/eslint-config"], 3 | "root": true, 4 | "overrides": [ 5 | { 6 | "files": ["packages/**/*.{ts,tsx}", "public/app/**/*.{ts,tsx}"], 7 | "rules": { 8 | "react-hooks/rules-of-hooks": "off", 9 | "react-hooks/exhaustive-deps": "off" 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | node_modules/ 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Compiled binary addons (https://nodejs.org/api/addons.html) 23 | dist/ 24 | artifacts/ 25 | work/ 26 | ci/ 27 | e2e-results/ 28 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("./node_modules/@grafana/toolkit/src/config/prettier.plugin.config.json"), 3 | }; 4 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | default: run 2 | 3 | build: 4 | mkdir loudml-grafana-app 5 | rsync -rpE dist/ loudml-grafana-app/ 6 | zip -y -r -q loudml-grafana-app-1.7.2.zip loudml-grafana-app 7 | rm -rf loudml-grafana-app 8 | 9 | run: 10 | yarn watch 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Volodymyr Sergeyev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LoudML Grafana Application 2 | 3 | Visualization panel and datasource for Grafana 6.7.x - 7.x to connect with Loud ML AI solution for ICT and IoT 4 | automation. https://loudml.io 5 | 6 | ![LoudML Panel in Grafana](https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/master/docs/loudml_grafana_panel.png) 7 | 8 | ![LoudML Datasource in Grafana](https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/master/docs/loudml_grafana_datasource.png) 9 | 10 | Loud ML is an open source inference engine for metrics and events, and the fastest way to embed machine learning in your time series application. This includes APIs for storing and querying data, processing it in the background for ML or detecting outliers for alerting purposes, and more. 11 | https://github.com/regel/loudml 12 | 13 | # Installation 14 | 15 | Repository conventions: 16 | 17 | * `master` branch is for Grafana 7 18 | * `grafana/6.x` branch is for Grafana 6 19 | 20 | 21 | ZIP files has packaged plugin for each of Grafana version supported. 22 | 23 | A) Give it a try with Docker 24 | 25 | docker run -d \ 26 | -p 3000:3000 \ 27 | --name=grafana \ 28 | -e "GF_INSTALL_PLUGINS=https://github.com/vsergeyev/loudml-grafana-app/raw/master/loudml-grafana-app-latest.zip;loudml-grafana-app" \ 29 | grafana/grafana 30 | 31 | Setup LoudML if needed (please refer to https://hub.docker.com/r/loudml/loudml for config.yml setup) 32 | 33 | docker run -p 8077:8077 \ 34 | -v $PWD/lib/loudml:/var/lib/loudml:rw \ 35 | -v $PWD/config.yml:/etc/loudml/config.yml:ro \ 36 | -ti \ 37 | loudml/loudml 38 | 39 | B) In existing Grafana container 40 | 41 | * Connect to your Grafana server if necessary (e.g. via SSH). 42 | * Go to plugins directory (usually data/plugins under Grafana installation or /var/lib/grafana/plugins) 43 | 44 | cd /var/lib/grafana/plugins 45 | * Download loudml-grafana-app-latest.zip zip file: 46 | 47 | wget https://github.com/vsergeyev/loudml-grafana-app/raw/master/loudml-grafana-app-latest.zip 48 | * Unpack it there 49 | 50 | unzip loudml-grafana-app-latest.zip 51 | * You may remove the downloaded archive 52 | * Restart Grafana 53 | 54 | C) From sources (note - default `master` branch is for Grafana 7.x) 55 | 56 | * Plugin should be placed in `.../grafana/data/plugins` 57 | * git clone https://github.com/vsergeyev/loudml-grafana-app.git 58 | * cd loudml-grafana-app 59 | * yarn 60 | * yarn dev --watch 61 | * restart Grafana 62 | * LoudML app should be in plugins list, you may need to activate it 63 | * enjoy :) 64 | 65 | # Whats inside 66 | 67 | Loud ML Panel - is a version of Grafana's default Graph Panel with a "Create Baseline" button 68 | to create ML model in 1-click. 69 | 70 | Currently 1-click ML button ("Create Baseline") can produce model from: 71 | 72 | * InfluxDB datasource 73 | * OpenTSDB datasource 74 | * Elasticsearch datasource (beta) 75 | * Prometheus datasource (very draft) 76 | 77 | Loud ML Datasource - is a connector to Loud ML server. It has capabilities to show models and jobs on server. You can add new and edit existing models. 78 | 79 | # Prerequisites 80 | 81 | * Loud ML server https://github.com/regel/loudml 82 | * Grafana >= 5.4.0 83 | 84 | # Configuration 85 | 86 | In order to use Loud ML with Grafana you need to have a buckets in **loudml.yml** to reflect Grafana datasource(s) used in LoudML Graph 87 | 88 | ![LoudML Panel Configuration in Grafana](https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/master/docs/loudml_props.png) 89 | 90 | Example: I have InfluxDB datasource with **telegraf** database as an input and will use **loudml** database as output for ML model predictions/forecasting/anomalies: 91 | 92 | buckets: 93 | - name: loudml 94 | type: influxdb 95 | addr: 127.0.0.1:8086 96 | database: loudml 97 | retention_policy: autogen 98 | measurement: loudml 99 | annotation_db: loudmlannotations 100 | - name: influxdb1 101 | type: influxdb 102 | addr: 127.0.0.1:8086 103 | database: telegraf 104 | retention_policy: autogen 105 | measurement: loudml 106 | - name: data 107 | type: influxdb 108 | addr: 127.0.0.1:8086 109 | database: data 110 | retention_policy: autogen 111 | measurement: sinus 112 | - name: opentsdb1 113 | type: opentsdb 114 | addr: 127.0.0.1:4242 115 | retention_policy: autogen 116 | - name: prom1 117 | type: prometheus 118 | addr: 127.0.0.1:9090 119 | retention_policy: autogen 120 | 121 | InfluxDB **loudmlannotations** here specified to store annotations. (By default Loud ML server will store annotations in **chronograf** database). So on Grafana dashboard annotations/anomalies from Loud ML should be configured as: 122 | 123 | SELECT "text" FROM "autogen"."annotations" WHERE $timeFilter 124 | 125 | ![LoudML Annotations in Grafana](https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/master/docs/loudml_annotations.png) 126 | 127 | # Support 128 | 129 | Please post issue to tracker or contact me via vova.sergeyev at gmail.com 130 | 131 | # Changelog 132 | 133 | * 1.7.2 Fixed compatibility issue with Grafana 7.x 134 | * 1.7.0dev Fixed issue with updating model on a server (#19). Fixed datasource config page in Grafana 7.x (#12). 135 | * 1.6.0 Better Grafana 6.x compatibility. Fixed issue with output bucket. 136 | * 1.5.0 Added capability to add and edit models on Loud ML Datasource page. 137 | * 1.4.0 Changed ID to correct format "loudml-grafana-app"; Fixes code style follow guidelines. 138 | * 1.3.0 Fixed issue #5 with fill(0); New capabilities: multiple metrics/features per ML model (for InfluxDB data). 139 | * 1.2.0 New capabilities: LoudML datasource - add scheduled job; list of scheduled jobs. 140 | * 1.1.0 Initial public release 141 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Test against the latest version of this Node.js version 2 | environment: 3 | nodejs_version: "12" 4 | 5 | # Local NPM Modules 6 | cache: 7 | - node_modules 8 | 9 | # Install scripts. (runs after repo cloning) 10 | install: 11 | # Get the latest stable version of Node.js or io.js 12 | - ps: Install-Product node $env:nodejs_version 13 | # install modules 14 | - npm install -g yarn --quiet 15 | - yarn install --pure-lockfile 16 | 17 | # Post-install test scripts. 18 | test_script: 19 | # Output useful info for debugging. 20 | - node --version 21 | - npm --version 22 | 23 | # Run the build 24 | build_script: 25 | - yarn dev # This will also run prettier! 26 | - yarn build # make sure both scripts work 27 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | 5 | function resolve(dir) { 6 | return path.join(__dirname, '..', dir) 7 | } 8 | 9 | module.exports = { 10 | target: 'node', 11 | context: resolve('src'), 12 | entry: { 13 | './module': './module.ts', 14 | './panel/module': './panel/module.ts', 15 | './datasource/module': './datasource/module.ts' 16 | }, 17 | output: { 18 | filename: '[name].js', 19 | path: resolve('dist'), 20 | libraryTarget: 'amd', 21 | publicPath: '/' 22 | }, 23 | externals: [ 24 | 'lodash', 25 | 'jquery', 26 | 'moment', 27 | 'slate', 28 | 'emotion', 29 | 'prismjs', 30 | 'slate-plain-serializer', 31 | '@grafana/slate-react', 32 | 'react', 33 | 'react-dom', 34 | 'react-redux', 35 | 'redux', 36 | 'rxjs', 37 | 'd3', 38 | 'angular', 39 | '@grafana/ui', 40 | '@grafana/runtime', 41 | '@grafana/data', 42 | function(context, request, callback) { 43 | var prefix = 'grafana/'; 44 | if (request.indexOf(prefix) === 0) { 45 | return callback(null, request.substr(prefix.length)); 46 | } 47 | callback(); 48 | } 49 | ], 50 | // stats: 'none', 51 | plugins: [ 52 | new webpack.optimize.OccurrenceOrderPlugin(), 53 | new CopyWebpackPlugin([ 54 | { from: 'plugin.json' }, 55 | { from: 'img/*' }, 56 | { from: 'panel/plugin.json' }, 57 | { from: 'datasource/plugin.json' }, 58 | ]) 59 | ], 60 | resolve: { 61 | extensions: ['.ts', '.tsx', '.js'], 62 | }, 63 | module: { 64 | rules: [ 65 | { 66 | test: /\.tsx?$/, 67 | loaders: [ 68 | 'ts-loader' 69 | ], 70 | exclude: /node_modules/, 71 | }, 72 | { 73 | test: /\.html$/, 74 | use: 'raw-loader' 75 | }, 76 | { 77 | test: /jquery\.flot\.(?!events)/, 78 | loaders: [ 79 | 'imports-loader?jQuery=jquery' 80 | ] 81 | }, 82 | { 83 | test: /jquery\.flot\.events/, 84 | loaders: [ 85 | 'imports-loader?jQuery=jquery,lodash=lodash,angular=angular,tetherDrop=tether-drop' 86 | ] 87 | } 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | const baseWebpackConfig = require('./webpack.base.conf'); 2 | 3 | var conf = baseWebpackConfig; 4 | conf.devtool = "source-map"; 5 | conf.watch = true; 6 | conf.mode = 'development'; 7 | 8 | module.exports = conf; 9 | 10 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | const baseWebpackConfig = require('./webpack.base.conf'); 2 | 3 | var conf = baseWebpackConfig; 4 | conf.mode = 'development'; 5 | 6 | module.exports = baseWebpackConfig; 7 | -------------------------------------------------------------------------------- /dist/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Volodymyr Sergeyev 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 | -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- 1 | # LoudML Grafana Application 2 | 3 | Visualization panel and datasource for Grafana 6.7.x - 7.x to connect with Loud ML AI solution for ICT and IoT 4 | automation. https://loudml.io 5 | 6 | ![LoudML Panel in Grafana](https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/master/docs/loudml_grafana_panel.png) 7 | 8 | ![LoudML Datasource in Grafana](https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/master/docs/loudml_grafana_datasource.png) 9 | 10 | Loud ML is an open source inference engine for metrics and events, and the fastest way to embed machine learning in your time series application. This includes APIs for storing and querying data, processing it in the background for ML or detecting outliers for alerting purposes, and more. 11 | https://github.com/regel/loudml 12 | 13 | # Installation 14 | 15 | Repository conventions: 16 | 17 | * `master` branch is for Grafana 7 18 | * `grafana/6.x` branch is for Grafana 6 19 | 20 | 21 | ZIP files has packaged plugin for each of Grafana version supported. 22 | 23 | A) Give it a try with Docker 24 | 25 | docker run -d \ 26 | -p 3000:3000 \ 27 | --name=grafana \ 28 | -e "GF_INSTALL_PLUGINS=https://github.com/vsergeyev/loudml-grafana-app/raw/master/loudml-grafana-app-latest.zip;loudml-grafana-app" \ 29 | grafana/grafana 30 | 31 | Setup LoudML if needed (please refer to https://hub.docker.com/r/loudml/loudml for config.yml setup) 32 | 33 | docker run -p 8077:8077 \ 34 | -v $PWD/lib/loudml:/var/lib/loudml:rw \ 35 | -v $PWD/config.yml:/etc/loudml/config.yml:ro \ 36 | -ti \ 37 | loudml/loudml 38 | 39 | B) In existing Grafana container 40 | 41 | * Connect to your Grafana server if necessary (e.g. via SSH). 42 | * Go to plugins directory (usually data/plugins under Grafana installation or /var/lib/grafana/plugins) 43 | 44 | cd /var/lib/grafana/plugins 45 | * Download loudml-grafana-app-latest.zip zip file: 46 | 47 | wget https://github.com/vsergeyev/loudml-grafana-app/raw/master/loudml-grafana-app-latest.zip 48 | * Unpack it there 49 | 50 | unzip loudml-grafana-app-latest.zip 51 | * You may remove the downloaded archive 52 | * Restart Grafana 53 | 54 | C) From sources (note - default `master` branch is for Grafana 7.x) 55 | 56 | * Plugin should be placed in `.../grafana/data/plugins` 57 | * git clone https://github.com/vsergeyev/loudml-grafana-app.git 58 | * cd loudml-grafana-app 59 | * yarn 60 | * yarn dev --watch 61 | * restart Grafana 62 | * LoudML app should be in plugins list, you may need to activate it 63 | * enjoy :) 64 | 65 | # Whats inside 66 | 67 | Loud ML Panel - is a version of Grafana's default Graph Panel with a "Create Baseline" button 68 | to create ML model in 1-click. 69 | 70 | Currently 1-click ML button ("Create Baseline") can produce model from: 71 | 72 | * InfluxDB datasource 73 | * OpenTSDB datasource 74 | * Elasticsearch datasource (beta) 75 | * Prometheus datasource (very draft) 76 | 77 | Loud ML Datasource - is a connector to Loud ML server. It has capabilities to show models and jobs on server. You can add new and edit existing models. 78 | 79 | # Prerequisites 80 | 81 | * Loud ML server https://github.com/regel/loudml 82 | * Grafana >= 5.4.0 83 | 84 | # Configuration 85 | 86 | In order to use Loud ML with Grafana you need to have a buckets in **loudml.yml** to reflect Grafana datasource(s) used in LoudML Graph 87 | 88 | ![LoudML Panel Configuration in Grafana](https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/master/docs/loudml_props.png) 89 | 90 | Example: I have InfluxDB datasource with **telegraf** database as an input and will use **loudml** database as output for ML model predictions/forecasting/anomalies: 91 | 92 | buckets: 93 | - name: loudml 94 | type: influxdb 95 | addr: 127.0.0.1:8086 96 | database: loudml 97 | retention_policy: autogen 98 | measurement: loudml 99 | annotation_db: loudmlannotations 100 | - name: influxdb1 101 | type: influxdb 102 | addr: 127.0.0.1:8086 103 | database: telegraf 104 | retention_policy: autogen 105 | measurement: loudml 106 | - name: data 107 | type: influxdb 108 | addr: 127.0.0.1:8086 109 | database: data 110 | retention_policy: autogen 111 | measurement: sinus 112 | - name: opentsdb1 113 | type: opentsdb 114 | addr: 127.0.0.1:4242 115 | retention_policy: autogen 116 | - name: prom1 117 | type: prometheus 118 | addr: 127.0.0.1:9090 119 | retention_policy: autogen 120 | 121 | InfluxDB **loudmlannotations** here specified to store annotations. (By default Loud ML server will store annotations in **chronograf** database). So on Grafana dashboard annotations/anomalies from Loud ML should be configured as: 122 | 123 | SELECT "text" FROM "autogen"."annotations" WHERE $timeFilter 124 | 125 | ![LoudML Annotations in Grafana](https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/master/docs/loudml_annotations.png) 126 | 127 | # Support 128 | 129 | Please post issue to tracker or contact me via vova.sergeyev at gmail.com 130 | 131 | # Changelog 132 | 133 | * 1.7.2 Fixed compatibility issue with Grafana 7.x 134 | * 1.7.0dev Fixed issue with updating model on a server (#19). Fixed datasource config page in Grafana 7.x (#12). 135 | * 1.6.0 Better Grafana 6.x compatibility. Fixed issue with output bucket. 136 | * 1.5.0 Added capability to add and edit models on Loud ML Datasource page. 137 | * 1.4.0 Changed ID to correct format "loudml-grafana-app"; Fixes code style follow guidelines. 138 | * 1.3.0 Fixed issue #5 with fill(0); New capabilities: multiple metrics/features per ML model (for InfluxDB data). 139 | * 1.2.0 New capabilities: LoudML datasource - add scheduled job; list of scheduled jobs. 140 | * 1.1.0 Initial public release 141 | -------------------------------------------------------------------------------- /dist/config/template.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /dist/datasource/partials/add_model.html: -------------------------------------------------------------------------------- 1 | 256 | 257 | -------------------------------------------------------------------------------- /dist/datasource/partials/config.html: -------------------------------------------------------------------------------- 1 |
2 |

HTTP

3 |
4 |
5 |
6 | Loud ML Server URL 7 | 16 | 17 |

Specify a complete Loud ML Server HTTP URL (for example http://your_loudml_server:8077)

18 | 19 | Your access method is Browser, this means the URL 20 | needs to be accessible from the browser. 21 | 22 | 23 | Your access method is Server, this means the URL 24 | needs to be accessible from the grafana backend/server. 25 | 26 |
27 |
28 |
29 | 30 |
31 |
32 | Access 33 |
34 | 39 |
40 |
41 |
42 | 47 |
48 |
49 | 50 |
51 |

52 | Access mode controls how requests to the data source will be handled. 53 | Server access mode should be the preferred way if nothing else stated. 54 |

55 |
Server access mode (Default):
56 |

57 | All requests will be made from the browser to Grafana backend/server which in turn will 58 | forward the requests to the data source and by that circumvent possible 59 | Cross-Origin Resource Sharing (CORS) requirements. 60 | The URL needs to be accessible from the grafana backend/server if you select this access mode. 61 |

62 |
Browser access mode:
63 |

64 | All requests will be made from the browser directly to the data source and may be subject to 65 | Cross-Origin Resource Sharing (CORS) requirements. The URL needs to be accessible from the browser 66 | if you select this access mode. 67 |

68 |
69 | 70 |
71 |
72 | Whitelisted Cookies 73 | 78 | 79 | Grafana Proxy deletes forwarded cookies by default. Specify cookies by name 80 | that should be forwarded to the data source. 81 | 82 |
83 |
84 |
85 |
86 | 87 |
88 |

Manage Machine Learning Tasks

89 |
90 | 94 | 95 | 99 | 100 | 104 |
105 | 106 |
107 |
Models
108 |
109 | 110 | 111 | 112 | 118 | 123 | 132 | 142 | 147 | 148 | 149 |
113 | 114 | 115 | {{model.settings.name}} 116 | 117 | 119 | Running. 120 | Trained. 121 | Not trained. 122 | 124 | 125 | Play 126 | 127 | 128 | 129 | Stop 130 | 131 | 143 | 144 | Delete 145 | 146 |
150 |
151 |
152 |
153 | No models to show. Click refresh to update data from Loud ML server 154 |
155 | 156 |
157 |
Scheduled Jobs
158 |
159 | 160 | 161 | 162 | 168 | 171 | 174 | 182 | 191 | 196 | 197 | 198 |
163 | 164 | 165 | {{job.name}} 166 | 167 | 169 | {{job.method}} 170 | 172 | {{job.relative_url}} 173 | 175 | Every {{job.every.count}} 176 | {{job.every.unit}} 177 | 178 | 179 | at {{job.every.at}} 180 | 181 | 183 | 184 | OK: {{job.ok}}; Result: {{job.status_code}} 185 | 186 |
187 | Error: {{job.error}} 188 |
189 |
190 |
192 | 193 | Delete 194 | 195 |
199 |
200 |
201 |
202 | No scheduled jobs to show. Click refresh to update data from Loud ML server 203 |
204 | 205 |
206 |
Jobs
207 |
208 | 209 | 210 | 211 | 214 | 217 | 220 | 225 | 226 | 227 |
212 | {{job.model}} 213 | 215 | {{job.start_date}} 216 | 218 | {{job.type}} 219 | 221 | {{job.state}} 222 |
223 | {{job.error}} 224 |
228 |
229 |
230 |
231 | No jobs to show. Click refresh to update data from Loud ML server 232 |
233 | 234 |
235 | -------------------------------------------------------------------------------- /dist/datasource/partials/edit_model.html: -------------------------------------------------------------------------------- 1 | 203 | 204 | -------------------------------------------------------------------------------- /dist/datasource/partials/query_ctrl.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/dist/datasource/partials/query_ctrl.html -------------------------------------------------------------------------------- /dist/datasource/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "datasource", 3 | "name": "Loud ML Datasource", 4 | "id": "loudml-datasource", 5 | "metrics": true, 6 | "info": { 7 | "logos": { 8 | "small": "../loudml-grafana-app/img/logo.png", 9 | "large": "../loudml-grafana-app/img/logo.png" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /dist/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/dist/img/logo.png -------------------------------------------------------------------------------- /dist/img/loudml_grafana_datasource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/dist/img/loudml_grafana_datasource.png -------------------------------------------------------------------------------- /dist/img/loudml_grafana_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/dist/img/loudml_grafana_panel.png -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Webpack App 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /dist/module.js: -------------------------------------------------------------------------------- 1 | define(function() { return /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 40 | /******/ } 41 | /******/ }; 42 | /******/ 43 | /******/ // define __esModule on exports 44 | /******/ __webpack_require__.r = function(exports) { 45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 47 | /******/ } 48 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 49 | /******/ }; 50 | /******/ 51 | /******/ // create a fake namespace object 52 | /******/ // mode & 1: value is a module id, require it 53 | /******/ // mode & 2: merge all properties of value into the ns 54 | /******/ // mode & 4: return value when already ns object 55 | /******/ // mode & 8|1: behave like require 56 | /******/ __webpack_require__.t = function(value, mode) { 57 | /******/ if(mode & 1) value = __webpack_require__(value); 58 | /******/ if(mode & 8) return value; 59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 60 | /******/ var ns = Object.create(null); 61 | /******/ __webpack_require__.r(ns); 62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 64 | /******/ return ns; 65 | /******/ }; 66 | /******/ 67 | /******/ // getDefaultExport function for compatibility with non-harmony modules 68 | /******/ __webpack_require__.n = function(module) { 69 | /******/ var getter = module && module.__esModule ? 70 | /******/ function getDefault() { return module['default']; } : 71 | /******/ function getModuleExports() { return module; }; 72 | /******/ __webpack_require__.d(getter, 'a', getter); 73 | /******/ return getter; 74 | /******/ }; 75 | /******/ 76 | /******/ // Object.prototype.hasOwnProperty.call 77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 78 | /******/ 79 | /******/ // __webpack_public_path__ 80 | /******/ __webpack_require__.p = "/"; 81 | /******/ 82 | /******/ 83 | /******/ // Load entry module and return exports 84 | /******/ return __webpack_require__(__webpack_require__.s = "./module.ts"); 85 | /******/ }) 86 | /************************************************************************/ 87 | /******/ ({ 88 | 89 | /***/ "../node_modules/tslib/tslib.es6.js": 90 | /*!******************************************!*\ 91 | !*** ../node_modules/tslib/tslib.es6.js ***! 92 | \******************************************/ 93 | /*! exports provided: __extends, __assign, __rest, __decorate, __param, __metadata, __awaiter, __generator, __createBinding, __exportStar, __values, __read, __spread, __spreadArrays, __await, __asyncGenerator, __asyncDelegator, __asyncValues, __makeTemplateObject, __importStar, __importDefault, __classPrivateFieldGet, __classPrivateFieldSet */ 94 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 95 | 96 | "use strict"; 97 | __webpack_require__.r(__webpack_exports__); 98 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__extends", function() { return __extends; }); 99 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__assign", function() { return __assign; }); 100 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__rest", function() { return __rest; }); 101 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__decorate", function() { return __decorate; }); 102 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__param", function() { return __param; }); 103 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__metadata", function() { return __metadata; }); 104 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__awaiter", function() { return __awaiter; }); 105 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__generator", function() { return __generator; }); 106 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__createBinding", function() { return __createBinding; }); 107 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__exportStar", function() { return __exportStar; }); 108 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__values", function() { return __values; }); 109 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__read", function() { return __read; }); 110 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__spread", function() { return __spread; }); 111 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__spreadArrays", function() { return __spreadArrays; }); 112 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__await", function() { return __await; }); 113 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__asyncGenerator", function() { return __asyncGenerator; }); 114 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__asyncDelegator", function() { return __asyncDelegator; }); 115 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__asyncValues", function() { return __asyncValues; }); 116 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__makeTemplateObject", function() { return __makeTemplateObject; }); 117 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__importStar", function() { return __importStar; }); 118 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__importDefault", function() { return __importDefault; }); 119 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__classPrivateFieldGet", function() { return __classPrivateFieldGet; }); 120 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__classPrivateFieldSet", function() { return __classPrivateFieldSet; }); 121 | /*! ***************************************************************************** 122 | Copyright (c) Microsoft Corporation. 123 | 124 | Permission to use, copy, modify, and/or distribute this software for any 125 | purpose with or without fee is hereby granted. 126 | 127 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 128 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 129 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 130 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 131 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 132 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 133 | PERFORMANCE OF THIS SOFTWARE. 134 | ***************************************************************************** */ 135 | /* global Reflect, Promise */ 136 | 137 | var extendStatics = function(d, b) { 138 | extendStatics = Object.setPrototypeOf || 139 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 140 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 141 | return extendStatics(d, b); 142 | }; 143 | 144 | function __extends(d, b) { 145 | extendStatics(d, b); 146 | function __() { this.constructor = d; } 147 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 148 | } 149 | 150 | var __assign = function() { 151 | __assign = Object.assign || function __assign(t) { 152 | for (var s, i = 1, n = arguments.length; i < n; i++) { 153 | s = arguments[i]; 154 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; 155 | } 156 | return t; 157 | } 158 | return __assign.apply(this, arguments); 159 | } 160 | 161 | function __rest(s, e) { 162 | var t = {}; 163 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) 164 | t[p] = s[p]; 165 | if (s != null && typeof Object.getOwnPropertySymbols === "function") 166 | for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { 167 | if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) 168 | t[p[i]] = s[p[i]]; 169 | } 170 | return t; 171 | } 172 | 173 | function __decorate(decorators, target, key, desc) { 174 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 175 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 176 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 177 | return c > 3 && r && Object.defineProperty(target, key, r), r; 178 | } 179 | 180 | function __param(paramIndex, decorator) { 181 | return function (target, key) { decorator(target, key, paramIndex); } 182 | } 183 | 184 | function __metadata(metadataKey, metadataValue) { 185 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue); 186 | } 187 | 188 | function __awaiter(thisArg, _arguments, P, generator) { 189 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 190 | return new (P || (P = Promise))(function (resolve, reject) { 191 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 192 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 193 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 194 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 195 | }); 196 | } 197 | 198 | function __generator(thisArg, body) { 199 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 200 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 201 | function verb(n) { return function (v) { return step([n, v]); }; } 202 | function step(op) { 203 | if (f) throw new TypeError("Generator is already executing."); 204 | while (_) try { 205 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 206 | if (y = 0, t) op = [op[0] & 2, t.value]; 207 | switch (op[0]) { 208 | case 0: case 1: t = op; break; 209 | case 4: _.label++; return { value: op[1], done: false }; 210 | case 5: _.label++; y = op[1]; op = [0]; continue; 211 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 212 | default: 213 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 214 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 215 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 216 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 217 | if (t[2]) _.ops.pop(); 218 | _.trys.pop(); continue; 219 | } 220 | op = body.call(thisArg, _); 221 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 222 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 223 | } 224 | } 225 | 226 | function __createBinding(o, m, k, k2) { 227 | if (k2 === undefined) k2 = k; 228 | o[k2] = m[k]; 229 | } 230 | 231 | function __exportStar(m, exports) { 232 | for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) exports[p] = m[p]; 233 | } 234 | 235 | function __values(o) { 236 | var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; 237 | if (m) return m.call(o); 238 | if (o && typeof o.length === "number") return { 239 | next: function () { 240 | if (o && i >= o.length) o = void 0; 241 | return { value: o && o[i++], done: !o }; 242 | } 243 | }; 244 | throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); 245 | } 246 | 247 | function __read(o, n) { 248 | var m = typeof Symbol === "function" && o[Symbol.iterator]; 249 | if (!m) return o; 250 | var i = m.call(o), r, ar = [], e; 251 | try { 252 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); 253 | } 254 | catch (error) { e = { error: error }; } 255 | finally { 256 | try { 257 | if (r && !r.done && (m = i["return"])) m.call(i); 258 | } 259 | finally { if (e) throw e.error; } 260 | } 261 | return ar; 262 | } 263 | 264 | function __spread() { 265 | for (var ar = [], i = 0; i < arguments.length; i++) 266 | ar = ar.concat(__read(arguments[i])); 267 | return ar; 268 | } 269 | 270 | function __spreadArrays() { 271 | for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; 272 | for (var r = Array(s), k = 0, i = 0; i < il; i++) 273 | for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) 274 | r[k] = a[j]; 275 | return r; 276 | }; 277 | 278 | function __await(v) { 279 | return this instanceof __await ? (this.v = v, this) : new __await(v); 280 | } 281 | 282 | function __asyncGenerator(thisArg, _arguments, generator) { 283 | if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); 284 | var g = generator.apply(thisArg, _arguments || []), i, q = []; 285 | return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i; 286 | function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; } 287 | function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } 288 | function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } 289 | function fulfill(value) { resume("next", value); } 290 | function reject(value) { resume("throw", value); } 291 | function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } 292 | } 293 | 294 | function __asyncDelegator(o) { 295 | var i, p; 296 | return i = {}, verb("next"), verb("throw", function (e) { throw e; }), verb("return"), i[Symbol.iterator] = function () { return this; }, i; 297 | function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === "return" } : f ? f(v) : v; } : f; } 298 | } 299 | 300 | function __asyncValues(o) { 301 | if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); 302 | var m = o[Symbol.asyncIterator], i; 303 | return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); 304 | function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } 305 | function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } 306 | } 307 | 308 | function __makeTemplateObject(cooked, raw) { 309 | if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } 310 | return cooked; 311 | }; 312 | 313 | function __importStar(mod) { 314 | if (mod && mod.__esModule) return mod; 315 | var result = {}; 316 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 317 | result.default = mod; 318 | return result; 319 | } 320 | 321 | function __importDefault(mod) { 322 | return (mod && mod.__esModule) ? mod : { default: mod }; 323 | } 324 | 325 | function __classPrivateFieldGet(receiver, privateMap) { 326 | if (!privateMap.has(receiver)) { 327 | throw new TypeError("attempted to get private field on non-instance"); 328 | } 329 | return privateMap.get(receiver); 330 | } 331 | 332 | function __classPrivateFieldSet(receiver, privateMap, value) { 333 | if (!privateMap.has(receiver)) { 334 | throw new TypeError("attempted to set private field on non-instance"); 335 | } 336 | privateMap.set(receiver, value); 337 | return value; 338 | } 339 | 340 | 341 | /***/ }), 342 | 343 | /***/ "./config/config_ctrl.ts": 344 | /*!*******************************!*\ 345 | !*** ./config/config_ctrl.ts ***! 346 | \*******************************/ 347 | /*! no static exports found */ 348 | /***/ (function(module, exports, __webpack_require__) { 349 | 350 | "use strict"; 351 | 352 | 353 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 354 | 355 | Object.defineProperty(exports, "__esModule", { 356 | value: true 357 | }); 358 | 359 | var tslib_1 = __webpack_require__(/*! tslib */ "../node_modules/tslib/tslib.es6.js"); 360 | 361 | var template_html_1 = tslib_1.__importDefault(__webpack_require__(/*! ./template.html */ "./config/template.html")); 362 | 363 | var ConfigCtrl = function ConfigCtrl() { 364 | _classCallCheck(this, ConfigCtrl); 365 | 366 | if (this.appModel.jsonData === undefined) { 367 | this.appModel.jsonData = {}; 368 | } 369 | }; 370 | 371 | exports.ConfigCtrl = ConfigCtrl; 372 | ConfigCtrl.template = template_html_1["default"]; 373 | 374 | /***/ }), 375 | 376 | /***/ "./config/template.html": 377 | /*!******************************!*\ 378 | !*** ./config/template.html ***! 379 | \******************************/ 380 | /*! no static exports found */ 381 | /***/ (function(module, exports) { 382 | 383 | module.exports = "\r\n"; 384 | 385 | /***/ }), 386 | 387 | /***/ "./module.ts": 388 | /*!*******************!*\ 389 | !*** ./module.ts ***! 390 | \*******************/ 391 | /*! no static exports found */ 392 | /***/ (function(module, exports, __webpack_require__) { 393 | 394 | "use strict"; 395 | 396 | 397 | Object.defineProperty(exports, "__esModule", { 398 | value: true 399 | }); 400 | 401 | var config_ctrl_1 = __webpack_require__(/*! ./config/config_ctrl */ "./config/config_ctrl.ts"); 402 | 403 | exports.ConfigCtrl = config_ctrl_1.ConfigCtrl; 404 | 405 | /***/ }) 406 | 407 | /******/ })});; 408 | //# sourceMappingURL=module.js.map -------------------------------------------------------------------------------- /dist/module.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///../node_modules/tslib/tslib.es6.js","webpack:///./config/config_ctrl.ts","webpack:///./config/template.html","webpack:///./module.ts"],"names":[],"mappings":";QAAA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;;QAEA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;;;QAGA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA,0CAA0C,gCAAgC;QAC1E;QACA;;QAEA;QACA;QACA;QACA,wDAAwD,kBAAkB;QAC1E;QACA,iDAAiD,cAAc;QAC/D;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA,yCAAyC,iCAAiC;QAC1E,gHAAgH,mBAAmB,EAAE;QACrI;QACA;;QAEA;QACA;QACA;QACA,2BAA2B,0BAA0B,EAAE;QACvD,iCAAiC,eAAe;QAChD;QACA;QACA;;QAEA;QACA,sDAAsD,+DAA+D;;QAErH;QACA;;;QAGA;QACA;;;;;;;;;;;;;AClFA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,UAAU,gBAAgB,sCAAsC,iBAAiB,EAAE;AACnF,yBAAyB,uDAAuD;AAChF;AACA;;AAEO;AACP;AACA,mBAAmB,sBAAsB;AACzC;AACA;;AAEO;AACP;AACA,gDAAgD,OAAO;AACvD;AACA;AACA;AACA;AACA;AACA;AACA;;AAEO;AACP;AACA;AACA;AACA;AACA,4DAA4D,cAAc;AAC1E;AACA;AACA;AACA;AACA;;AAEO;AACP;AACA;AACA,4CAA4C,QAAQ;AACpD;AACA;;AAEO;AACP,mCAAmC,oCAAoC;AACvE;;AAEO;AACP;AACA;;AAEO;AACP,2BAA2B,+DAA+D,gBAAgB,EAAE,EAAE;AAC9G;AACA,mCAAmC,MAAM,6BAA6B,EAAE,YAAY,WAAW,EAAE;AACjG,kCAAkC,MAAM,iCAAiC,EAAE,YAAY,WAAW,EAAE;AACpG,+BAA+B,qFAAqF;AACpH;AACA,KAAK;AACL;;AAEO;AACP,aAAa,6BAA6B,0BAA0B,aAAa,EAAE,qBAAqB;AACxG,gBAAgB,qDAAqD,oEAAoE,aAAa,EAAE;AACxJ,sBAAsB,sBAAsB,qBAAqB,GAAG;AACpE;AACA;AACA;AACA;AACA;AACA;AACA,uCAAuC;AACvC,kCAAkC,SAAS;AAC3C,kCAAkC,WAAW,UAAU;AACvD,yCAAyC,cAAc;AACvD;AACA,6GAA6G,OAAO,UAAU;AAC9H,gFAAgF,iBAAiB,OAAO;AACxG,wDAAwD,gBAAgB,QAAQ,OAAO;AACvF,8CAA8C,gBAAgB,gBAAgB,OAAO;AACrF;AACA,iCAAiC;AACjC;AACA;AACA,SAAS,YAAY,aAAa,OAAO,EAAE,UAAU,WAAW;AAChE,mCAAmC,SAAS;AAC5C;AACA;;AAEO;AACP;AACA;AACA;;AAEO;AACP;AACA;;AAEO;AACP;AACA;AACA;AACA;AACA;AACA,oBAAoB;AACpB;AACA;AACA;AACA;;AAEO;AACP;AACA;AACA;AACA;AACA;AACA;AACA,mBAAmB,MAAM,gBAAgB;AACzC;AACA;AACA;AACA;AACA,iBAAiB,sBAAsB;AACvC;AACA;AACA;;AAEO;AACP,4BAA4B,sBAAsB;AAClD;AACA;AACA;;AAEO;AACP,iDAAiD,QAAQ;AACzD,wCAAwC,QAAQ;AAChD,wDAAwD,QAAQ;AAChE;AACA;AACA;;AAEO;AACP;AACA;;AAEO;AACP;AACA;AACA,iBAAiB,sFAAsF,aAAa,EAAE;AACtH,sBAAsB,gCAAgC,qCAAqC,0CAA0C,EAAE,EAAE,GAAG;AAC5I,2BAA2B,MAAM,eAAe,EAAE,YAAY,oBAAoB,EAAE;AACpF,sBAAsB,oGAAoG;AAC1H,6BAA6B,uBAAuB;AACpD,4BAA4B,wBAAwB;AACpD,2BAA2B,yDAAyD;AACpF;;AAEO;AACP;AACA,iBAAiB,4CAA4C,SAAS,EAAE,qDAAqD,aAAa,EAAE;AAC5I,yBAAyB,6BAA6B,oBAAoB,gDAAgD,gBAAgB,EAAE,KAAK;AACjJ;;AAEO;AACP;AACA;AACA,2GAA2G,sFAAsF,aAAa,EAAE;AAChN,sBAAsB,8BAA8B,gDAAgD,uDAAuD,EAAE,EAAE,GAAG;AAClK,4CAA4C,sCAAsC,UAAU,oBAAoB,EAAE,EAAE,UAAU;AAC9H;;AAEO;AACP,gCAAgC,uCAAuC,aAAa,EAAE,EAAE,OAAO,kBAAkB;AACjH;AACA;;AAEO;AACP;AACA;AACA;AACA;AACA;AACA;;AAEO;AACP,4CAA4C;AAC5C;;AAEO;AACP;AACA;AACA;AACA;AACA;;AAEO;AACP;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;ACzNA;;IAEM,U,GAIJ;AAAA;;AACE,MAAI,KAAK,QAAL,CAAc,QAAd,KAA2B,SAA/B,EAA0C;AACxC,SAAK,QAAL,CAAc,QAAd,GAAyB,EAAzB;AACD;AACF,C;;AAGM;AAVA,sBAAW,0BAAX,C;;;;;;;;;;;ACHT,wB;;;;;;;;;;;;;;;;;;ACAA;;AAES,qBAFA,wBAEA,C","file":"module.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./module.ts\");\n","/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport function __createBinding(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n}\r\n\r\nexport function __exportStar(m, exports) {\r\n for (var p in m) if (p !== \"default\" && !exports.hasOwnProperty(p)) exports[p] = m[p];\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n};\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];\r\n result.default = mod;\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, privateMap) {\r\n if (!privateMap.has(receiver)) {\r\n throw new TypeError(\"attempted to get private field on non-instance\");\r\n }\r\n return privateMap.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, privateMap, value) {\r\n if (!privateMap.has(receiver)) {\r\n throw new TypeError(\"attempted to set private field on non-instance\");\r\n }\r\n privateMap.set(receiver, value);\r\n return value;\r\n}\r\n","import template from './template.html';\r\n\r\nclass ConfigCtrl {\r\n static template = template;\r\n appModel: any;\r\n\r\n constructor() {\r\n if (this.appModel.jsonData === undefined) {\r\n this.appModel.jsonData = {};\r\n }\r\n }\r\n}\r\n\r\nexport { ConfigCtrl };\r\n","module.exports = \"\\r\\n\";","import { ConfigCtrl } from './config/config_ctrl';\r\n\r\nexport { ConfigCtrl };\r\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/panel/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "panel", 3 | "name": "Loud ML Graph", 4 | "id": "loudml-graph-panel", 5 | "info": { 6 | "logos": { 7 | "small": "../loudml-grafana-app/img/logo.png", 8 | "large": "../loudml-grafana-app/img/logo.png" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /dist/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "app", 3 | "name": "Loud ML app", 4 | "id": "loudml-grafana-app", 5 | 6 | "info": { 7 | "description": "Loud ML app: Visualization panel and datasource for Grafana to connect with Loud ML AI", 8 | "author": { 9 | "name": "Volodymyr Sergeyev" 10 | }, 11 | "keywords": ["LoudML", "ML", "AI"], 12 | "logos": { 13 | "small": "img/logo.png", 14 | "large": "img/logo.png" 15 | }, 16 | "links": [ 17 | {"name": "Website", "url": "https://github.com/vsergeyev/loudml-grafana-app"}, 18 | {"name": "License", "url": "https://github.com/vsergeyev/loudml-grafana-app/blob/master/LICENSE"} 19 | ], 20 | "screenshots": [ 21 | {"name": "LoudML graph panel", "path": "img/loudml_grafana_panel.png"}, 22 | {"name": "LoudML datasource", "path": "img/loudml_grafana_datasource.png"} 23 | ], 24 | "version": "1.7.2", 25 | "updated": "2021-02-01" 26 | }, 27 | 28 | "includes": [ 29 | { "type": "panel", "name": "Loud ML Graph Panel" }, 30 | { "type": "datasource", "name": "Loud ML Datasource" } 31 | ], 32 | 33 | "dependencies": { 34 | "grafanaDependency": ">=7.0.0", 35 | "plugins": [] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | LoudML Grafana Application 2 | ========================== 3 | https://github.com/vsergeyev/loudml-grafana-app 4 | 5 | Datasource and Graph panel visualization to connect with Loud ML Machine Learning server. 6 | 7 | ![LoudML Panel in Grafana](loudml_grafana_panel.png) 8 | 9 | Create a ML models in 1-click with "Create Baseline" button on graph. 10 | 11 | * select a time frame 12 | * click "Create Baseline" 13 | * train model 14 | * run a prediction 15 | * observe anomalies (as annotations on graph) 16 | 17 | Currently 1-click ML button ("Create Baseline") can produce model from: 18 | 19 | * InfluxDB datasource 20 | * OpenTSDB datasource 21 | * Elasticsearch datasource (beta) 22 | * Prometheus datasource (very draft) 23 | 24 | Watch this video on YouTube: 25 | 26 | IMAGE ALT TEXT HERE 29 | 30 | Loud ML use a Tensor Flow and Keras as a backend. It works with VAE models, combines the best of unsupervised and supervised learning. Based on a work "Unsupervised Anomaly Detection via Variational Auto-Encoderfor Seasonal KPIs in Web Applications" algorythm is best suitable to closely monitor various KPIs (DB connections, page views, number of online users, number of orders, etc). 31 | 32 | Per ML algorythm documentation: 33 | 34 | `Donut is an unsupervisedanomaly detection algorithm based on VAE. It greatly outperforms a state-of-arts super-vised ensemble approach and a baseline VAE approach, and its best F-scores range from 0.75 to 0.9 for the studied KPIs from a top global Internet company. ... Unlike discriminative models which are designed for just one pur-pose (e.g., a classifier is designed for just computing the classifi-cation probabilityp(y|x)), generative models like VAE can derivevarious outputs. ` 35 | 36 | # Installation 37 | 38 | Default assumption - you use Grafana 6.x. For Grafana 7.x please use appropriate ZIP file. 39 | 40 | A) Give it a try with Docker 41 | 42 | docker run -d \ 43 | -p 3000:3000 \ 44 | --name=grafana \ 45 | -e "GF_INSTALL_PLUGINS=https://github.com/vsergeyev/loudml-grafana-app/raw/master/loudml-grafana-app-latest.zip;loudml-grafana-app" \ 46 | grafana/grafana 47 | 48 | Setup LoudML if needed (please refer to https://hub.docker.com/r/loudml/loudml for config.yml setup) 49 | 50 | docker run -p 8077:8077 \ 51 | -v $PWD/lib/loudml:/var/lib/loudml:rw \ 52 | -v $PWD/config.yml:/etc/loudml/config.yml:ro \ 53 | -ti \ 54 | loudml/loudml 55 | 56 | B) In existing Grafana container 57 | 58 | * Connect to your Grafana server if necessary (e.g. via SSH). 59 | * Go to plugins directory (usually data/plugins under Grafana installation or /var/lib/grafana/plugins) 60 | 61 | cd /var/lib/grafana/plugins 62 | * Download loudml-grafana-app-latest.zip zip file: 63 | 64 | wget https://github.com/vsergeyev/loudml-grafana-app/raw/master/loudml-grafana-app-latest.zip 65 | * Unpack it there 66 | 67 | unzip loudml-grafana-app-latest.zip 68 | * You may remove the downloaded archive 69 | * Restart Grafana 70 | 71 | C) From sources (note - github latest is for Grafana 7.x) 72 | 73 | * Plugin should be placed in `.../grafana/data/plugins` 74 | * git clone https://github.com/vsergeyev/loudml-grafana-app.git 75 | * cd loudml-grafana-app 76 | * yarn 77 | * yarn dev --watch 78 | * restart Grafana 79 | * LoudML app should be in plugins list, you may need to activate it 80 | * enjoy :) 81 | 82 | # Prerequisites 83 | 84 | * Loud ML server https://github.com/regel/loudml 85 | * Grafana >= 5.4.0 86 | 87 | # Configuration 88 | 89 | In order to use Loud ML with Grafana you need to have a buckets in **loudml.yml** to reflect Grafana datasource(s) used in LoudML Graph 90 | 91 | ![LoudML Panel Configuration in Grafana](loudml_props.png) 92 | 93 | Example: I have InfluxDB datasource with **telegraf** database as an input and will use **loudml** database as output for ML model predictions/forecasting/anomalies: 94 | 95 | buckets: 96 | - name: loudml 97 | type: influxdb 98 | addr: 127.0.0.1:8086 99 | database: loudml 100 | retention_policy: autogen 101 | measurement: loudml 102 | annotation_db: loudmlannotations 103 | - name: influxdb1 104 | type: influxdb 105 | addr: 127.0.0.1:8086 106 | database: telegraf 107 | retention_policy: autogen 108 | measurement: loudml 109 | - name: data 110 | type: influxdb 111 | addr: 127.0.0.1:8086 112 | database: data 113 | retention_policy: autogen 114 | measurement: sinus 115 | - name: opentsdb1 116 | type: opentsdb 117 | addr: 127.0.0.1:4242 118 | retention_policy: autogen 119 | - name: prom1 120 | type: prometheus 121 | addr: 127.0.0.1:9090 122 | retention_policy: autogen 123 | 124 | InfluxDB **loudmlannotations** here specified to store annotations. (By default Loud ML server will store annotations in **chronograf** database). So on Grafana dashboard annotations/anomalies from Loud ML should be configured as: 125 | 126 | SELECT "text" FROM "autogen"."annotations" WHERE $timeFilter 127 | 128 | ![LoudML Annotations in Grafana](loudml_annotations.png) 129 | 130 | # Links 131 | 132 | * Creating a model for system usage metric 133 | * Loud ML 134 | * Unsupervised Anomaly Detection via Variational Auto-Encoderfor Seasonal KPIs in Web Applications 135 | * Forecasting time series with 1-click machine learning 136 | * Applying Machine Learning Models to InfluxDB 137 | 138 | # Changelog 139 | 140 | * 1.6.0 Better Grafana 6.x compatibility. Fixed issue with output bucket. 141 | * 1.5.0 Added capability to add and edit models on Loud ML Datasource page. 142 | * 1.4.0 Changed ID to correct format "loudml-grafana-app"; Fixes code style follow guidelines. 143 | * 1.3.0 Fixed issue #5 with fill(0); New capabilities: multiple metrics/features per ML model (for InfluxDB data). 144 | * 1.2.0 New capabilities: LoudML datasource - add scheduled job; list of scheduled jobs. 145 | * 1.1.0 Initial public release 146 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /docs/loudml_annotations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/docs/loudml_annotations.png -------------------------------------------------------------------------------- /docs/loudml_grafana_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/docs/loudml_grafana_app.png -------------------------------------------------------------------------------- /docs/loudml_grafana_datasource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/docs/loudml_grafana_datasource.png -------------------------------------------------------------------------------- /docs/loudml_grafana_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/docs/loudml_grafana_panel.png -------------------------------------------------------------------------------- /docs/loudml_props.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/docs/loudml_props.png -------------------------------------------------------------------------------- /grafana-loudml-app-1.1.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/grafana-loudml-app-1.1.0.zip -------------------------------------------------------------------------------- /grafana-loudml-app-1.2.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/grafana-loudml-app-1.2.0.zip -------------------------------------------------------------------------------- /grafana-loudml-app-1.3.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/grafana-loudml-app-1.3.0.zip -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // This file is needed because it is used by vscode and other tools that 2 | // call `jest` directly. However, unless you are doing anything special 3 | // do not edit this file 4 | 5 | const standard = require('@grafana/toolkit/src/config/jest.plugin.config'); 6 | 7 | // This process will use the same config that `yarn test` is using 8 | module.exports = standard.jestConfig(); 9 | -------------------------------------------------------------------------------- /loudml-grafana-app-1.4.0-grafana7.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/loudml-grafana-app-1.4.0-grafana7.zip -------------------------------------------------------------------------------- /loudml-grafana-app-1.4.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/loudml-grafana-app-1.4.0.zip -------------------------------------------------------------------------------- /loudml-grafana-app-1.6.0-grafana6.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/loudml-grafana-app-1.6.0-grafana6.zip -------------------------------------------------------------------------------- /loudml-grafana-app-1.7.0dev-grafana6.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/loudml-grafana-app-1.7.0dev-grafana6.zip -------------------------------------------------------------------------------- /loudml-grafana-app-1.7.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/loudml-grafana-app-1.7.1.zip -------------------------------------------------------------------------------- /loudml-grafana-app-1.7.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/loudml-grafana-app-1.7.2.zip -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loudml-grafana-app", 3 | "version": "1.7.2", 4 | "description": "Loud ML app: visualization panel for Grafana to connect with Loud ML AI", 5 | "scripts": { 6 | "build": "grafana-toolkit plugin:build", 7 | "test": "grafana-toolkit plugin:test", 8 | "dev": "grafana-toolkit plugin:dev", 9 | "watch": "grafana-toolkit plugin:dev --watch" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/vsergeyev/loudml-grafana-app" 14 | }, 15 | "author": "Volodymyr Sergeyev ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/vsergeyev/loudml-grafana-app/issues", 19 | "email": "vova.sergeyev@gmail.com" 20 | }, 21 | "devDependencies": { 22 | "react": ">=16.8.0", 23 | "react-dom": ">=16.8.0", 24 | "slate": ">=0.55.0", 25 | "@grafana/data": "^6.7.1", 26 | "@grafana/runtime": "^6.7.1", 27 | "@grafana/toolkit": "^6.7.1", 28 | "@grafana/ui": "^6.7.1", 29 | "@babel/core": "^7.0.0-0", 30 | "slate-react": ">=0.22.0", 31 | "babel-loader": ">=8.1.0", 32 | "webpack": ">=2", 33 | "@babel/plugin-proposal-numeric-separator": "^7.8.3" 34 | }, 35 | "dependencies": { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/config/config_ctrl.ts: -------------------------------------------------------------------------------- 1 | import template from './template.html'; 2 | 3 | class ConfigCtrl { 4 | static template = template; 5 | appModel: any; 6 | 7 | constructor() { 8 | if (this.appModel.jsonData === undefined) { 9 | this.appModel.jsonData = {}; 10 | } 11 | } 12 | } 13 | 14 | export { ConfigCtrl }; 15 | -------------------------------------------------------------------------------- /src/config/template.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/datasource/config_ctrl.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { getDataSourceSrv } from '@grafana/runtime'; 3 | import { AppEvents } from '@grafana/data'; 4 | import { CoreEvents } from 'grafana/app/types'; 5 | import appEvents from 'grafana/app/core/app_events'; 6 | 7 | import LoudMLAPI from './loudml_api'; 8 | import configTemplate from './partials/config.html'; 9 | import LoudMLDatasource from './datasource'; 10 | import { DEFAULT_MODEL, DEFAULT_JOB, DEFAULT_FEATURE, ANOMALY_HOOK_NAME, ANOMALY_HOOK } from './types'; 11 | 12 | const POST_A_BUG_SAVE_PLANET = 13 | 'Be aware, it may be an alien bug. If so - save planet, post bug report at https://github.com/vsergeyev/loudml-grafana-app/issues (starship troopers will do the rest of bloody job for you)'; 14 | 15 | function sorry_its_error(err) { 16 | // Guys, it's really sorry 17 | window.console.log('Model update error.'); 18 | window.console.log(POST_A_BUG_SAVE_PLANET); 19 | window.console.log(err); 20 | appEvents.emit(AppEvents.alertError, ['Model update error', err.data]); 21 | } 22 | 23 | export class LoudMLConfigCtrl { 24 | static template = configTemplate; 25 | ACCESS_OPTIONS = [ 26 | { key: 'proxy', value: 'Server (Default)' }, 27 | { key: 'direct', value: 'Browser' }, 28 | ]; 29 | 30 | current: any; 31 | 32 | showAccessHelp = false; 33 | modelsList = []; 34 | jobsList = []; 35 | scheduledList = []; 36 | buckets = []; 37 | job: any; 38 | model: any; 39 | 40 | constructor(private $scope: any) { 41 | if (this.$scope.current === undefined) { 42 | this.$scope.current = { 43 | url: '', 44 | access: 'proxy', 45 | }; 46 | } 47 | } 48 | 49 | toggleAccessHelp() { 50 | this.showAccessHelp = !this.showAccessHelp; 51 | } 52 | 53 | /** 54 | * Displays list of ML models and jobs on server 55 | */ 56 | async refreshModels() { 57 | this.$scope.ctrl.modelsList = [{ is_loading: true, settings: { name: 'Loading...' }, state: { trained: '' } }]; 58 | 59 | const ds = (await getDataSourceSrv().loadDatasource(this.current.name)) as LoudMLDatasource; 60 | 61 | try { 62 | ds.query({ url: '/models', params: {} }) 63 | .then(response => { 64 | this.$scope.ctrl.modelsList = response; 65 | this.$scope.$apply(); 66 | }) 67 | .catch(err => { 68 | console.error(err.statusText); 69 | console.error('Long time ago, in a galaxy far far away...'); 70 | console.log('https://www.google.com/search?q=parallel+worlds+michio+kaku'); 71 | appEvents.emit(AppEvents.alertError, [err.statusText]); 72 | }); 73 | 74 | ds.query({ url: '/jobs', params: {} }) 75 | .then(response => { 76 | this.$scope.ctrl.jobsList = response; 77 | this.$scope.$apply(); 78 | }) 79 | .catch(err => { 80 | appEvents.emit(AppEvents.alertError, [err.statusText]); 81 | }); 82 | 83 | ds.query({ url: '/scheduled_jobs', params: {} }) 84 | .then(response => { 85 | this.$scope.ctrl.scheduledList = response; 86 | this.$scope.$apply(); 87 | }) 88 | .catch(err => { 89 | appEvents.emit(AppEvents.alertError, [err.statusText]); 90 | }); 91 | 92 | ds.query({ url: '/buckets', params: {} }) 93 | .then(response => { 94 | this.$scope.ctrl.buckets = response; 95 | }) 96 | .catch(err => { 97 | appEvents.emit(AppEvents.alertError, [err.statusText]); 98 | }); 99 | } catch (err) { 100 | console.error(err); 101 | appEvents.emit(AppEvents.alertError, [err]); 102 | } 103 | } 104 | 105 | /** 106 | * Displays new ML model dialog 107 | */ 108 | addModel() { 109 | this.model = Object.assign({ 110 | ...DEFAULT_MODEL, 111 | features: [ 112 | { 113 | ...DEFAULT_FEATURE, 114 | }, 115 | ], 116 | }); 117 | appEvents.emit('show-modal', { 118 | src: '/public/plugins/loudml-grafana-app/datasource/partials/add_model.html', 119 | modalClass: 'confirm-modal', 120 | model: this, 121 | }); 122 | } 123 | 124 | editModel(name: any) { 125 | const model = this.$scope.ctrl.modelsList.find(el => el.settings.name === name); 126 | this.model = model.settings; 127 | // appEvents.emit(CoreEvents.showModal, { 128 | appEvents.emit('show-modal', { 129 | src: '/public/plugins/loudml-grafana-app/datasource/partials/add_model.html', 130 | modalClass: 'confirm-modal', 131 | model: this, 132 | }); 133 | } 134 | 135 | /** 136 | * Posts dialog data to LoudML server 137 | */ 138 | async addModelPost() { 139 | console.log(this.model); 140 | 141 | if (this.model.features[0].default !== 'previous') { 142 | this.model.features[0].default = 0; 143 | } 144 | 145 | delete this.model.features[0].$$hashKey; 146 | 147 | const ds = (await getDataSourceSrv().loadDatasource(this.current.name)) as LoudMLDatasource; 148 | ds.loudml 149 | .getModel(this.model.name) 150 | .then(result => { 151 | console.log('Model exists, updating it...'); 152 | ds.loudml 153 | .patchModel(this.model.name, this.model) 154 | .then(result => {}) 155 | .catch(err => { 156 | sorry_its_error(err); 157 | return; 158 | }); 159 | // // Let remove it and recreate 160 | // ds.loudml.deleteModel(this.model.name).then(response => { 161 | // ds.loudml 162 | // .createModel(this.model) 163 | // .then(result => { 164 | // ds.loudml 165 | // .createModelHook(this.model.name, ds.loudml.createHook(ANOMALY_HOOK, this.model.default_bucket)) 166 | // .then(result => { 167 | // appEvents.emit(AppEvents.alertSuccess, ['Model has been updated on Loud ML server']); 168 | // this.refreshModels(); 169 | // }) 170 | // .catch(err => { 171 | // window.console.log('createModelHook error', err); 172 | // appEvents.emit(AppEvents.alertError, [err.message]); 173 | // return; 174 | // }); 175 | // }) 176 | // .catch(err => { 177 | // window.console.log('Model create error', err); 178 | // appEvents.emit(AppEvents.alertError, ['Model create error', err.data]); 179 | // return; 180 | // }); 181 | // }); 182 | }) 183 | .catch(err => { 184 | // New model 185 | console.log('New model, creating it...'); 186 | ds.loudml 187 | .createModel(this.model) 188 | .then(result => { 189 | ds.loudml 190 | .createModelHook(this.model.name, ds.loudml.createHook(ANOMALY_HOOK, this.model.default_bucket)) 191 | .then(result => { 192 | appEvents.emit(AppEvents.alertSuccess, ['Model has been created on Loud ML server']); 193 | this.refreshModels(); 194 | }) 195 | .catch(err => { 196 | window.console.log('createModelHook error', err); 197 | appEvents.emit(AppEvents.alertError, [err.message]); 198 | return; 199 | }); 200 | }) 201 | .catch(err => { 202 | window.console.log('createModel error', err); 203 | appEvents.emit(AppEvents.alertError, ['Model create error', err.data]); 204 | return; 205 | }); 206 | }); 207 | } 208 | 209 | /** 210 | * Displays new job dialog 211 | */ 212 | addJob() { 213 | this.job = Object.assign({}, DEFAULT_JOB); 214 | 215 | appEvents.emit('show-modal', { 216 | src: '/public/plugins/loudml-grafana-app/datasource/partials/add_job.html', 217 | modalClass: 'confirm-modal', 218 | model: this, 219 | }); 220 | } 221 | 222 | /** 223 | * Displays edit job dialog 224 | */ 225 | editJob(name: any) { 226 | this.job = this.$scope.ctrl.scheduledList.find(el => el.name === name); 227 | if (this.job.params) { 228 | this.job.params = JSON.stringify(this.job.params); 229 | } 230 | 231 | if (this.job.json) { 232 | this.job.json = JSON.stringify(this.job.json); 233 | } 234 | 235 | appEvents.emit('show-modal', { 236 | src: '/public/plugins/loudml-grafana-app/datasource/partials/add_job.html', 237 | modalClass: 'confirm-modal', 238 | model: this, 239 | }); 240 | } 241 | 242 | /** 243 | * Posts job data to LoudML server 244 | */ 245 | async scheduleJob() { 246 | const ds = (await getDataSourceSrv().loadDatasource(this.current.name)) as LoudMLDatasource; 247 | ds.loudml 248 | .scheduleJob(this.job) 249 | .then(response => { 250 | window.console.log(response); 251 | appEvents.emit(AppEvents.alertSuccess, ['Job has been scheduled on Loud ML server']); 252 | this.refreshModels(); 253 | }) 254 | .catch(error => { 255 | console.log(error); 256 | appEvents.emit(AppEvents.alertError, ['Job schedule error', error.statusText]); 257 | }); 258 | } 259 | 260 | /** 261 | * Deletes job on LoudML server 262 | */ 263 | async deleteJob(name: any) { 264 | const ds = (await getDataSourceSrv().loadDatasource(this.current.name)) as LoudMLDatasource; 265 | ds.loudml 266 | .deleteJob(name) 267 | .then(response => { 268 | window.console.log(response); 269 | appEvents.emit(AppEvents.alertSuccess, ['Scheduled job has been deleted on Loud ML server']); 270 | this.refreshModels(); 271 | }) 272 | .catch(error => { 273 | appEvents.emit(AppEvents.alertError, ['Job delete error', error.statusText]); 274 | }); 275 | } 276 | 277 | async startModel(name: any) { 278 | const ds = (await getDataSourceSrv().loadDatasource(this.current.name)) as LoudMLDatasource; 279 | 280 | try { 281 | ds.loudml.startModel(name).then(response => { 282 | window.console.log(response); 283 | appEvents.emit(AppEvents.alertSuccess, ['Model has been started on Loud ML server']); 284 | this.refreshModels(); 285 | }); 286 | } catch (err) { 287 | console.error(err); 288 | appEvents.emit(AppEvents.alertError, ['Model start error', err]); 289 | } 290 | } 291 | 292 | async stopModel(name: any) { 293 | const ds = (await getDataSourceSrv().loadDatasource(this.current.name)) as LoudMLDatasource; 294 | 295 | try { 296 | ds.loudml.stopModel(name).then(response => { 297 | window.console.log(response); 298 | appEvents.emit(AppEvents.alertSuccess, ['Model has been stoped on Loud ML server']); 299 | this.refreshModels(); 300 | }); 301 | } catch (err) { 302 | console.error(err); 303 | appEvents.emit(AppEvents.alertError, ['Model stop error', err]); 304 | } 305 | } 306 | 307 | async forecastModel(name: any) { 308 | window.console.log('FORECAST MODEL'); 309 | } 310 | 311 | async trainModel(name: any) { 312 | window.console.log('TRAIN MODEL'); 313 | } 314 | 315 | async deleteModel(name: any) { 316 | const ds = (await getDataSourceSrv().loadDatasource(this.current.name)) as LoudMLDatasource; 317 | 318 | try { 319 | ds.loudml.deleteModel(name).then(response => { 320 | window.console.log(response); 321 | appEvents.emit(AppEvents.alertSuccess, ['Model has been deleted on Loud ML server']); 322 | this.refreshModels(); 323 | }); 324 | } catch (err) { 325 | console.error(err); 326 | appEvents.emit(AppEvents.alertError, ['Model delete error', err]); 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/datasource/datasource.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { BackendSrv } from 'grafana/app/core/services/backend_srv'; 3 | 4 | import LoudMLAPI from './loudml_api'; 5 | 6 | export default class LoudMLDatasource { 7 | loudml: LoudMLAPI; 8 | bucket: string; 9 | jsonData: any; 10 | 11 | /** @ngInject */ 12 | constructor(instanceSettings: any, backendSrv: BackendSrv) { 13 | this.bucket = (instanceSettings.jsonData || {}).bucket; 14 | this.loudml = new LoudMLAPI(instanceSettings, backendSrv); 15 | } 16 | 17 | async query(options: any) { 18 | const { url, params } = options; 19 | const response = await this.loudml.get(url, params); 20 | return response; 21 | } 22 | 23 | async testDatasource() { 24 | const response = await this.loudml.get('/'); 25 | return response.version 26 | ? { status: 'success', message: 'Data source is working, Loud ML server version ' + response.version } 27 | : { status: 'error', message: response.error }; 28 | } 29 | 30 | metricFindQuery(options: any) { 31 | return []; 32 | } 33 | } 34 | 35 | export { LoudMLDatasource }; 36 | -------------------------------------------------------------------------------- /src/datasource/loudml_api.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // Loud ML API class 3 | // Connects to Loud ML server 4 | // 5 | // https://loudml.io 6 | // http://github.com/regel/loudml 7 | 8 | import { BackendSrv } from 'grafana/app/core/services/backend_srv'; 9 | 10 | import { 11 | DEFAULT_LOUDML_RP, 12 | MODEL_TYPE_LIST, 13 | DEFAULT_MODEL, 14 | DEFAULT_FEATURE, 15 | DEFAULT_START_OPTIONS, 16 | MIN_INTERVAL_SECOND, 17 | MIN_INTERVAL_UNIT, 18 | MAX_INTERVAL_SECOND, 19 | MAX_INTERVAL_UNIT, 20 | MIN_SPAN, 21 | MAX_SPAN, 22 | DEFAULT_ANOMALY_TYPE, 23 | ANOMALY_HOOK_NAME, 24 | ANOMALY_HOOK, 25 | } from './types'; 26 | 27 | export default class LoudMLAPI { 28 | private url: string; 29 | 30 | constructor(instanceSettings: any, private backendSrv: BackendSrv) { 31 | this.url = instanceSettings.url; 32 | } 33 | 34 | async get(url: string, params?: any) { 35 | return this._query('GET', url, params); 36 | } 37 | 38 | private async _query(method: string, url: string, data?: any, data_as_params?: false) { 39 | method = method.toUpperCase(); 40 | let options: any = { 41 | method, 42 | url: this.url + url, 43 | headers: {}, 44 | }; 45 | if (method === 'GET' || method === 'DELETE' || data_as_params) { 46 | options.params = data; 47 | } else { 48 | options.data = data; 49 | options.headers['Content-Type'] = 'application/json'; 50 | } 51 | 52 | const response = await this.backendSrv.datasourceRequest(options); 53 | const responseData = response.data; 54 | 55 | return responseData; 56 | } 57 | 58 | createAndGetBucket = async (database, retentionPolicy, measurement, source) => { 59 | const { host, port } = this.splitAddr('http://localhost:8086', 8086); //source.url, 8086) 60 | const bucketName = [database, retentionPolicy, measurement].join('_'); 61 | const settings = { 62 | type: source.type, 63 | name: bucketName, 64 | retention_policy: retentionPolicy, 65 | database, 66 | measurement, 67 | addr: `${host}:${port}`, 68 | username: source.username, 69 | password: source.password, 70 | }; 71 | 72 | await this._query('POST', '/buckets', settings); 73 | const response = await this._query('GET', `/buckets/${bucketName}`); 74 | 75 | return response[0]; 76 | }; 77 | 78 | splitAddr = (url, port) => { 79 | // extract host and port from url address 80 | const re = /(https?:)?(\/\/)?([\w\.]*):?(\d*)?/; 81 | const res = re.exec(url); 82 | return { 83 | host: res[3], 84 | port: res[4] || port, 85 | }; 86 | }; 87 | 88 | createModel = async model => { 89 | // POST model JSON to /models 90 | return this._query('POST', '/models', model); 91 | }; 92 | 93 | patchModel = async (name, model) => { 94 | return this._query('PATCH', `/models/${name}`, model); 95 | }; 96 | 97 | getModel = async name => { 98 | return this._query('GET', `/models/${name}`); 99 | }; 100 | 101 | deleteModel = async name => { 102 | return this._query('DELETE', `/models/${name}`); 103 | }; 104 | 105 | createHook = (hook, bucket) => { 106 | const h = { ...hook }; 107 | h.config.bucket = bucket; 108 | return h; 109 | }; 110 | 111 | createModelHook = async (name, hook) => { 112 | // POST model hook to /models/${name}/hooks 113 | return this._query('POST', `/models/${name}/hooks`, hook); 114 | }; 115 | 116 | trainAndStartModel = async (name, from, to, output_bucket) => { 117 | const params = { 118 | ...DEFAULT_START_OPTIONS, 119 | from, 120 | to, 121 | output_bucket: output_bucket, 122 | }; 123 | return this._query('POST', `/models/${name}/_train`, params, true); 124 | }; 125 | 126 | forecastModel = async (name, data, output_bucket) => { 127 | const { from, to } = data.timeRange.raw; 128 | const params = { 129 | from, 130 | to, 131 | save_output_data: true, 132 | output_bucket: output_bucket, 133 | bg: true, 134 | }; 135 | return this._query('POST', `/models/${name}/_forecast`, params, true); 136 | }; 137 | 138 | trainModel = async (name, data, output_bucket) => { 139 | const { lower, upper } = this.convertTimeRange(data.timeRange); 140 | return await this.trainAndStartModel(name, lower, upper, output_bucket); 141 | }; 142 | 143 | startModel = async (name, output_bucket) => { 144 | const params = { 145 | ...DEFAULT_START_OPTIONS, 146 | output_bucket: output_bucket, 147 | }; 148 | return this._query('POST', `/models/${name}/_start`, params, true); 149 | }; 150 | 151 | stopModel = async name => { 152 | const params = {}; 153 | return this._query('POST', `/models/${name}/_stop`, params, true); 154 | }; 155 | 156 | scheduleJob = async job => { 157 | let params = { 158 | ...job, 159 | }; 160 | 161 | window.console.log(params); 162 | params.every.count = parseInt(params.every.count, 10) || 1; 163 | 164 | if (!params.params) { 165 | delete params.params; 166 | } else { 167 | params.params = JSON.parse(params.params); 168 | } 169 | 170 | if (!params.json) { 171 | delete params.json; 172 | } else { 173 | params.json = JSON.parse(params.json); 174 | } 175 | 176 | if (!params.every.at) { 177 | delete params.every.at; 178 | } 179 | 180 | delete params['$$hashKey']; 181 | delete params.ok; 182 | delete params.error; 183 | delete params.status_code; 184 | delete params.last_run_timestamp; 185 | 186 | window.console.log(params); 187 | return this._query('POST', `/scheduled_jobs`, params); 188 | }; 189 | 190 | deleteJob = async name => { 191 | return this._query('DELETE', `/scheduled_jobs/${name}`); 192 | }; 193 | 194 | convertTimeRange = timeRange => { 195 | const { from, to } = timeRange.raw; 196 | return { 197 | lower: from, 198 | upper: to, 199 | }; 200 | }; 201 | } 202 | -------------------------------------------------------------------------------- /src/datasource/module.ts: -------------------------------------------------------------------------------- 1 | import { LoudMLDatasource } from './datasource'; 2 | // import { LoudMLQueryCtrl as QueryCtrl } from './query_ctrl'; 3 | // import { LoudMLConfigCtrl as ConfigCtrl } from './config_ctrl'; 4 | import { LoudMLQueryCtrl } from './query_ctrl'; 5 | import { LoudMLConfigCtrl } from './config_ctrl'; 6 | import { LoudMLQuery, LoudMLOptions } from './types'; 7 | 8 | // export { LoudMLDatasource as Datasource, ConfigCtrl, QueryCtrl }; 9 | 10 | export { LoudMLDatasource, LoudMLDatasource as Datasource, LoudMLQueryCtrl as QueryCtrl, LoudMLConfigCtrl as ConfigCtrl }; 11 | -------------------------------------------------------------------------------- /src/datasource/partials/add_job.html: -------------------------------------------------------------------------------- 1 | 137 | 138 | -------------------------------------------------------------------------------- /src/datasource/partials/add_model.html: -------------------------------------------------------------------------------- 1 | 256 | 257 | -------------------------------------------------------------------------------- /src/datasource/partials/config.html: -------------------------------------------------------------------------------- 1 |
2 |

HTTP

3 |
4 |
5 |
6 | Loud ML Server URL 7 | 16 | 17 |

Specify a complete Loud ML Server HTTP URL (for example http://your_loudml_server:8077)

18 | 19 | Your access method is Browser, this means the URL 20 | needs to be accessible from the browser. 21 | 22 | 23 | Your access method is Server, this means the URL 24 | needs to be accessible from the grafana backend/server. 25 | 26 |
27 |
28 |
29 | 30 |
31 |
32 | Access 33 |
34 | 39 |
40 |
41 |
42 | 47 |
48 |
49 | 50 |
51 |

52 | Access mode controls how requests to the data source will be handled. 53 | Server access mode should be the preferred way if nothing else stated. 54 |

55 |
Server access mode (Default):
56 |

57 | All requests will be made from the browser to Grafana backend/server which in turn will 58 | forward the requests to the data source and by that circumvent possible 59 | Cross-Origin Resource Sharing (CORS) requirements. 60 | The URL needs to be accessible from the grafana backend/server if you select this access mode. 61 |

62 |
Browser access mode:
63 |

64 | All requests will be made from the browser directly to the data source and may be subject to 65 | Cross-Origin Resource Sharing (CORS) requirements. The URL needs to be accessible from the browser 66 | if you select this access mode. 67 |

68 |
69 | 70 |
71 |
72 | Whitelisted Cookies 73 | 78 | 79 | Grafana Proxy deletes forwarded cookies by default. Specify cookies by name 80 | that should be forwarded to the data source. 81 | 82 |
83 |
84 |
85 |
86 | 87 |
88 |

Manage Machine Learning Tasks

89 |
90 | 94 | 95 | 99 | 100 | 104 |
105 | 106 |
107 |
Models
108 |
109 | 110 | 111 | 112 | 118 | 123 | 132 | 142 | 147 | 148 | 149 |
113 | 114 | 115 | {{model.settings.name}} 116 | 117 | 119 | Running. 120 | Trained. 121 | Not trained. 122 | 124 | 125 | Play 126 | 127 | 128 | 129 | Stop 130 | 131 | 143 | 144 | Delete 145 | 146 |
150 |
151 |
152 |
153 | No models to show. Click refresh to update data from Loud ML server 154 |
155 | 156 |
157 |
Scheduled Jobs
158 |
159 | 160 | 161 | 162 | 168 | 171 | 174 | 182 | 191 | 196 | 197 | 198 |
163 | 164 | 165 | {{job.name}} 166 | 167 | 169 | {{job.method}} 170 | 172 | {{job.relative_url}} 173 | 175 | Every {{job.every.count}} 176 | {{job.every.unit}} 177 | 178 | 179 | at {{job.every.at}} 180 | 181 | 183 | 184 | OK: {{job.ok}}; Result: {{job.status_code}} 185 | 186 |
187 | Error: {{job.error}} 188 |
189 |
190 |
192 | 193 | Delete 194 | 195 |
199 |
200 |
201 |
202 | No scheduled jobs to show. Click refresh to update data from Loud ML server 203 |
204 | 205 |
206 |
Jobs
207 |
208 | 209 | 210 | 211 | 214 | 217 | 220 | 225 | 226 | 227 |
212 | {{job.model}} 213 | 215 | {{job.start_date}} 216 | 218 | {{job.type}} 219 | 221 | {{job.state}} 222 |
223 | {{job.error}} 224 |
228 |
229 |
230 |
231 | No jobs to show. Click refresh to update data from Loud ML server 232 |
233 | 234 |
235 | -------------------------------------------------------------------------------- /src/datasource/partials/edit_model.html: -------------------------------------------------------------------------------- 1 | 203 | 204 | -------------------------------------------------------------------------------- /src/datasource/partials/query_ctrl.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/src/datasource/partials/query_ctrl.html -------------------------------------------------------------------------------- /src/datasource/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "datasource", 3 | "name": "Loud ML Datasource", 4 | "id": "loudml-datasource", 5 | "metrics": true, 6 | "info": { 7 | "logos": { 8 | "small": "../loudml-grafana-app/img/logo.png", 9 | "large": "../loudml-grafana-app/img/logo.png" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/datasource/query_ctrl.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import template from './partials/query_ctrl.html'; 3 | 4 | import { QueryCtrl } from 'grafana/app/plugins/sdk'; 5 | 6 | export class LoudMLQueryCtrl extends QueryCtrl { 7 | static template = template; 8 | 9 | /** @ngInject */ 10 | constructor($scope, $injector) { 11 | super($scope, $injector); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/datasource/types.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { DataQuery, DataSourceJsonData } from '@grafana/data'; 3 | 4 | export interface LoudMLOptions extends DataSourceJsonData { 5 | bucket: string; 6 | } 7 | 8 | export interface LoudMLQuery extends DataQuery {} 9 | 10 | export const DEFAULT_LOUDML_RP = 'autogen'; 11 | 12 | export const MODEL_TYPE_LIST = [ 13 | { name: 'Donut', type: 'donut', default: true }, 14 | // { name: 'DiskUtil', type: 'donut-ns', default: false }, 15 | // { name: 'donut multivariate', type: 'donut-mv', default: false }, 16 | ]; 17 | 18 | export const DEFAULT_MODEL = { 19 | bucket_interval: '20m', 20 | default_bucket: null, 21 | features: [], 22 | interval: '1m', 23 | max_evals: 20, 24 | name: '', 25 | offset: '10s', 26 | span: 10, 27 | type: MODEL_TYPE_LIST[0].type, 28 | }; 29 | 30 | export const DEFAULT_JOB = { 31 | name: '', 32 | method: 'get', 33 | relative_url: '', 34 | params: '', 35 | json: '', 36 | every: { 37 | count: 1, 38 | unit: '', 39 | at: '', 40 | }, 41 | }; 42 | 43 | export const DEFAULT_FEATURE = { 44 | name: '', 45 | measurement: null, 46 | field: null, 47 | metric: 'mean', 48 | default: null, 49 | io: 'io', 50 | anomaly_type: 'low_high', 51 | match_all: [], 52 | }; 53 | 54 | export const DEFAULT_START_OPTIONS = { 55 | output_bucket: 'loudml', 56 | save_output_data: true, 57 | flag_abnormal_data: true, 58 | }; 59 | 60 | export const MIN_INTERVAL_SECOND = 5; 61 | 62 | export const MIN_INTERVAL_UNIT = `${MIN_INTERVAL_SECOND}s`; 63 | 64 | export const MAX_INTERVAL_SECOND = 60; 65 | 66 | export const MAX_INTERVAL_UNIT = `${MAX_INTERVAL_SECOND}s`; 67 | 68 | export const MIN_SPAN = 10; 69 | 70 | export const MAX_SPAN = 100; 71 | 72 | export const DEFAULT_ANOMALY_TYPE = [ 73 | { text: 'low', value: 'low' }, 74 | { text: 'high', value: 'high' }, 75 | { text: 'low/high', value: 'low_high' }, 76 | ]; 77 | 78 | export const ANOMALY_HOOK_NAME = 'add_annotation'; 79 | 80 | export const ANOMALY_HOOK = { 81 | type: 'annotations', 82 | name: ANOMALY_HOOK_NAME, 83 | config: { 84 | id: null, 85 | type: 'loudml', 86 | bucket: null, 87 | }, 88 | }; 89 | -------------------------------------------------------------------------------- /src/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/src/img/logo.png -------------------------------------------------------------------------------- /src/img/loudml_grafana_datasource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/src/img/loudml_grafana_datasource.png -------------------------------------------------------------------------------- /src/img/loudml_grafana_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/src/img/loudml_grafana_panel.png -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { ConfigCtrl } from './config/config_ctrl'; 2 | 3 | export { ConfigCtrl }; 4 | -------------------------------------------------------------------------------- /src/module_raw.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.html' { 2 | const contents: string; 3 | export = contents; 4 | } 5 | -------------------------------------------------------------------------------- /src/panel/Graph2.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // Libraries 3 | import _ from 'lodash'; 4 | import $ from 'jquery'; 5 | import tinycolor from 'tinycolor2'; 6 | import React, { PureComponent } from 'react'; 7 | import uniqBy from 'lodash/uniqBy'; 8 | import flattenDeep from 'lodash/flattenDeep'; 9 | import cloneDeep from 'lodash/cloneDeep'; 10 | 11 | import appEvents from 'grafana/app/core/app_events'; 12 | import { getDataSourceSrv, getBackendSrv } from '@grafana/runtime'; 13 | import { getValueFormat, formattedValueToString, AnnotationEvent, DataSourceApi, AppEvents } from '@grafana/data'; 14 | import { Graph, OK_COLOR, ALERTING_COLOR, NO_DATA_COLOR, PENDING_COLOR, DEFAULT_ANNOTATION_COLOR, REGION_FILL_ALPHA } from '@grafana/ui'; 15 | 16 | export class Graph2 extends Graph { 17 | dashboard: any; 18 | datasourcePromises: any; 19 | globalAnnotationsPromise: any; 20 | annotations: AnnotationEvent[]; 21 | 22 | constructor(props) { 23 | super(props); 24 | 25 | const promises = []; 26 | const dsPromises = []; 27 | const range = props.timeRange; 28 | this.annotations = []; 29 | 30 | if (props.panelChrome) { 31 | this.dashboard = props.panelChrome.props.dashboard; 32 | 33 | Promise.all([this.getGlobalAnnotations(range)]) 34 | .then(results => { 35 | this.annotations = flattenDeep(results[0]); 36 | // filter out annotations that do not belong to requesting panel 37 | this.annotations = this.annotations.filter(item => { 38 | // if event has panel id and query is of type dashboard then panel and requesting panel id must match 39 | if (item.panelId && item.source.type === 'dashboard') { 40 | return item.panelId === props.panelChrome.props.panel.id; 41 | } 42 | return true; 43 | }); 44 | 45 | this.annotations = this.dedupAnnotations(this.annotations); 46 | this.draw(); 47 | }) 48 | .catch(err => { 49 | if (!err.message && err.data && err.data.message) { 50 | err.message = err.data.message; 51 | } 52 | console.log('AnnotationSrv.query error', err); 53 | appEvents.emit(AppEvents.alertError, ['Annotation Query Failed', err.message || err]); 54 | return []; 55 | }); 56 | } 57 | } 58 | 59 | getGlobalAnnotations(range: any) { 60 | const promises = []; 61 | const dsPromises = []; 62 | 63 | for (const annotation of this.dashboard.annotations.list) { 64 | if (!annotation.enable) { 65 | continue; 66 | } 67 | 68 | if (annotation.snapshotData) { 69 | return this.translateQueryResult(annotation, annotation.snapshotData); 70 | } 71 | 72 | const datasourcePromise = getDataSourceSrv().get(annotation.datasource); 73 | dsPromises.push(datasourcePromise); 74 | promises.push( 75 | datasourcePromise 76 | .then((datasource: DataSourceApi) => { 77 | // issue query against data source 78 | return datasource.annotationQuery({ 79 | range, 80 | rangeRaw: range.raw, 81 | annotation: annotation, 82 | dashboard: this.dashboard, 83 | }); 84 | }) 85 | .then(results => { 86 | // store response in annotation object if this is a snapshot call 87 | if (this.dashboard.snapshot) { 88 | annotation.snapshotData = cloneDeep(results); 89 | } 90 | // translate result 91 | return this.translateQueryResult(annotation, results); 92 | }) 93 | ); 94 | } 95 | 96 | this.datasourcePromises = Promise.all(dsPromises); 97 | this.globalAnnotationsPromise = Promise.all(promises); 98 | return this.globalAnnotationsPromise; 99 | } 100 | 101 | dedupAnnotations(annotations: any) { 102 | let dedup = []; 103 | 104 | // Split events by annotationId property existence 105 | const events = _.partition(annotations, 'id'); 106 | 107 | const eventsById = _.groupBy(events[0], 'id'); 108 | dedup = _.map(eventsById, eventGroup => { 109 | if (eventGroup.length > 1 && !_.every(eventGroup, this.isPanelAlert)) { 110 | // Get first non-panel alert 111 | return _.find(eventGroup, event => { 112 | return event.eventType !== 'panel-alert'; 113 | }); 114 | } else { 115 | return _.head(eventGroup); 116 | } 117 | }); 118 | 119 | dedup = _.concat(dedup, events[1]); 120 | return dedup; 121 | } 122 | 123 | isPanelAlert(event: { eventType: string }) { 124 | return event.eventType === 'panel-alert'; 125 | } 126 | 127 | translateQueryResult(annotation: any, results: any) { 128 | // if annotation has snapshotData 129 | // make clone and remove it 130 | if (annotation.snapshotData) { 131 | annotation = cloneDeep(annotation); 132 | delete annotation.snapshotData; 133 | } 134 | 135 | for (const item of results) { 136 | item.source = annotation; 137 | item.isRegion = item.timeEnd && item.time !== item.timeEnd; 138 | } 139 | 140 | return results; 141 | } 142 | 143 | tickFormatter(val, axis) { 144 | const formatter = getValueFormat('short'); 145 | 146 | if (!formatter) { 147 | throw new Error(`Unit '${format}' is not supported`); 148 | } 149 | return formattedValueToString(formatter(val, axis.tickDecimals, axis.scaledDecimals)); 150 | } 151 | 152 | getYAxes(series: GraphSeriesXY[]) { 153 | if (series.length === 0) { 154 | return [{ show: true, min: -1, max: 1 }]; 155 | } 156 | return uniqBy( 157 | series.map(s => { 158 | const index = s.yAxis ? s.yAxis.index : 1; 159 | const min = s.yAxis && !isNaN(s.yAxis.min as number) ? s.yAxis.min : null; 160 | const tickDecimals = s.yAxis && !isNaN(s.yAxis.tickDecimals as number) ? s.yAxis.tickDecimals : null; 161 | return { 162 | show: true, 163 | index, 164 | position: index === 1 ? 'left' : 'right', 165 | min, 166 | tickDecimals, 167 | tickFormatter: this.tickFormatter, 168 | }; 169 | }), 170 | yAxisConfig => yAxisConfig.index 171 | ); 172 | } 173 | 174 | getFillGradient(amount: number) { 175 | if (!amount) { 176 | return null; 177 | } 178 | 179 | return { 180 | colors: [{ opacity: 0.0 }, { opacity: amount / 10 }], 181 | }; 182 | } 183 | 184 | translateFillOption(fill: number) { 185 | if (this.props.stack) { 186 | return fill === 0 ? 0.001 : fill / 10; 187 | } else { 188 | return fill / 10; 189 | } 190 | } 191 | 192 | draw() { 193 | if (this.element === null) { 194 | return; 195 | } 196 | 197 | const { 198 | width, 199 | series, 200 | timeRange, 201 | showLines, 202 | showBars, 203 | showPoints, 204 | isStacked, 205 | lineWidth, 206 | fill, 207 | fillGradient, 208 | timeZone, 209 | onHorizontalRegionSelected, 210 | } = this.props; 211 | 212 | if (!width) { 213 | return; 214 | } 215 | 216 | const ticks = width / 100; 217 | const min = timeRange.from.valueOf(); 218 | const max = timeRange.to.valueOf(); 219 | const yaxes = this.getYAxes(series); 220 | 221 | const flotOptions: any = { 222 | legend: { 223 | show: false, 224 | }, 225 | series: { 226 | stack: isStacked, 227 | lines: { 228 | show: showLines, 229 | fill: this.translateFillOption(fill), 230 | fillColor: this.getFillGradient(fillGradient), 231 | lineWidth: lineWidth, 232 | zero: false, 233 | }, 234 | points: { 235 | show: showPoints, 236 | fill: 1, 237 | fillColor: false, 238 | radius: 2, 239 | }, 240 | bars: { 241 | show: showBars, 242 | fill: 1, 243 | // Dividig the width by 1.5 to make the bars not touch each other 244 | barWidth: showBars ? this.getBarWidth() / 1.5 : 1, 245 | zero: false, 246 | lineWidth: lineWidth, 247 | }, 248 | shadowSize: 0, 249 | }, 250 | xaxis: { 251 | show: true, 252 | mode: 'time', 253 | min: min, 254 | max: max, 255 | label: 'Datetime', 256 | ticks: ticks, 257 | timeformat: timeFormat(ticks, min, max), 258 | timezone: timeZone ?? DefaultTimeZone, 259 | }, 260 | yaxes, 261 | grid: { 262 | minBorderMargin: 0, 263 | markings: [], 264 | backgroundColor: null, 265 | borderWidth: 0, 266 | hoverable: true, 267 | clickable: true, 268 | color: '#a1a1a1', 269 | margin: { left: 0, right: 0 }, 270 | labelMarginX: 0, 271 | mouseActiveRadius: 30, 272 | }, 273 | selection: { 274 | mode: onHorizontalRegionSelected ? 'x' : null, 275 | color: '#666', 276 | }, 277 | crosshair: { 278 | mode: 'x', 279 | }, 280 | }; 281 | 282 | this.addFlotEvents(this.annotations, flotOptions); 283 | 284 | try { 285 | $.plot( 286 | this.element, 287 | series.filter(s => s.isVisible), 288 | flotOptions 289 | ); 290 | } catch (err) { 291 | console.log('Graph rendering error', err, flotOptions, series); 292 | throw new Error('Error rendering panel'); 293 | } 294 | } 295 | 296 | render() { 297 | const { height, width, series } = this.props; 298 | const noDataToBeDisplayed = series.length === 0; 299 | const tooltip = this.renderTooltip(); 300 | const context = this.renderContextMenu(); 301 | return ( 302 |
303 |
(this.element = e)} 306 | style={{ height, width }} 307 | onMouseLeave={() => { 308 | this.setState({ isTooltipVisible: false }); 309 | }} 310 | /> 311 | {noDataToBeDisplayed &&
No data
} 312 | {tooltip} 313 | {context} 314 |
315 | ); 316 | } 317 | 318 | addFlotEvents(annotations: any, flotOptions: any) { 319 | if (!annotations) { 320 | return; 321 | } 322 | 323 | const types: any = { 324 | $__alerting: { 325 | color: ALERTING_COLOR, 326 | position: 'BOTTOM', 327 | markerSize: 5, 328 | }, 329 | $__ok: { 330 | color: OK_COLOR, 331 | position: 'BOTTOM', 332 | markerSize: 5, 333 | }, 334 | $__no_data: { 335 | color: NO_DATA_COLOR, 336 | position: 'BOTTOM', 337 | markerSize: 5, 338 | }, 339 | $__pending: { 340 | color: PENDING_COLOR, 341 | position: 'BOTTOM', 342 | markerSize: 5, 343 | }, 344 | $__editing: { 345 | color: DEFAULT_ANNOTATION_COLOR, 346 | position: 'BOTTOM', 347 | markerSize: 5, 348 | }, 349 | }; 350 | 351 | // annotations from query 352 | annotations.map(item => { 353 | // add properties used by jquery flot events 354 | item.min = item.time; 355 | item.max = item.time; 356 | item.eventType = item.source.name; 357 | 358 | if (item.newState) { 359 | item.eventType = '$__' + item.newState; 360 | } else { 361 | if (!types[item.source.name]) { 362 | types[item.source.name] = { 363 | color: item.source.iconColor, 364 | position: 'BOTTOM', 365 | markerSize: 5, 366 | }; 367 | } 368 | } 369 | }); 370 | 371 | const regions = getRegions(annotations); 372 | addRegionMarking(regions, flotOptions); 373 | 374 | const eventSectionHeight = 20; 375 | const eventSectionMargin = 7; 376 | flotOptions.grid.eventSectionHeight = eventSectionMargin; 377 | flotOptions.xaxis.eventSectionHeight = eventSectionHeight; 378 | 379 | flotOptions.events = { 380 | levels: _.keys(types).length + 1, 381 | data: annotations, 382 | types: types, 383 | manager: this, 384 | }; 385 | } 386 | } 387 | 388 | function getRegions(events: AnnotationEvent[]) { 389 | return _.filter(events, 'isRegion'); 390 | } 391 | 392 | function addRegionMarking(regions: any[], flotOptions: { grid: { markings: any } }) { 393 | const markings = flotOptions.grid.markings; 394 | const defaultColor = DEFAULT_ANNOTATION_COLOR; 395 | let fillColor; 396 | 397 | _.each(regions, region => { 398 | if (region.source) { 399 | fillColor = region.source.iconColor || defaultColor; 400 | } else { 401 | fillColor = defaultColor; 402 | } 403 | 404 | fillColor = addAlphaToRGB(fillColor, REGION_FILL_ALPHA); 405 | markings.push({ 406 | xaxis: { from: region.min, to: region.timeEnd }, 407 | color: fillColor, 408 | }); 409 | }); 410 | } 411 | 412 | function addAlphaToRGB(colorString: string, alpha: number): string { 413 | const color = tinycolor(colorString); 414 | if (color.isValid()) { 415 | color.setAlpha(alpha); 416 | return color.toRgbString(); 417 | } else { 418 | return colorString; 419 | } 420 | } 421 | 422 | // Copied from graph.ts 423 | function timeFormat(ticks: number, min: number, max: number): string { 424 | if (min && max && ticks) { 425 | const range = max - min; 426 | const secPerTick = range / ticks / 1000; 427 | const oneDay = 86400000; 428 | const oneYear = 31536000000; 429 | 430 | if (secPerTick <= 45) { 431 | return '%H:%M:%S'; 432 | } 433 | if (secPerTick <= 7200 || range <= oneDay) { 434 | return '%H:%M'; 435 | } 436 | if (secPerTick <= 80000) { 437 | return '%m/%d %H:%M'; 438 | } 439 | if (secPerTick <= 2419200 || range <= oneYear) { 440 | return '%m/%d'; 441 | } 442 | return '%Y-%m'; 443 | } 444 | 445 | return '%H:%M'; 446 | } 447 | 448 | export default Graph2; 449 | -------------------------------------------------------------------------------- /src/panel/GraphLegendEditor.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import React from 'react'; 3 | import { LegendOptions, PanelOptionsGroup, Switch, Input, StatsPicker } from '@grafana/ui'; 4 | 5 | export interface GraphLegendEditorLegendOptions extends LegendOptions { 6 | stats?: string[]; 7 | decimals?: number; 8 | sortBy?: string; 9 | sortDesc?: boolean; 10 | } 11 | 12 | interface GraphLegendEditorProps { 13 | options: GraphLegendEditorLegendOptions; 14 | onChange: (options: GraphLegendEditorLegendOptions) => void; 15 | } 16 | 17 | export const GraphLegendEditor: React.FunctionComponent = props => { 18 | const { options, onChange } = props; 19 | 20 | const onStatsChanged = (stats: string[]) => { 21 | onChange({ 22 | ...options, 23 | stats, 24 | }); 25 | }; 26 | 27 | const onOptionToggle = (option: keyof LegendOptions) => (event?: React.ChangeEvent) => { 28 | const newOption: Partial = {}; 29 | if (!event) { 30 | return; 31 | } 32 | 33 | if (option === 'placement') { 34 | newOption[option] = event.target.checked ? 'right' : 'under'; 35 | } else { 36 | newOption[option] = event.target.checked; 37 | } 38 | 39 | onChange({ 40 | ...options, 41 | ...newOption, 42 | }); 43 | }; 44 | 45 | const labelWidth = 8; 46 | return ( 47 | 48 |
49 |

Options

50 | 51 | 52 | 58 |
59 | 60 |
61 |

Show

62 |
63 | 64 |
65 | 66 |
67 |
Decimals
68 | { 74 | onChange({ 75 | ...options, 76 | decimals: parseInt(event.target.value, 10), 77 | }); 78 | }} 79 | /> 80 |
81 |
82 | 83 |
84 |

Hidden series

85 | {/* */} 86 | 87 |
88 |
89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /src/panel/GraphPanel.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import React from 'react'; 3 | import { Chart, LegendDisplayMode } from '@grafana/ui'; 4 | import { GraphWithLegend2 } from './GraphWithLegend2'; 5 | import { PanelProps } from '@grafana/data'; 6 | import { Options } from './types'; 7 | import { GraphPanelController, CreateBaselineButton, MLModelController } from './GraphPanelController'; 8 | 9 | interface GraphPanelProps extends PanelProps {} 10 | 11 | export const GraphPanel: React.FunctionComponent = ({ 12 | data, 13 | timeRange, 14 | timeZone, 15 | width, 16 | height, 17 | options, 18 | fieldConfig, 19 | onOptionsChange, 20 | onChangeTimeRange, 21 | }) => { 22 | if (!data) { 23 | return ( 24 |
25 |

No data found in response

26 |
27 | ); 28 | } 29 | 30 | const { 31 | graph: { showLines, showBars, showPoints, isStacked, lineWidth, fill, fillGradient }, 32 | legend: legendOptions, 33 | tooltipOptions, 34 | } = options; 35 | 36 | const graphProps = { 37 | showBars, 38 | showLines, 39 | showPoints, 40 | isStacked, 41 | lineWidth, 42 | fill, 43 | fillGradient, 44 | tooltipOptions, 45 | }; 46 | const { asTable, isVisible, ...legendProps } = legendOptions; 47 | 48 | return ( 49 | <> 50 | 51 | 52 | 53 | 54 | 55 | {({ onSeriesToggle, onHorizontalRegionSelected, ...controllerApi }) => { 56 | return ( 57 | 73 | 74 | 75 | ); 76 | }} 77 | 78 | 79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /src/panel/GraphPanelEditor.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // Libraries 3 | import _ from 'lodash'; 4 | import React, { PureComponent } from 'react'; 5 | 6 | // Services 7 | import { getDataSourceSrv } from '@grafana/runtime'; 8 | 9 | // Types 10 | import { PanelEditorProps, FieldConfig, DataSourceSelectItem } from '@grafana/data'; 11 | import { Switch, LegendOptions, GraphTooltipOptions, PanelOptionsGrid, PanelOptionsGroup, FieldPropertiesEditor, Select, Input } from '@grafana/ui'; 12 | 13 | import { Options, GraphOptions, GraphDatasourceOptions } from './types'; 14 | import { GraphLegendEditor } from './GraphLegendEditor'; 15 | 16 | export class GraphPanelEditor extends PureComponent> { 17 | datasources: DataSourceSelectItem[] = getDataSourceSrv().getMetricSources(); 18 | 19 | constructor(props) { 20 | super(props); 21 | } 22 | 23 | datasourcesList = function() { 24 | var res = new Array({ label: 'Not selected', value: '' }); 25 | 26 | this.datasources.forEach(function(val) { 27 | if (val.meta.id === 'loudml-datasource') { 28 | res.push({ label: val.name, value: val.value }); 29 | } 30 | }); 31 | 32 | return res; 33 | }; 34 | 35 | onChangeDataSource = (value: any) => { 36 | this.props.options.datasourceOptions.datasource = value.value; 37 | this.setState({ value: value.value }); 38 | }; 39 | 40 | onChangeInputBucket = (event: any) => { 41 | this.props.options.datasourceOptions.input_bucket = event.target.value; 42 | this.setState({ value: event.target.value }); 43 | }; 44 | 45 | onBlurInputBucket = () => { 46 | // window.console.log("onBlurInputBucket", this.state); 47 | }; 48 | 49 | onChangeOutputBucket = (event: any) => { 50 | this.props.options.datasourceOptions.output_bucket = event.target.value; 51 | this.setState({ value: event.target.value }); 52 | }; 53 | 54 | onBlurOutputBucket = () => { 55 | // window.console.log("onBlurOutputBucket", this.state); 56 | }; 57 | 58 | onGraphOptionsChange = (options: Partial) => { 59 | this.props.onOptionsChange({ 60 | ...this.props.options, 61 | graph: { 62 | ...this.props.options.graph, 63 | ...options, 64 | }, 65 | }); 66 | }; 67 | 68 | onLegendOptionsChange = (options: LegendOptions) => { 69 | this.props.onOptionsChange({ ...this.props.options, legend: options }); 70 | }; 71 | 72 | onTooltipOptionsChange = (options: GraphTooltipOptions) => { 73 | this.props.onOptionsChange({ ...this.props.options, tooltipOptions: options }); 74 | }; 75 | 76 | onToggleLines = () => { 77 | this.onGraphOptionsChange({ showLines: !this.props.options.graph.showLines }); 78 | }; 79 | 80 | onToggleBars = () => { 81 | this.onGraphOptionsChange({ showBars: !this.props.options.graph.showBars }); 82 | }; 83 | 84 | onTogglePoints = () => { 85 | this.onGraphOptionsChange({ showPoints: !this.props.options.graph.showPoints }); 86 | }; 87 | 88 | onToggleisStacked = () => { 89 | this.onGraphOptionsChange({ isStacked: !this.props.options.graph.isStacked }); 90 | }; 91 | 92 | onChangeFill = (value: any) => { 93 | this.onGraphOptionsChange({ fill: value.value }); 94 | this.setState({ value: value.value }); 95 | }; 96 | 97 | onChangeFillGradient = (value: any) => { 98 | this.onGraphOptionsChange({ fillGradient: value.value }); 99 | this.setState({ value: value.value }); 100 | }; 101 | 102 | onChangeLineWidth = (value: any) => { 103 | this.onGraphOptionsChange({ lineWidth: value.value }); 104 | this.setState({ value: value.value }); 105 | }; 106 | 107 | onDefaultsChange = (field: FieldConfig) => { 108 | this.props.onOptionsChange({ 109 | ...this.props.options, 110 | fieldOptions: { 111 | ...this.props.options.fieldOptions, 112 | defaults: field, 113 | }, 114 | }); 115 | }; 116 | 117 | render() { 118 | const { 119 | graph: { showBars, showPoints, showLines, isStacked, lineWidth, fill, fillGradient }, 120 | tooltipOptions: { mode }, 121 | datasourceOptions: { datasource, input_bucket, output_bucket }, 122 | } = this.props.options; 123 | 124 | return ( 125 | <> 126 | 127 |
128 | Loud ML Server 129 | 148 |
149 |

Bucket to get data for ML Model (Equal to Datasource used on Query tab; it should be in Loud ML YAML config)

150 | 151 |
152 | Output Bucket 153 | 162 |
163 |

Specify a bucket to store ML training results (It should be in Loud ML YAML config)

164 |
165 | 166 | 167 |
168 |
Draw Modes
169 | 170 | 171 | 172 |
173 |
174 |
Mode Options
175 |
176 | Fill 177 | { 192 | this.onChangeFillGradient({ value: value.value as any }); 193 | }} 194 | options={[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(t => ({ value: t, label: t }))} 195 | /> 196 |
197 | 198 |
199 | Line Width 200 | { 226 | this.onTooltipOptionsChange({ mode: value.value as any }); 227 | }} 228 | options={[ 229 | { label: 'All series', value: 'multi' }, 230 | { label: 'Single', value: 'single' }, 231 | ]} 232 | /> 233 | 234 | 235 | 236 | 237 | ); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/panel/GraphWithLegend2.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // Libraries 3 | import React from 'react'; 4 | import { css } from 'emotion'; 5 | import { GraphSeriesValue } from '@grafana/data'; 6 | 7 | import { Graph2 } from './Graph2'; 8 | import { LegendRenderOptions, VizLegendItem, LegendDisplayMode } from '@grafana/ui'; 9 | import { Graph, GraphProps, VizLegend } from '@grafana/ui'; 10 | import { CustomScrollbar } from '@grafana/ui'; 11 | import { stylesFactory } from '@grafana/ui'; 12 | 13 | // import { GraphCtrl } from 'grafana/app/plugins/panel/graph'; 14 | 15 | export type SeriesOptionChangeHandler = (label: string, option: TOption) => void; 16 | export type SeriesColorChangeHandler = SeriesOptionChangeHandler; 17 | export type SeriesAxisToggleHandler = SeriesOptionChangeHandler; 18 | 19 | export interface GraphWithLegendProps extends GraphProps, LegendRenderOptions { 20 | isLegendVisible: boolean; 21 | legendDisplayMode: LegendDisplayMode; 22 | sortLegendBy?: string; 23 | sortLegendDesc?: boolean; 24 | onSeriesColorChange?: SeriesColorChangeHandler; 25 | onSeriesAxisToggle?: SeriesAxisToggleHandler; 26 | onSeriesToggle?: (label: string, event: React.MouseEvent) => void; 27 | onToggleSort: (sortBy: string) => void; 28 | } 29 | 30 | const getGraphWithLegendStyles = stylesFactory(({ placement }: GraphWithLegendProps) => ({ 31 | wrapper: css` 32 | display: flex; 33 | flex-direction: ${placement === 'under' ? 'column' : 'row'}; 34 | height: 100%; 35 | `, 36 | graphContainer: css` 37 | min-height: 65%; 38 | flex-grow: 1; 39 | `, 40 | legendContainer: css` 41 | padding: 25px 0; 42 | max-height: ${placement === 'under' ? '35%' : 'none'}; 43 | `, 44 | })); 45 | 46 | const shouldHideLegendItem = (data: GraphSeriesValue[][], hideEmpty = false, hideZero = false) => { 47 | const isZeroOnlySeries = data.reduce((acc, current) => acc + (current[1] || 0), 0) === 0; 48 | const isNullOnlySeries = !data.reduce((acc, current) => acc && current[1] !== null, true); 49 | 50 | return (hideEmpty && isNullOnlySeries) || (hideZero && isZeroOnlySeries); 51 | }; 52 | 53 | export const GraphWithLegend2: React.FunctionComponent = (props: GraphWithLegendProps) => { 54 | const { 55 | series, 56 | timeRange, 57 | width, 58 | height, 59 | showBars, 60 | showLines, 61 | showPoints, 62 | sortLegendBy, 63 | sortLegendDesc, 64 | isLegendVisible, 65 | legendDisplayMode, 66 | placement, 67 | onSeriesAxisToggle, 68 | onSeriesColorChange, 69 | onSeriesToggle, 70 | onToggleSort, 71 | hideEmpty, 72 | hideZero, 73 | isStacked, 74 | lineWidth, 75 | fill, 76 | fillGradient, 77 | onHorizontalRegionSelected, 78 | timeZone, 79 | children, 80 | panelChrome, 81 | } = props; 82 | const { graphContainer, wrapper, legendContainer } = getGraphWithLegendStyles(props); 83 | 84 | const legendItems = series.reduce((acc, s) => { 85 | return shouldHideLegendItem(s.data, hideEmpty, hideZero) 86 | ? acc 87 | : acc.concat([ 88 | { 89 | label: s.label, 90 | color: s.color || '', 91 | disabled: !s.isVisible, 92 | yAxis: s.yAxis.index, 93 | getDisplayValues: () => s.info || [], 94 | }, 95 | ]); 96 | }, []); 97 | 98 | return ( 99 |
100 |
101 | 118 | {children} 119 | 120 |
121 | 122 |
123 | {isLegendVisible && ( 124 | 125 | { 132 | if (onSeriesToggle) { 133 | onSeriesToggle(item.label, event); 134 | } 135 | }} 136 | onSeriesColorChange={onSeriesColorChange} 137 | onToggleSort={onToggleSort} 138 | /> 139 | 140 | )} 141 |
142 |
143 | ); 144 | }; 145 | -------------------------------------------------------------------------------- /src/panel/extractors.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /*eslint-disable */ 3 | // Data extractor functions 4 | // to parse selected data/queries into features, groupBy and params for Loud ML Model 5 | 6 | import { 7 | DEFAULT_MODEL, 8 | } from '../datasource/types'; 9 | 10 | export function extract_tooltip_feature(target: any): any { 11 | if (target.select) { 12 | // InfluxDB or PostgreSQL or so 13 | if (target.measurement) { 14 | // InfluxDB 15 | return target.measurement + ": " + target.select 16 | .map(o => _formatFeature(o)) 17 | .join(', '); 18 | // return target.measurement + ": " + _formatFeature(target.select[0]) 19 | } 20 | 21 | if (target.table) { 22 | // PostgreSQL 23 | return target.table + ": " + target.select[0] 24 | .filter(o => o.type==='column') 25 | .map(o => o.params.join(', ')).join('; ') 26 | } 27 | } 28 | 29 | if (target.metric) { 30 | // OpenTSDB or so 31 | return target.metric 32 | } 33 | 34 | if (target.expr) { 35 | // Prometheus or so 36 | return target.expr 37 | } 38 | 39 | if (target.query && target.bucketAggs && target.metrics && target.metrics.length > 0) { 40 | // Elasticsearch or so 41 | return target.metrics[0].type + ": " + target.metrics[0].field 42 | } 43 | } 44 | 45 | export function extract_group_by(target: any): any { 46 | if (target.groupBy) { 47 | // InfluxDB or so 48 | let res = _formatGroupBy(target.groupBy); 49 | if (res == "time: $__interval") { 50 | return "time: " + DEFAULT_MODEL.interval; 51 | } 52 | return res; 53 | } 54 | 55 | if (target.group) { 56 | // PostgreSQL or so 57 | return target.group 58 | .filter(o => o.type==='time') 59 | .map(o => [o.type, o.params[0]].join(': ')).join(', ') 60 | } 61 | 62 | if (target.aggregator && target.downsampleInterval && target.downsampleAggregator) { 63 | // OpenTSDB or so 64 | return target.aggregator + ":" + target.downsampleInterval + "-" + target.downsampleAggregator + "-nan" 65 | } 66 | 67 | if (target.expr) { 68 | // Prometheus or so 69 | return target.interval || "auto" 70 | } 71 | 72 | if (target.query && target.bucketAggs && target.bucketAggs.length>0) { 73 | // Elasticsearch or so 74 | if (target.bucketAggs[0].settings) { 75 | return target.bucketAggs[0].settings.interval; 76 | } 77 | return "auto" 78 | } 79 | } 80 | 81 | export function extract_fill_value(target: any) { 82 | if (target.groupBy) { 83 | // InfluxDB or so 84 | const f = _formatFillValue(target.groupBy) 85 | 86 | if (f == "null") { 87 | return null; 88 | } 89 | if (f == "none") { 90 | return null; 91 | } 92 | 93 | return f; 94 | } 95 | 96 | if (target.group) { 97 | // PostgreSQL or so 98 | return target.group 99 | .filter(o => o.type==='time') 100 | .map(o => o.params[1]).join(', ') 101 | } 102 | 103 | // OpenTSDB or Prometheus or so 104 | return null; 105 | } 106 | 107 | export function extract_format_tags(target: any) { 108 | if (target.tags && target.tags.map) { 109 | // InfluxDB or so 110 | return target.tags.map(o => [o.key, o.operator, o.value].join(' ')).join(', ') 111 | } 112 | 113 | // OpenTSDB or so 114 | // 1 tag selected 115 | if (target.currentTagKey && target.currentTagValue) { 116 | return target.currentTagKey + " = " + target.currentTagValue 117 | } 118 | // 2 and more tags 119 | if (target.tags && typeof target.tags === 'object') { 120 | let res = []; 121 | for (const key in target.tags) { 122 | if (target.tags.hasOwnProperty(key)) { 123 | res.push(key + " = " + target.tags[key]); 124 | } 125 | } 126 | return res.join(', '); 127 | } 128 | 129 | if (target.where) { 130 | // PostgreSQL or so 131 | return target.where 132 | .filter(o => o.type==='expression') 133 | .map(o => o.params.join(' ')).join(', ') 134 | } 135 | 136 | if (target.query) { 137 | // Elasticsearch or so 138 | return target.query 139 | } 140 | } 141 | 142 | export function extract_is_valid(target: any) { 143 | if (target.select) { 144 | // InfluxDB or so 145 | return true 146 | } 147 | 148 | if (target.metric) { 149 | // OpenTSDB or so 150 | return true 151 | } 152 | 153 | if (target.expr) { 154 | // Prometheus or so 155 | return true 156 | } 157 | 158 | if (target.query && target.bucketAggs && target.metrics && target.metrics.length > 0) { 159 | // Elasticsearch or so 160 | return true 161 | } 162 | } 163 | 164 | export function extract_model_database(datasource: any) { 165 | // console.log(datasource); 166 | if (datasource.database) { 167 | // InfluxDB 168 | return datasource.database; 169 | } 170 | 171 | if (datasource.index) { 172 | // Elasticsearch 173 | return datasource.index; 174 | } 175 | 176 | return datasource.name.toLowerCase().replace(/-/g, "_"); 177 | } 178 | 179 | export function extract_model_measurement(target: any) { 180 | if (target.measurement) { 181 | // InfluxDB or so 182 | return target.measurement; 183 | } 184 | 185 | if (target.metric) { 186 | // OpenTSDB or so 187 | return target.metric; 188 | } 189 | 190 | if (target.query && target.bucketAggs && target.metrics && target.metrics.length > 0) { 191 | // Elasticsearch or so 192 | return target.metrics[0].type; 193 | } 194 | 195 | return "auto"; 196 | } 197 | 198 | export function extract_model_select(target: any, field: any) { 199 | if (target.select) { 200 | // InfluxDB or so 201 | return _formatSelect(field); 202 | } 203 | 204 | if (target.metric && target.aggregator) { 205 | // OpenTSDB or so 206 | return target.aggregator + "_" + target.metric.replace(/\./g, "_"); 207 | } 208 | 209 | if (target.query && target.bucketAggs && target.metrics && target.metrics.length > 0) { 210 | // Elasticsearch or so 211 | return target.metrics[0].type + "_" + target.metrics[0].field.replace(/\./g, "_"); 212 | } 213 | 214 | if (target.expr) { 215 | // Prometheus or so 216 | return string_to_slug(target.expr); 217 | } 218 | } 219 | 220 | export function extract_model_feature(target: any, field: any) { 221 | if (target.select) { 222 | // InfluxDB or so 223 | return _get_feature(field) 224 | } 225 | 226 | if (target.metric) { 227 | // OpenTSDB or so 228 | return target.metric 229 | } 230 | 231 | if (target.query && target.bucketAggs && target.metrics && target.metrics.length > 0) { 232 | // Elasticsearch or so 233 | return target.metrics[0].field 234 | } 235 | 236 | if (target.expr) { 237 | // Prometheus or so 238 | return target.expr 239 | } 240 | } 241 | 242 | export function extract_model_func(target: any, field: any) { 243 | if (target.select) { 244 | // InfluxDB or so 245 | return _get_func(field) 246 | } 247 | 248 | if (target.aggregator) { 249 | // OpenTSDB or so 250 | return target.aggregator 251 | } 252 | 253 | if (target.query && target.bucketAggs && target.metrics && target.metrics.length > 0) { 254 | // Elasticsearch or so 255 | return target.metrics[0].type 256 | } 257 | 258 | return "avg" 259 | } 260 | 261 | export function extract_model_fill(target: any) { 262 | if (target.groupBy) { 263 | // InfluxDB or so 264 | const f = _get_fill(target.groupBy); 265 | if (f == "0") { 266 | return 0; 267 | } 268 | if (f == "null") { 269 | return null; 270 | } 271 | if (f == "none") { 272 | return null; 273 | } 274 | return f; 275 | } 276 | // OpenTSDB or so 277 | return null; // TODO 278 | } 279 | 280 | export function extract_model_time_format(target: any) { 281 | if (target.groupBy) { 282 | // InfluxDB or so 283 | let res = _formatTime(target.groupBy); 284 | // console.log(res); 285 | if (res == "time_$__interval" ) { 286 | return DEFAULT_MODEL.interval; 287 | } 288 | return res; 289 | } else { 290 | // OpenTSDB or Prometheus or so 291 | return target.downsampleInterval || target.interval || "auto" 292 | } 293 | } 294 | 295 | export function extract_model_time(target: any) { 296 | if (target.groupBy) { 297 | // InfluxDB or so 298 | let res = _get_time(target.groupBy); 299 | // console.log(res); 300 | if (res == "$__interval" ) { 301 | return DEFAULT_MODEL.interval; 302 | } 303 | return res; 304 | } 305 | 306 | if (target.query && target.bucketAggs && target.bucketAggs.length > 0 && target.bucketAggs[0].settings) { 307 | // Elasticsearch or so 308 | return target.bucketAggs[0].settings.interval 309 | } 310 | 311 | // else - OpenTSDB or Prometheus or so 312 | return target.downsampleInterval || target.interval || "20m" 313 | } 314 | 315 | export function extract_model_tags(target: any) { 316 | if (target.tags && target.tags.map) { 317 | // InfluxDB or so 318 | return _formatTags(target.tags) 319 | } 320 | 321 | // OpenTSDB or so 322 | // 1 tag selected 323 | if (target.currentTagKey && target.currentTagValue) { 324 | return target.currentTagKey + "_" + target.currentTagValue 325 | } 326 | // 2 and more tags 327 | if (target.tags && typeof target.tags === 'object') { 328 | let res = []; 329 | for (const key in target.tags) { 330 | if (target.tags.hasOwnProperty(key)) { 331 | res.push(key + "_" + target.tags[key]); 332 | } 333 | } 334 | return res.join('_') 335 | } 336 | 337 | return "" 338 | } 339 | 340 | export function extract_model_tags_map(target: any) { 341 | if (target.tags && target.tags.map) { 342 | // InfluxDB or so 343 | return target.tags.map( 344 | (tag) => ({ 345 | tag: tag.key, 346 | value: tag.value, 347 | }) 348 | ) 349 | } 350 | 351 | // OpenTSDB or so 352 | // 1 tag selected 353 | if (target.currentTagKey && target.currentTagValue) { 354 | return { 355 | tag: target.currentTagKey, 356 | value: target.currentTagValue 357 | } 358 | } 359 | // 2 and more tags 360 | if (target.tags && typeof target.tags === 'object') { 361 | let res = []; 362 | for (const key in target.tags) { 363 | if (target.tags.hasOwnProperty(key)) { 364 | res.push({tag: key, value: target.tags[key]}); 365 | } 366 | } 367 | return res 368 | } 369 | 370 | return [] 371 | } 372 | 373 | // Internal parser functions ------------------------------------------------- 374 | 375 | function _formatFeature(value) { 376 | // window.console.log('Feature Value', value); 377 | const selectField = value.filter(o => o.type==='field'); 378 | return selectField.map(o => o.params.join(', ')).join('; ') 379 | } 380 | 381 | function _formatGroupBy(value: any) { 382 | // window.console.log('Group By Value', value); 383 | const groupBy = value.filter(o => o.type==='time'); 384 | return groupBy.map(o => [o.type, o.params].join(': ')).join(', ') 385 | } 386 | 387 | function _formatFillValue(value: any) { 388 | // window.console.log('Fill Value', value); 389 | const fill = value.filter(o => o.type==='fill'); 390 | return fill.map(o => [o.type, o.params].join(': ')).join(', ') 391 | } 392 | 393 | function _formatSelect(value: any) { 394 | const selectFunc = value.filter(o => o.type!=='field'); 395 | const selectField = value.filter(o => o.type==='field'); 396 | 397 | return selectFunc.map(o => o.type).join('_') + '_' + selectField.map(o => o.params.join('_')).join('_') 398 | } 399 | 400 | function _formatTime(value: any) { 401 | const groupBy = value.filter(o => o.type==='time'); 402 | return groupBy.map(o => [o.type, o.params].join('_')).join('_') 403 | } 404 | 405 | function _get_feature(value: any) { 406 | const field = value.filter(o => o.type==='field'); 407 | if (field.length === 0) { 408 | // TODO: check how we ended up with empty field and allowed user to click ML Button 409 | return ""; 410 | } 411 | return field[0].params[0]; 412 | } 413 | 414 | function _get_func(value: any) { 415 | const func = value.filter(o => o.type!=='field'); 416 | if (func.length === 0) { 417 | return ""; 418 | } 419 | return func[0].type; 420 | } 421 | 422 | function _get_fill(value: any) { 423 | const fill = value.filter(o => o.type==='fill'); 424 | if (fill.length === 0) { 425 | return "null"; 426 | } 427 | return fill[0].params[0]; 428 | } 429 | 430 | function _get_time(value: any) { 431 | const time = value.filter(o => o.type==='time'); 432 | if (time.length !== 1) { 433 | return DEFAULT_MODEL.interval; 434 | } 435 | return time[0].params[0]; 436 | } 437 | 438 | function _formatTags(value: any) { 439 | return value.map(o => [o.key, o.value].join('_')).join('_') 440 | } 441 | 442 | function string_to_slug(str: any) { 443 | str = str.replace(/^\s+|\s+$/g, ''); 444 | str = str.toLowerCase(); 445 | 446 | // remove accents, swap ñ for n, etc 447 | var from = "àáäâèéëêìíïîòóöôùúüûñç·/_,:;"; 448 | var to = "aaaaeeeeiiiioooouuuunc------"; 449 | for (var i=0, l=from.length ; i { 34 | const graphs: GraphSeriesXY[] = []; 35 | 36 | const displayProcessor = getDisplayProcessor({ 37 | field: { 38 | config: { 39 | unit: fieldOptions?.defaults?.unit, 40 | decimals: legendOptions.decimals, 41 | }, 42 | }, 43 | }); 44 | 45 | let fieldColumnIndex = -1; 46 | for (const series of dataFrames) { 47 | const { timeField } = getTimeField(series); 48 | if (!timeField) { 49 | continue; 50 | } 51 | 52 | for (const field of series.fields) { 53 | if (field.type !== FieldType.number) { 54 | continue; 55 | } 56 | // Storing index of series field for future inspection 57 | fieldColumnIndex++; 58 | 59 | // Use external calculator just to make sure it works :) 60 | const points = getFlotPairs({ 61 | xField: timeField, 62 | yField: field, 63 | nullValueMode: NullValueMode.Null, 64 | }); 65 | 66 | if (points.length > 0) { 67 | const seriesStats = reduceField({ field, reducers: legendOptions.stats }); 68 | let statsDisplayValues: DisplayValue[]; 69 | 70 | if (legendOptions.stats) { 71 | statsDisplayValues = legendOptions.stats.map(stat => { 72 | const statDisplayValue = displayProcessor(seriesStats[stat]); 73 | 74 | return { 75 | ...statDisplayValue, 76 | title: stat, 77 | }; 78 | }); 79 | } 80 | 81 | let color: FieldColor; 82 | if (seriesOptions[field.name] && seriesOptions[field.name].color) { 83 | // Case when panel has settings provided via SeriesOptions, i.e. graph panel 84 | color = { 85 | mode: FieldColorMode.Fixed, 86 | fixedColor: seriesOptions[field.name].color, 87 | }; 88 | } else if (field.config && field.config.color) { 89 | // Case when color settings are set on field, i.e. Explore logs histogram (see makeSeriesForLogs) 90 | color = field.config.color; 91 | } else { 92 | color = { 93 | mode: FieldColorMode.Fixed, 94 | fixedColor: colors[graphs.length % colors.length], 95 | }; 96 | } 97 | 98 | field.config = fieldOptions 99 | ? { 100 | ...field.config, 101 | unit: fieldOptions.defaults.unit, 102 | decimals: fieldOptions.defaults.decimals, 103 | color, 104 | } 105 | : { ...field.config, color }; 106 | 107 | field.display = getDisplayProcessor({ field }); 108 | 109 | // Time step is used to determine bars width when graph is rendered as bar chart 110 | const timeStep = getSeriesTimeStep(timeField); 111 | const useMsDateFormat = hasMsResolution(timeField); 112 | 113 | timeField.display = getDisplayProcessor({ 114 | timeZone, 115 | field: { 116 | ...timeField, 117 | type: timeField.type, 118 | config: { 119 | unit: `time:${useMsDateFormat ? MS_DATE_TIME_FORMAT : DEFAULT_DATE_TIME_FORMAT}`, 120 | }, 121 | }, 122 | }); 123 | 124 | graphs.push({ 125 | label: field.name, 126 | data: points, 127 | color: field.config.color?.fixedColor, 128 | info: statsDisplayValues, 129 | isVisible: true, 130 | yAxis: { 131 | index: (seriesOptions[field.name] && seriesOptions[field.name].yAxis) || 1, 132 | }, 133 | // This index is used later on to retrieve appropriate series/time for X and Y axes 134 | seriesIndex: fieldColumnIndex, 135 | timeField: { ...timeField }, 136 | valueField: { ...field }, 137 | timeStep, 138 | }); 139 | } 140 | } 141 | } 142 | 143 | return graphs; 144 | }; 145 | -------------------------------------------------------------------------------- /src/panel/module.test.ts: -------------------------------------------------------------------------------- 1 | // Just a stub test 2 | describe('placeholder test', () => { 3 | it('should return true', () => { 4 | expect(true).toBeTruthy(); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /src/panel/module.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { FieldConfigProperty, PanelPlugin } from '@grafana/data'; 3 | import { getDataSourceSrv, GrafanaBootConfig } from '@grafana/runtime'; 4 | 5 | import { GraphPanelEditor } from './GraphPanelEditor'; 6 | import { GraphPanel } from './GraphPanel'; 7 | import { Options, defaults } from './types'; 8 | 9 | // function grafanaVersion(): string | null { 10 | // if(_.has(window, 'grafanaBootData.settings.buildInfo.version')) { 11 | // return window.grafanaBootData.settings.buildInfo.version; 12 | // } 13 | // return null; 14 | // } 15 | // console.log(GrafanaBootConfig); 16 | 17 | // export const plugin = new PanelPlugin(GraphPanel).setDefaults(defaults).setEditor(GraphPanelEditor); 18 | 19 | function datasourcesList() { 20 | var res = new Array({ label: 'Not selected', value: '' }); 21 | var datasources = getDataSourceSrv().getMetricSources(); 22 | 23 | datasources.forEach(function(val) { 24 | if (val.meta.id === 'loudml-datasource') { 25 | res.push({ label: val.name, value: val.value }); 26 | } 27 | }); 28 | 29 | return res; 30 | } 31 | 32 | export const plugin = new PanelPlugin(GraphPanel) 33 | .useFieldConfig({ standardOptions: [FieldConfigProperty.Unit, FieldConfigProperty.Decimals] }) 34 | .setPanelOptions(builder => { 35 | builder 36 | .addSelect({ 37 | path: 'datasourceOptions.datasource', 38 | name: 'Loud ML Server', 39 | description: '', 40 | settings: { 41 | options: datasourcesList(), 42 | }, 43 | defaultValue: '', 44 | }) 45 | .addTextInput({ 46 | path: 'datasourceOptions.input_bucket', 47 | name: 'Input Bucket', 48 | description: 'Datasource/Database used in Query', 49 | defaultValue: '', 50 | }) 51 | .addTextInput({ 52 | path: 'datasourceOptions.output_bucket', 53 | name: 'Output Bucket', 54 | description: 'Database to store ML Model training results', 55 | defaultValue: 'loudml', 56 | }) 57 | .addBooleanSwitch({ 58 | path: 'graph.showBars', 59 | name: 'Bars', 60 | description: '', 61 | defaultValue: false, 62 | }) 63 | .addBooleanSwitch({ 64 | path: 'graph.showLines', 65 | name: 'Lines', 66 | description: '', 67 | defaultValue: true, 68 | }) 69 | .addSelect({ 70 | path: 'graph.lineWidth', 71 | name: 'Line Width', 72 | description: '', 73 | settings: { 74 | options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(t => ({ value: t, label: t })), 75 | }, 76 | defaultValue: 1, 77 | }) 78 | .addSelect({ 79 | path: 'graph.fill', 80 | name: 'Area fill', 81 | description: '', 82 | settings: { 83 | options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(t => ({ value: t, label: t })), 84 | }, 85 | defaultValue: 1, 86 | }) 87 | .addSelect({ 88 | path: 'graph.fillGradient', 89 | name: 'Fill Gradient', 90 | description: '', 91 | settings: { 92 | options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(t => ({ value: t, label: t })), 93 | }, 94 | defaultValue: 0, 95 | }) 96 | .addBooleanSwitch({ 97 | path: 'graph.showPoints', 98 | name: 'Poins', 99 | description: '', 100 | defaultValue: false, 101 | }) 102 | .addBooleanSwitch({ 103 | path: 'graph.isStacked', 104 | name: 'Stack', 105 | description: '', 106 | defaultValue: false, 107 | }) 108 | .addBooleanSwitch({ 109 | path: 'legend.isVisible', 110 | name: 'Show legend', 111 | description: '', 112 | defaultValue: true, 113 | }) 114 | .addBooleanSwitch({ 115 | path: 'legend.asTable', 116 | name: 'Display legend as table', 117 | description: '', 118 | defaultValue: false, 119 | }) 120 | .addRadio({ 121 | path: 'legend.placement', 122 | name: 'Legend placement', 123 | description: '', 124 | defaultValue: 'under', 125 | settings: { 126 | options: [ 127 | { value: 'under', label: 'Below graph' }, 128 | { value: 'right', label: 'Right to the graph' }, 129 | ], 130 | }, 131 | }) 132 | .addRadio({ 133 | path: 'tooltipOptions.mode', 134 | name: 'Tooltip mode', 135 | description: '', 136 | defaultValue: 'single', 137 | settings: { 138 | options: [ 139 | { value: 'single', label: 'Single series' }, 140 | { value: 'multi', label: 'All series' }, 141 | ], 142 | }, 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/panel/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "panel", 3 | "name": "Loud ML Graph", 4 | "id": "loudml-graph-panel", 5 | "info": { 6 | "logos": { 7 | "small": "../loudml-grafana-app/img/logo.png", 8 | "large": "../loudml-grafana-app/img/logo.png" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/panel/types.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { LegendOptions, GraphTooltipOptions } from '@grafana/ui'; 3 | import { GraphLegendEditorLegendOptions } from './GraphLegendEditor'; 4 | import { YAxis, ReducerID, FieldDisplayOptions, ThresholdsMode, DataSourceSelectItem } from '@grafana/data'; 5 | 6 | export const standardFieldDisplayOptions: FieldDisplayOptions = { 7 | values: false, 8 | calcs: [ReducerID.mean], 9 | defaults: { 10 | thresholds: { 11 | mode: ThresholdsMode.Absolute, 12 | steps: [ 13 | { value: -Infinity, color: 'green' }, 14 | { value: 80, color: 'red' }, // 80% 15 | ], 16 | }, 17 | mappings: [], 18 | }, 19 | overrides: [], 20 | }; 21 | 22 | export interface SeriesOptions { 23 | color?: string; 24 | yAxis?: YAxis; 25 | [key: string]: any; 26 | } 27 | export interface GraphOptions { 28 | showBars: boolean; 29 | showLines: boolean; 30 | showPoints: boolean; 31 | isStacked: boolean; 32 | lineWidth: number; 33 | fill: number; 34 | fillGradient: number; 35 | } 36 | 37 | export interface GraphDatasourceOptions { 38 | datasource?: string; 39 | input_bucket?: string; 40 | output_bucket?: string; 41 | } 42 | 43 | export interface Options { 44 | graph: GraphOptions; 45 | legend: LegendOptions & GraphLegendEditorLegendOptions; 46 | series: { 47 | [alias: string]: SeriesOptions; 48 | }; 49 | fieldOptions: FieldDisplayOptions; 50 | tooltipOptions: GraphTooltipOptions; 51 | datasourceOptions: GraphDatasourceOptions; 52 | modelName?: string; 53 | } 54 | 55 | export const defaults: Options = { 56 | graph: { 57 | showBars: false, 58 | showLines: true, 59 | showPoints: false, 60 | isStacked: false, 61 | lineWidth: 1, 62 | fill: 1, 63 | fillGradient: 0, 64 | }, 65 | legend: { 66 | asTable: false, 67 | isVisible: true, 68 | placement: 'under', 69 | }, 70 | series: {}, 71 | fieldOptions: { ...standardFieldDisplayOptions }, 72 | tooltipOptions: { mode: 'single' }, 73 | datasourceOptions: { 74 | datasource: '', 75 | input_bucket: '', 76 | output_bucket: 'loudml', 77 | }, 78 | modelName: '', 79 | }; 80 | -------------------------------------------------------------------------------- /src/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "app", 3 | "name": "Loud ML app", 4 | "id": "loudml-grafana-app", 5 | 6 | "info": { 7 | "description": "Loud ML app: Visualization panel and datasource for Grafana to connect with Loud ML AI", 8 | "author": { 9 | "name": "Volodymyr Sergeyev" 10 | }, 11 | "keywords": ["LoudML", "ML", "AI"], 12 | "logos": { 13 | "small": "img/logo.png", 14 | "large": "img/logo.png" 15 | }, 16 | "links": [ 17 | {"name": "Website", "url": "https://github.com/vsergeyev/loudml-grafana-app"}, 18 | {"name": "License", "url": "https://github.com/vsergeyev/loudml-grafana-app/blob/master/LICENSE"} 19 | ], 20 | "screenshots": [ 21 | {"name": "LoudML graph panel", "path": "img/loudml_grafana_panel.png"}, 22 | {"name": "LoudML datasource", "path": "img/loudml_grafana_datasource.png"} 23 | ], 24 | "version": "1.7.2", 25 | "updated": "%TODAY%" 26 | }, 27 | 28 | "includes": [ 29 | { "type": "panel", "name": "Loud ML Graph Panel" }, 30 | { "type": "datasource", "name": "Loud ML Datasource" } 31 | ], 32 | 33 | "dependencies": { 34 | "grafanaDependency": ">=7.0.0", 35 | "plugins": [] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/src/utils.ts -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@grafana/toolkit/src/config/tsconfig.plugin.json", 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "alwaysStrict": false, 6 | "baseUrl": "./src", 7 | "typeRoots": ["./node_modules/@types"], 8 | "skipLibCheck": true, 9 | "suppressImplicitAnyIndexErrors": true, 10 | "noImplicitReturns": false, 11 | "noImplicitThis": false, 12 | "noImplicitUseStrict": false, 13 | "noImplicitAny": false, 14 | "noUnusedLocals": false, 15 | "allowUnreachableCode": true, 16 | "module": "commonjs", 17 | "target": "es6", 18 | "jsx": "react", 19 | "strict": false, 20 | "paths": { 21 | "@": ["."] 22 | } 23 | }, 24 | "linterOptions": { 25 | "exclude": ["./dist/**", "./node_modules/**", "grafana/**"] 26 | }, 27 | "exclude": ["node_modules", "grafana"], 28 | } 29 | --------------------------------------------------------------------------------