diff --git a/.gitignore b/.gitignore index e0a0048b514..c1491666d88 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,12 @@ src/css/*.min.css *.sublime-workspace *.swp .idea/ +*.iml /data/* /bin/* /grafana-pro +/grafana grafana.custom.ini +fig.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d5721604c4..989dab3bb99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,11 @@ - [Issue #1331](https://github.com/grafana/grafana/issues/1331). Graph & Singlestat: New axis/unit format selector and more units (kbytes, Joule, Watt, eV), and new design for graph axis & grid tab and single stat options tab views - [Issue #1241](https://github.com/grafana/grafana/issues/1242). Timepicker: New option in timepicker (under dashboard settings), to change ``now`` to be for example ``now-1m``, usefull when you want to ignore last minute because it contains incomplete data - [Issue #171](https://github.com/grafana/grafana/issues/171). Panel: Different time periods, panels can override dashboard relative time and/or add a time shift +- [Issue #1488](https://github.com/grafana/grafana/issues/1488). Dashboard: Clone dashboard / Save as **Enhancements** +- [Issue #1366](https://github.com/grafana/grafana/issues/1366). Graph & Singlestat: Support for additional units, Fahrenheit (°F) and Celsius (°C), Humidity (%H), kW, watt-hour (Wh), kilowatt-hour (kWh), velocities (m/s, km/h, mpg, knot) +- [Issue #978](https://github.com/grafana/grafana/issues/978). Graph: Shared tooltip improvement, can now support metrics of different resolution/intervals - [Issue #1297](https://github.com/grafana/grafana/issues/1297). Graphite: Added cumulative and minimumBelow graphite functions - [Issue #1296](https://github.com/grafana/grafana/issues/1296). InfluxDB: Auto escape column names with special characters. Thanks @steven-aerts - [Issue #1321](https://github.com/grafana/grafana/issues/1321). SingleStatPanel: You can now use template variables in pre & postfix @@ -18,6 +21,15 @@ - [Issue #1372](https://github.com/grafana/grafana/issues/1372). Graphite: Fix for nested complex queries, where a query references a query that references another query (ie the #[A-Z] syntax) - [Issue #1363](https://github.com/grafana/grafana/issues/1363). Templating: Fix to allow custom template variables to contain white space, now only splits on ',' - [Issue #1359](https://github.com/grafana/grafana/issues/1359). Graph: Fix for all series tooltip showing series with all null values when ``Hide Empty`` option is enabled +- [Issue #1497](https://github.com/grafana/grafana/issues/1497). Dashboard: Fixed memory leak when switching dashboards + +**Changes** +- Dashboard title change & save will no longer create a new dashboard, it will just change the title. + +**OpenTSDB breaking change** +- [Issue #1438](https://github.com/grafana/grafana/issues/1438). OpenTSDB: Automatic downsample interval passed to OpenTSDB (depends on timespan and graph width) +- NOTICE, Downsampling is now enabled by default, so if you have not picked a downsample aggregator in your metric query do so or your graphs will be missleading +- This will make Grafana a lot quicker for OpenTSDB users when viewing large time spans without having to change the downsample interval manually. **Tech** - [Issue #1311](https://github.com/grafana/grafana/issues/1311). Tech: Updated Font-Awesome from 3.2 to 4.2 diff --git a/README.md b/README.md index 48fc24ce0c7..f3a399597a5 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,12 @@ Graphite, InfluxDB & OpenTSDB. Grafana 2.0 comes with a backend written in Go. It is not ready for production use yet as there is still a lot of small issues to fix and polish missing. But feedback on what is done and bug reports would be greatly appreciated. +## Try it out with docker +``` +docker run -i -p 3000:3000 grafana/grafana:develop +``` +The default admin user is admin/admin. + ## building and running ``` @@ -26,18 +32,13 @@ Building ``` cd $GOPATH/src/github.com/grafana/grafana git checkout -t origin/develop -go run build.go setup (only needed once to install godep) -go run build.go build +go run build.go setup (only needed once to install godep) +godep restore (will pull down all golang lib dependecies in your current GOPATH) +go build . ``` -For quicker builds: - -``` -godep restore (will pull down all golang lib dependecies in your current GOPATH) -go build -o ./bin/grafana . -``` - -To build less to css for frontend: +To build less to css for the frontend you will need a recent version of of node (v0.12.0), +npm (v2.5.0) and grunt (v0.4.5). Run the following: ``` npm install diff --git a/benchmarks/ab/ab_test.sh b/benchmarks/ab/ab_test.sh new file mode 100755 index 00000000000..c52d24a179d --- /dev/null +++ b/benchmarks/ab/ab_test.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +ab -n 20000 -c 100 -H "Authorization: Bearer vEustw23NSOZ27y3zlj28ZL3B7BpBk5kqR85DOfT5AwiS3nCi33dnsk6nhvXhZdn" \ + http://localhost:3000/api/dashboards/db/dash1 diff --git a/conf/grafana.ini b/conf/grafana.ini index 2d643191b4a..491dfd1063b 100644 --- a/conf/grafana.ini +++ b/conf/grafana.ini @@ -2,12 +2,18 @@ app_name = Grafana app_mode = production [server] +; protocol (http or https) protocol = http -domain = localhost -root_url = %(protocol)s://%(domain)s:%(http_port)s/ +; the ip address to bind to, empty will bind to all interfaces http_addr = +; the http port to use http_port = 3000 +; The public facing domain name used to access grafana from a browser +domain = localhost +; the full public facing url +root_url = %(protocol)s://%(domain)s:%(http_port)s/ router_logging = false +; the path relative to the binary where the static (html/js/css) files are placed static_root_path = public enable_gzip = false diff --git a/data/dashboards/annotations.json b/data/dashboards/annotations.json deleted file mode 100644 index 727313e95dd..00000000000 --- a/data/dashboards/annotations.json +++ /dev/null @@ -1,269 +0,0 @@ -{ - "title": "Annotations", - "services": { - "filter": { - "list": [], - "time": { - "from": "now-1h", - "to": "now" - } - } - }, - "rows": [ - { - "title": "Welcome to Grafana", - "height": "350px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 12, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 0, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(apps.fakesite.web_server_02.counters.requests.count,2)" - }, - { - "target": "aliasByNode(apps.fakesite.web_server_01.counters.requests.count,2)" - } - ], - "aliasColors": {}, - "aliasYAxis": {}, - "title": "Amnotations example", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - } - ], - "notice": false - }, - { - "title": "test", - "height": "350px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(apps.fakesite.web_server_02.counters.request_status.code_304.count,5)" - } - ], - "aliasColors": { - "web_server_01": "#1F78C1", - "web_server_02": "#6ED0E0" - }, - "aliasYAxis": {}, - "title": "Annotations example", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "### Annotations\n\n- Annotation is a feature that must be enabled in the dashboards settings / Controls tab / Feature toggles\n- Annotation bar is then visible at the top. \n- Click on the cog to open the Annotations dialog \n- In this dialog you can add or edit annotations \n- Currently only Graphite metrics and Graphite events are supported sources of annotations\n- More datasource options for annotations will be added \n- Click on the annotation name in the bar to toggle the annotation on or off\n\n", - "style": {}, - "title": "Description" - } - ], - "notice": false - } - ], - "editable": true, - "failover": false, - "panel_hints": true, - "style": "dark", - "pulldowns": [ - { - "type": "filtering", - "collapse": false, - "notice": false, - "enable": false - }, - { - "type": "annotations", - "enable": true, - "annotations": [ - { - "name": "deploys", - "type": "graphite metric", - "showLine": true, - "iconColor": "#C0C6BE", - "lineColor": "rgba(253, 54, 54, 0.77)", - "iconSize": 13, - "enable": true, - "target": "alias(apps.fakesite.web_server_01.counters.request_status.code_500.count, 'deployed v1.3')" - }, - { - "name": "puppet apply", - "type": "graphite metric", - "showLine": true, - "iconColor": "#C0C6BE", - "lineColor": "rgba(255, 96, 96, 0.592157)", - "iconSize": 13, - "enable": false, - "target": "alias(apps.fakesite.web_server_02.counters.request_status.code_403.count,'puppet apply')" - } - ] - } - ], - "nav": [ - { - "type": "timepicker", - "collapse": false, - "notice": false, - "enable": true, - "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "now": true - } - ], - "loader": { - "save_gist": false, - "save_elasticsearch": true, - "save_local": true, - "save_default": true, - "save_temp": true, - "save_temp_ttl_enable": true, - "save_temp_ttl": "30d", - "load_gist": false, - "load_elasticsearch": true, - "load_elasticsearch_size": 20, - "load_local": false, - "hide": false - }, - "refresh": false, - "tags": [ - "annotations", - "graphite", - "showcase" - ], - "timezone": "browser" -} \ No newline at end of file diff --git a/data/dashboards/default.json b/data/dashboards/default.json deleted file mode 100644 index 2dc6f36ab62..00000000000 --- a/data/dashboards/default.json +++ /dev/null @@ -1,409 +0,0 @@ -{ - "title": "Grafana Play Home", - "services": { - "filter": { - "list": [], - "time": { - "from": "now-15m", - "to": "now" - } - } - }, - "rows": [ - { - "title": "test", - "height": "190px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "error": false, - "span": 12, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "html", - "content": "

Welcome to grafana demo, playground and interactive tutorial site.

\n\n
\n\t
\n\t\t

Feature showcases

\n\t\t\n\t
\n\t
\n\t\t

Graphite tutorials

\n\t\t\n\t
\n\t
\n\t\t

InfluxDB examples

\n\t\t\n\t
\n
\n\n", - "style": {}, - "title": "Grafana demo site" - } - ], - "notice": false - }, - { - "title": "test", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 3, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(scaleToSeconds(apps.fakesite.*.counters.requests.count,1),2)" - } - ], - "aliasColors": { - "web_server_04": "#3F6833", - "web_server_03": "#508642", - "web_server_02": "#7EB26D", - "web_server_01": "#B7DBAB" - }, - "aliasYAxis": {}, - "title": "server requests", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": true, - "max": true, - "current": true, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "alias(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),'logins')" - }, - { - "target": "alias(timeShift(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),'1h'),'logins (-1 hour)')" - } - ], - "aliasColors": { - "logins": "#7EB26D", - "logins (-1 day)": "#447EBC" - }, - "aliasYAxis": {}, - "title": "logins", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - } - ], - "notice": false - }, - { - "title": "", - "height": "300px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 4, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "bytes", - "none" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 0, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": true, - "max": false, - "current": true, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "alias(scale(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),1000000),'memory')" - }, - { - "target": "alias(scaleToSeconds(apps.fakesite.web_server_01.counters.request_status.code_302.count,1),'cpu')" - } - ], - "aliasColors": { - "cpu": "#E24D42" - }, - "aliasYAxis": { - "cpu": 2 - }, - "title": "Memory / CPU", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "span": 8, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "ms", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": false, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": true, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(statsd.fakesite.timers.ads_timer.*,4)" - } - ], - "aliasColors": { - "upper_75": "#EAB839", - "upper_50": "#7EB26D", - "upper_25": "#BA43A9" - }, - "aliasYAxis": {}, - "title": "client side full page load", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - } - ], - "notice": false - }, - { - "title": "test", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [], - "notice": false - } - ], - "editable": true, - "failover": false, - "panel_hints": true, - "style": "dark", - "pulldowns": [ - { - "type": "filtering", - "collapse": false, - "notice": false, - "enable": false - }, - { - "type": "annotations", - "enable": false - } - ], - "nav": [ - { - "type": "timepicker", - "collapse": false, - "notice": false, - "enable": true, - "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "now": true - } - ], - "loader": { - "save_gist": false, - "save_elasticsearch": true, - "save_local": true, - "save_default": true, - "save_temp": true, - "save_temp_ttl_enable": true, - "save_temp_ttl": "30d", - "load_gist": false, - "load_elasticsearch": true, - "load_elasticsearch_size": 20, - "load_local": false, - "hide": false - }, - "refresh": false, - "tags": [ - "showcase", - "startpage", - "home", - "default" - ], - "timezone": "browser" -} \ No newline at end of file diff --git a/data/dashboards/grafana-play-home.json b/data/dashboards/grafana-play-home.json deleted file mode 100644 index 49061346f2a..00000000000 --- a/data/dashboards/grafana-play-home.json +++ /dev/null @@ -1 +0,0 @@ -{"editable":true,"nav":[{"collapse":false,"enable":true,"notice":false,"now":true,"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"status":"Stable","time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"],"type":"timepicker"}],"pulldowns":[{"collapse":false,"enable":false,"notice":false,"type":"filtering"},{"enable":false,"type":"annotations"}],"rows":[{"collapsable":true,"collapse":false,"editable":true,"height":"190px","notice":false,"panels":[{"content":"\u003ch3 class=\"text-center\"\u003eWelcome to grafana demo, playground and interactive tutorial site.\u003c/h2\u003e\n\n\u003cdiv class=\"row-fluid\"\u003e\n\u0009\u003cdiv class=\"span4\"\u003e\n\u0009\u0009\u003ch4\u003eFeature showcases\u003c/h2\u003e\n\u0009\u0009\u003cul\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/graph-styles.json\"\u003eGraphs styles\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/templated-graphs.json\"\u003eTemplated graphs\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/templated-graphs-nested.json\"\u003eTemplated graphs nested\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/annotations.json\"\u003eAnnotations\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/white-theme.json\"\u003eWhite theme\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u003c/ul\u003e\n\u0009\u003c/div\u003e\n\u0009\u003cdiv class=\"span4\"\u003e\n\u0009\u0009\u003ch4\u003eGraphite tutorials\u003c/h2\u003e\n\u0009\u0009\u003cul\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Graphite introduction (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Basic functions (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Advanced functions (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Tips and tricks (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u003c/ul\u003e\n\u0009\u003c/div\u003e\n\u0009\u003cdiv class=\"span4\"\u003e\n\u0009\u0009\u003ch4\u003eInfluxDB examples\u003c/h2\u003e\n\u0009\u0009\u003cul\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009TODO\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u003c/ul\u003e\n\u0009\u003c/div\u003e\n\u003c/div\u003e\n\n\u003cscript type=\"text/javascript\"\u003e(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){\n(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),\nm=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)\n})(window,document,'script','//www.google-analytics.com/analytics.js','ga');\n\nga('create', 'UA-47280256-1', 'grafana.org');\nga('send', 'pageview');\u003c/script\u003e","editable":true,"error":false,"id":1,"loadingEditor":false,"mode":"html","span":12,"style":{},"title":"Grafana demo site","type":"text"}],"title":"test"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"web_server_01":"#B7DBAB","web_server_02":"#7EB26D","web_server_03":"#508642","web_server_04":"#3F6833"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":3,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":2,"interactive":true,"legend":{"avg":false,"current":false,"max":false,"min":false,"show":true,"total":false,"values":false},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(scaleToSeconds(apps.fakesite.*.counters.requests.count,1),2)"}],"timezone":"browser","title":"server requests","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true},{"aliasColors":{"logins":"#7EB26D","logins (-1 day)":"#447EBC"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":3,"interactive":true,"legend":{"avg":false,"current":true,"max":true,"min":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"alias(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),'logins')"},{"target":"alias(timeShift(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),'1h'),'logins (-1 hour)')"}],"timezone":"browser","title":"logins","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true}],"title":"test"},{"collapsable":true,"collapse":false,"editable":true,"height":"300px","notice":false,"panels":[{"aliasColors":{"cpu":"#E24D42"},"aliasYAxis":{"cpu":2},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":4,"interactive":true,"legend":{"avg":false,"current":true,"max":false,"min":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"alias(scale(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),1000000),'memory')"},{"target":"alias(scaleToSeconds(apps.fakesite.web_server_01.counters.request_status.code_302.count,1),'cpu')"}],"timezone":"browser","title":"Memory / CPU","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["bytes","none"],"zerofill":true},{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":true,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":5,"interactive":true,"legend":{"alignAsTable":true,"avg":true,"current":false,"max":false,"min":false,"rightSide":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":false,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.*,4)"}],"timezone":"browser","title":"client side full page load","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true}],"title":""},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[],"title":"test"}],"style":"dark","tags":["showcase","startpage","home","default"],"templating":{"list":[]},"time":{"from":"now-15m","to":"now"},"timezone":"browser","title":"Grafana Play Home","version":3} \ No newline at end of file diff --git a/data/dashboards/graph-styles.json b/data/dashboards/graph-styles.json deleted file mode 100644 index 4f9147cdf62..00000000000 --- a/data/dashboards/graph-styles.json +++ /dev/null @@ -1,796 +0,0 @@ -{ - "title": "Graph styles", - "services": { - "filter": { - "list": [], - "time": { - "from": "now-15m", - "to": "now" - } - } - }, - "rows": [ - { - "title": "Simple graph", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 0, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(scaleToSeconds(apps.backend.*.counters.requests.count,1),2)" - } - ], - "aliasColors": { - "web_server_04": "#E24D42", - "web_server_03": "#508642", - "web_server_02": "#EAB839", - "web_server_01": "#EF843C" - }, - "aliasYAxis": {}, - "title": "Simple graph", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Simple graph\n- Click on the title and select edit to open edit mode\n- The display styles tab allows you change line width, fill, stacking, and more\n- You can change a series color by clicking the colored line in the legend ", - "style": {}, - "title": "Description" - } - ], - "notice": false - }, - { - "title": "Stacked Graph", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 2, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(scaleToSeconds(apps.fakesite.*.counters.requests.count,1),2)" - } - ], - "aliasColors": { - "web_server_04": "#E24D42", - "web_server_03": "#EF843C", - "web_server_02": "#EAB839", - "web_server_01": "#F2C96D" - }, - "aliasYAxis": {}, - "title": "Stacked lines", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Stacked graph\n- This graph shows stacked series, with area fill and 2px line width\n- We have also added legend values. These can be enabled in the Grid & Axes tab in edit mode. \n- Legend values can be Min, Max, Total, Current and Average", - "style": {}, - "title": "Description" - } - ], - "notice": false - }, - { - "title": "Staircase line", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "ms", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": true, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(statsd.fakesite.timers.ads_timer.upper_90,4)" - }, - { - "target": "aliasByNode(statsd.fakesite.timers.ads_timer.upper_90,4)" - } - ], - "aliasColors": { - "upper_75": "#EAB839", - "upper_50": "#7EB26D", - "upper_25": "#BA43A9" - }, - "aliasYAxis": {}, - "title": "Staircase line", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Staircase & Y-Axis format\n- In display styles tab you can switch to staircase line \n- In Axes & Grid tab you can change to different Y units & formats.\n", - "style": {}, - "title": "Description" - } - ], - "notice": false - }, - { - "title": "Right Y-Axis", - "height": "300px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "bytes", - "none" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 0, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": true, - "max": false, - "current": true, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "alias(scale(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),1000000),'memory')" - }, - { - "target": "alias(scaleToSeconds(apps.fakesite.web_server_02.counters.requests.count,1),'cpu')" - } - ], - "aliasColors": { - "cpu": "#E24D42" - }, - "aliasYAxis": { - "cpu": 2 - }, - "title": "Memory / CPU", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Second Y-Axis\n- Click on the series legend color line to open the color selector\n- In the series color selector popup you can also move the series to the Right-Y axis\n- Multiple Y-Axis are great for showing to related series that have different magnitudes ", - "style": {}, - "title": "Description" - } - ], - "notice": false - }, - { - "title": "test", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "none", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "null", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(apps.fakesite.web_server_01.counters.request_status.code_404.count,4)" - } - ], - "aliasColors": { - "upper_75": "#EAB839", - "upper_50": "#7EB26D", - "upper_25": "#BA43A9" - }, - "aliasYAxis": {}, - "title": "Null point mode", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Null point mode\n- This option under Display styles tab controls how null values are handled\n- The graph to left shows how the default \"null\" looks. \n- __null__ null values are left null and this leaves empty spaces in the graph\n- __null as zero__ null values are drawn as zero values\n- __connected__ null values are ignored and the line jumps directly to the next value.", - "style": {}, - "title": "Description" - } - ], - "notice": false - }, - { - "title": "Thresholds", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "none", - "short" - ], - "grid": { - "max": 700, - "min": 0, - "threshold1": 400, - "threshold2": 600, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 0, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": true, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(apps.fakesite.web_server_01.counters.requests.count,4)" - } - ], - "aliasColors": { - "upper_75": "#EAB839", - "upper_50": "#7EB26D", - "upper_25": "#BA43A9", - "requests": "#6ED0E0" - }, - "aliasYAxis": {}, - "title": "Thresholds", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Thresholds\n- You can define thresholds in the Grid & Axes tab in edit mode \n- You can define one or two thresholds, color is also changeable. \n- You can have lower bound thresholds as well. ", - "style": {}, - "title": "Description" - } - ], - "notice": false - }, - { - "title": "Bars", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "ms", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": false, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": true, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": true, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(statsd.fakesite.timers.ads_timer.*,4)" - } - ], - "aliasColors": { - "upper_75": "#EAB839", - "upper_50": "#7EB26D", - "upper_25": "#BA43A9" - }, - "aliasYAxis": {}, - "title": "Bars", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Bars\n- In display styles tab you can switch from line to bars\n- The width of the bar is relative to the pixel width between two values\n", - "style": {}, - "title": "Description" - } - ], - "notice": false - }, - { - "title": "Graphite PNG", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "ms", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": false, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 2, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(apps.backend.*.counters.requests.count,2)" - } - ], - "aliasColors": { - "upper_75": "#EAB839", - "upper_50": "#7EB26D", - "upper_25": "#BA43A9" - }, - "aliasYAxis": {}, - "title": "Graphite PNG", - "datasource": null, - "renderer": "png", - "annotate": { - "enable": false - } - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Graphite PNG support\n- You can switch from client side rendering to graphite's server side PNG rendering\n- You cannot click and drag to zoom in in this render mode.\n", - "style": {}, - "title": "Description" - } - ], - "notice": false - } - ], - "editable": true, - "failover": false, - "panel_hints": true, - "style": "dark", - "pulldowns": [ - { - "type": "filtering", - "collapse": false, - "notice": false, - "enable": false - }, - { - "type": "annotations", - "enable": false - } - ], - "nav": [ - { - "type": "timepicker", - "collapse": false, - "notice": false, - "enable": true, - "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "now": true - } - ], - "loader": { - "save_gist": false, - "save_elasticsearch": true, - "save_local": true, - "save_default": true, - "save_temp": true, - "save_temp_ttl_enable": true, - "save_temp_ttl": "30d", - "load_gist": false, - "load_elasticsearch": true, - "load_elasticsearch_size": 20, - "load_local": false, - "hide": false - }, - "refresh": false, - "tags": [ - "showcase", - "annotations" - ], - "timezone": "browser" -} \ No newline at end of file diff --git a/data/dashboards/graph_styles_collapsed.json b/data/dashboards/graph_styles_collapsed.json deleted file mode 100644 index 0467ac0d335..00000000000 --- a/data/dashboards/graph_styles_collapsed.json +++ /dev/null @@ -1 +0,0 @@ -{"editable":true,"nav":[{"collapse":false,"enable":true,"notice":false,"now":false,"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"status":"Stable","time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"],"type":"timepicker"}],"pulldowns":[{"collapse":false,"enable":false,"notice":false,"type":"filtering"},{"enable":false,"type":"annotations"}],"refresh":false,"rows":[{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":null,"leftMin":null,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":17,"legend":{"avg":false,"current":false,"max":false,"min":false,"show":true,"total":false,"values":false},"lines":true,"linewidth":1,"nullPointMode":"connected","percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":12,"stack":false,"steppedLine":false,"targets":[{}],"tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true}],"title":"Simple graph"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"web_server_01":"#F2C96D","web_server_02":"#EAB839","web_server_03":"#EF843C","web_server_04":"#E24D42"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":2,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":3,"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(scaleToSeconds(apps.fakesite.*.counters.requests.count,1),2)"}],"timezone":"browser","title":"Stacked lines","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true},{"content":"#### Stacked graph\n- This graph shows stacked series, with area fill and 2px line width\n- We have also added legend values. These can be enabled in the Grid \u0026 Axes tab in edit mode. \n- Legend values can be Min, Max, Total, Current and Average","editable":true,"error":false,"id":4,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Stacked Graph"},{"collapsable":true,"collapse":true,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":5,"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":true,"targets":[{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.upper_90,4)"},{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.upper_90,4)"}],"timezone":"browser","title":"Staircase line","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true},{"content":"#### Staircase \u0026 Y-Axis format\n- In display styles tab you can switch to staircase line \n- In Axes \u0026 Grid tab you can change to different Y units \u0026 formats.\n","editable":true,"error":false,"id":6,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Staircase line"},{"collapsable":true,"collapse":true,"editable":true,"height":"300px","notice":false,"panels":[{"aliasColors":{"cpu":"#E24D42"},"aliasYAxis":{"cpu":2},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":7,"interactive":true,"legend":{"avg":false,"current":true,"max":false,"min":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"alias(scale(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),1000000),'memory')"},{"target":"alias(scaleToSeconds(apps.fakesite.web_server_02.counters.requests.count,1),'cpu')"}],"timezone":"browser","title":"Memory / CPU","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["bytes","none"],"zerofill":true},{"content":"#### Second Y-Axis\n- Click on the series legend color line to open the color selector\n- In the series color selector popup you can also move the series to the Right-Y axis\n- Multiple Y-Axis are great for showing to related series that have different magnitudes ","editable":true,"error":false,"id":8,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Right Y-Axis"},{"collapsable":true,"collapse":true,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":9,"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"null","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"aliasByNode(apps.fakesite.web_server_01.counters.request_status.code_404.count,4)"}],"timezone":"browser","title":"Null point mode","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["none","short"],"zerofill":true},{"content":"#### Null point mode\n- This option under Display styles tab controls how null values are handled\n- The graph to left shows how the default \"null\" looks. \n- __null__ null values are left null and this leaves empty spaces in the graph\n- __null as zero__ null values are drawn as zero values\n- __connected__ null values are ignored and the line jumps directly to the next value.","editable":true,"error":false,"id":10,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"test"},{"collapsable":true,"collapse":true,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"requests":"#6ED0E0","upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":700,"leftMin":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":400,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":600,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":11,"interactive":true,"legend":{"avg":false,"current":false,"max":false,"min":false,"show":true,"total":true,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"aliasByNode(apps.fakesite.web_server_01.counters.requests.count,4)"}],"timezone":"browser","title":"Thresholds","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["none","short"],"zerofill":true},{"content":"#### Thresholds\n- You can define thresholds in the Grid \u0026 Axes tab in edit mode \n- You can define one or two thresholds, color is also changeable. \n- You can have lower bound thresholds as well. ","editable":true,"error":false,"id":12,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Thresholds"},{"collapsable":true,"collapse":true,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":true,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":13,"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":false,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":true,"targets":[{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.*,4)"}],"timezone":"browser","title":"Bars","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true},{"content":"#### Bars\n- In display styles tab you can switch from line to bars\n- The width of the bar is relative to the pixel width between two values\n","editable":true,"error":false,"id":14,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Bars"},{"collapsable":true,"collapse":true,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":15,"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":false,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":2,"points":false,"renderer":"png","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(apps.backend.*.counters.requests.count,2)"}],"timezone":"browser","title":"Graphite PNG","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true},{"content":"#### Graphite PNG support\n- You can switch from client side rendering to graphite's server side PNG rendering\n- You cannot click and drag to zoom in in this render mode.\n","editable":true,"error":false,"id":16,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Graphite PNG"}],"style":"dark","tags":["showcase","annotations"],"templating":{"list":[]},"time":{"from":"2014-08-16T13:36:29.354Z","to":"2014-08-16T13:51:59.670Z"},"timezone":"browser","title":"Graph_styles_collapsed","version":3} \ No newline at end of file diff --git a/data/dashboards/home.json b/data/dashboards/home.json deleted file mode 100644 index 62330969e74..00000000000 --- a/data/dashboards/home.json +++ /dev/null @@ -1 +0,0 @@ -{"editable":true,"nav":[{"collapse":false,"enable":true,"notice":false,"now":true,"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"status":"Stable","time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"],"type":"timepicker"}],"pulldowns":[{"collapse":false,"enable":false,"notice":false,"type":"filtering"},{"enable":false,"type":"annotations"}],"rows":[{"collapsable":true,"collapse":false,"editable":true,"height":"190px","notice":false,"panels":[{"content":"\u003ch3 class=\"text-center\"\u003eWelcome to grafana demo, playground and interactive tutorial site.\u003c/h2\u003e\n\n\u003cdiv class=\"row-fluid\"\u003e\n\u0009\u003cdiv class=\"span4\"\u003e\n\u0009\u0009\u003ch4\u003eFeature showcases\u003c/h2\u003e\n\u0009\u0009\u003cul\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/graph-styles.json\"\u003eGraphs styles\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/templated-graphs.json\"\u003eTemplated graphs\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/templated-graphs-nested.json\"\u003eTemplated graphs nested\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/annotations.json\"\u003eAnnotations\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009\u003ca href=\"#/dashboard/file/white-theme.json\"\u003eWhite theme\u003c/a\u003e\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u003c/ul\u003e\n\u0009\u003c/div\u003e\n\u0009\u003cdiv class=\"span4\"\u003e\n\u0009\u0009\u003ch4\u003eGraphite tutorials\u003c/h2\u003e\n\u0009\u0009\u003cul\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Graphite introduction (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Basic functions (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Advanced functions (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009Tips and tricks (TODO)\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u003c/ul\u003e\n\u0009\u003c/div\u003e\n\u0009\u003cdiv class=\"span4\"\u003e\n\u0009\u0009\u003ch4\u003eInfluxDB examples\u003c/h2\u003e\n\u0009\u0009\u003cul\u003e\n\u0009\u0009\u0009\u003cli\u003e\n\u0009\u0009\u0009\u0009TODO\n\u0009\u0009\u0009\u003c/li\u003e\n\u0009\u0009\u003c/ul\u003e\n\u0009\u003c/div\u003e\n\u003c/div\u003e\n\n\u003cscript type=\"text/javascript\"\u003e(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){\n(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),\nm=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)\n})(window,document,'script','//www.google-analytics.com/analytics.js','ga');\n\nga('create', 'UA-47280256-1', 'grafana.org');\nga('send', 'pageview');\u003c/script\u003e","editable":true,"error":false,"id":1,"loadingEditor":false,"mode":"html","span":12,"style":{},"title":"Grafana demo site","type":"text"}],"title":"test"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"web_server_01":"#B7DBAB","web_server_02":"#7EB26D","web_server_03":"#508642","web_server_04":"#3F6833"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":3,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":2,"interactive":true,"legend":{"avg":false,"current":false,"max":false,"min":false,"show":true,"total":false,"values":false},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(scaleToSeconds(apps.fakesite.*.counters.requests.count,1),2)"}],"timezone":"browser","title":"SERVER REQUESTS","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true},{"aliasColors":{"logins":"#7EB26D","logins (-1 day)":"#447EBC"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":3,"interactive":true,"legend":{"avg":false,"current":true,"max":true,"min":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"alias(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),'logins')"},{"target":"alias(timeShift(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),'1h'),'logins (-1 hour)')"}],"timezone":"browser","title":"logins","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true}],"title":"test"},{"collapsable":true,"collapse":false,"editable":true,"height":"300px","notice":false,"panels":[{"aliasColors":{"cpu":"#E24D42"},"aliasYAxis":{"cpu":2},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":4,"interactive":true,"legend":{"avg":false,"current":true,"max":false,"min":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"alias(scale(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),1000000),'memory')"},{"target":"alias(scaleToSeconds(apps.fakesite.web_server_01.counters.request_status.code_302.count,1),'cpu')"}],"timezone":"browser","title":"Memory / CPU","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["bytes","none"],"zerofill":true},{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":true,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"id":5,"interactive":true,"legend":{"alignAsTable":true,"avg":true,"current":false,"max":false,"min":false,"rightSide":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":false,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.*,4)"}],"timezone":"browser","title":"client side full page load","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true}],"title":""},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[],"title":"test"}],"style":"dark","tags":["showcase","startpage","home","default"],"templating":{"list":[]},"time":{"from":"now-15m","to":"now"},"timezone":"browser","title":"home","version":3} \ No newline at end of file diff --git a/data/dashboards/templated-graphs-nested.json b/data/dashboards/templated-graphs-nested.json deleted file mode 100644 index abd26cc8f17..00000000000 --- a/data/dashboards/templated-graphs-nested.json +++ /dev/null @@ -1,288 +0,0 @@ -{ - "title": "Templated Graphs Nested", - "services": { - "filter": { - "list": [ - { - "type": "filter", - "name": "app", - "query": "apps.*", - "includeAll": true, - "options": [ - { - "text": "All", - "value": "{backend,fakesite}" - }, - { - "text": "backend", - "value": "backend" - }, - { - "text": "fakesite", - "value": "fakesite" - } - ], - "current": { - "text": "backend", - "value": "backend" - } - }, - { - "type": "filter", - "name": "server", - "query": "apps.[[app]].*", - "includeAll": true, - "options": [ - { - "text": "All", - "value": "{backend_01,backend_02,backend_03,backend_04}" - }, - { - "text": "backend_01", - "value": "backend_01" - }, - { - "text": "backend_02", - "value": "backend_02" - }, - { - "text": "backend_03", - "value": "backend_03" - }, - { - "text": "backend_04", - "value": "backend_04" - } - ], - "current": { - "text": "All", - "value": "{backend_01,backend_02,backend_03,backend_04}" - } - } - ], - "time": { - "from": "now-6h", - "to": "now" - } - } - }, - "rows": [ - { - "title": "Row1", - "height": "350px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 12, - "editable": true, - "type": "graphite", - "loadingEditor": false, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "annotate": { - "enable": false - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "groupByNode(apps.[[app]].[[server]].counters.requests.count,2,'sum')" - } - ], - "aliasColors": { - "highres.test": "#1F78C1", - "scale(highres.test,3)": "#6ED0E0", - "mobile": "#6ED0E0", - "tablet": "#EAB839" - }, - "aliasYAxis": {}, - "title": "Traffic" - } - ], - "notice": false - }, - { - "title": "Row1", - "height": "350px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 12, - "editable": true, - "type": "graphite", - "loadingEditor": false, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "annotate": { - "enable": false - }, - "resolution": 100, - "lines": true, - "fill": 0, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(movingAverage(scaleToSeconds(apps.[[app]].[[server]].counters.requests.count,60),10),1,2)" - } - ], - "aliasColors": { - "highres.test": "#1F78C1", - "scale(highres.test,3)": "#6ED0E0", - "mobile": "#6ED0E0", - "tablet": "#EAB839" - }, - "aliasYAxis": {}, - "title": "Sessions / min" - } - ], - "notice": false - } - ], - "editable": true, - "failover": false, - "panel_hints": true, - "style": "dark", - "pulldowns": [ - { - "type": "filtering", - "collapse": false, - "notice": false, - "enable": true - }, - { - "type": "annotations", - "enable": false - } - ], - "nav": [ - { - "type": "timepicker", - "collapse": false, - "notice": false, - "enable": true, - "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "now": true - } - ], - "loader": { - "save_gist": false, - "save_elasticsearch": true, - "save_local": true, - "save_default": true, - "save_temp": true, - "save_temp_ttl_enable": true, - "save_temp_ttl": "30d", - "load_gist": false, - "load_elasticsearch": true, - "load_elasticsearch_size": 20, - "load_local": false, - "hide": false - }, - "refresh": false, - "tags": [ - "showcase", - "templated" - ], - "timezone": "browser" -} \ No newline at end of file diff --git a/data/dashboards/templated-graphs.json b/data/dashboards/templated-graphs.json deleted file mode 100644 index f979e4d1223..00000000000 --- a/data/dashboards/templated-graphs.json +++ /dev/null @@ -1,272 +0,0 @@ -{ - "title": "Templated Graphs", - "services": { - "filter": { - "list": [ - { - "type": "filter", - "name": "root", - "query": "statsd.fakesite.counters.session_start.*", - "includeAll": true, - "options": [ - { - "text": "All", - "value": "{desktop,mobile,tablet}" - }, - { - "text": "desktop", - "value": "desktop" - }, - { - "text": "mobile", - "value": "mobile" - }, - { - "text": "tablet", - "value": "tablet" - } - ], - "current": { - "text": "All", - "value": "{desktop,mobile,tablet}" - } - } - ], - "time": { - "from": "now-6h", - "to": "now" - } - } - }, - "rows": [ - { - "title": "Row1", - "height": "350px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 12, - "editable": true, - "type": "graphite", - "loadingEditor": false, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "annotate": { - "enable": false - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 1, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(summarize(statsd.fakesite.counters.session_start.[[root]].count,'1min','sum'),4)" - } - ], - "aliasColors": { - "highres.test": "#1F78C1", - "scale(highres.test,3)": "#6ED0E0", - "mobile": "#6ED0E0", - "tablet": "#EAB839" - }, - "aliasYAxis": {}, - "title": "Device sessions" - } - ], - "notice": false - }, - { - "title": "Row1", - "height": "350px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "loadingEditor": false, - "datasource": null, - "renderer": "flot", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "annotate": { - "enable": false - }, - "resolution": 100, - "lines": false, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": true, - "stack": true, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(summarize(statsd.fakesite.counters.session_start.[[root]].count,'1h','sum'),4)" - } - ], - "aliasColors": { - "highres.test": "#1F78C1", - "scale(highres.test,3)": "#6ED0E0", - "tablet": "#EAB839", - "desktop": "#7EB26D", - "mobile": "#6ED0E0" - }, - "aliasYAxis": {}, - "title": "Device sessions (1h)" - }, - { - "error": false, - "span": 6, - "editable": true, - "type": "text", - "loadingEditor": false, - "mode": "markdown", - "content": "#### Templated metric queries / graphs \n- In dashboard settings, in the Controls tab / Feature toggles. You can enable 'Filtering' \n- This feature when enabled will show you a bar bellow the menu.\n- In this bar you can add filters, or what should be named templated metric segments. \n- A filter is a query for a specific metric segment\n- Open any graph in this dashboard and edit mode and you can see that the [[device]] filter is used instead of a wildcard.\n- Try clicking the All link in the filter menu at the top, change device and see that all graphs change to only show values for that device. ", - "style": {}, - "title": "Description" - } - ], - "notice": false - } - ], - "editable": true, - "failover": false, - "panel_hints": true, - "style": "dark", - "pulldowns": [ - { - "type": "filtering", - "collapse": false, - "notice": false, - "enable": true - }, - { - "type": "annotations", - "enable": false - } - ], - "nav": [ - { - "type": "timepicker", - "collapse": false, - "notice": false, - "enable": true, - "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "now": true - } - ], - "loader": { - "save_gist": false, - "save_elasticsearch": true, - "save_local": true, - "save_default": true, - "save_temp": true, - "save_temp_ttl_enable": true, - "save_temp_ttl": "30d", - "load_gist": false, - "load_elasticsearch": true, - "load_elasticsearch_size": 20, - "load_local": false, - "hide": false - }, - "refresh": false, - "tags": [ - "showcase", - "templated" - ], - "timezone": "browser" -} \ No newline at end of file diff --git a/data/dashboards/testing-save.json b/data/dashboards/testing-save.json deleted file mode 100644 index fdd008c4cf7..00000000000 --- a/data/dashboards/testing-save.json +++ /dev/null @@ -1 +0,0 @@ -{"editable":true,"nav":[{"collapse":false,"enable":true,"notice":false,"now":true,"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"status":"Stable","time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"],"type":"timepicker"}],"pulldowns":[{"collapse":false,"enable":false,"notice":false,"type":"filtering"},{"enable":false,"type":"annotations"}],"refresh":false,"rows":[{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"web_server_01":"#EF843C","web_server_02":"#EAB839","web_server_03":"#508642","web_server_04":"#E24D42"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":false,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":1,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"aliasByNode(scaleToSeconds(apps.backend.*.counters.requests.count,1),2)"}],"timezone":"browser","title":"Simple graph","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true},{"content":"#### Simple graph\n- Click on the title and select edit to open edit mode\n- The display styles tab allows you change line width, fill, stacking, and more\n- You can change a series color by clicking the colored line in the legend ","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Simple graph"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"web_server_01":"#F2C96D","web_server_02":"#EAB839","web_server_03":"#EF843C","web_server_04":"#E24D42"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":2,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(scaleToSeconds(apps.fakesite.*.counters.requests.count,1),2)"}],"timezone":"browser","title":"Stacked lines","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["short","short"],"zerofill":true},{"content":"#### Stacked graph\n- This graph shows stacked series, with area fill and 2px line width\n- We have also added legend values. These can be enabled in the Grid \u0026 Axes tab in edit mode. \n- Legend values can be Min, Max, Total, Current and Average","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Stacked Graph"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":true,"targets":[{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.upper_90,4)"},{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.upper_90,4)"}],"timezone":"browser","title":"Staircase line","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true},{"content":"#### Staircase \u0026 Y-Axis format\n- In display styles tab you can switch to staircase line \n- In Axes \u0026 Grid tab you can change to different Y units \u0026 formats.\n","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Staircase line"},{"collapsable":true,"collapse":false,"editable":true,"height":"300px","notice":false,"panels":[{"aliasColors":{"cpu":"#E24D42"},"aliasYAxis":{"cpu":2},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":false,"current":true,"max":false,"min":true,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"alias(scale(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),1000000),'memory')"},{"target":"alias(scaleToSeconds(apps.fakesite.web_server_02.counters.requests.count,1),'cpu')"}],"timezone":"browser","title":"Memory / CPU","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["bytes","none"],"zerofill":true},{"content":"#### Second Y-Axis\n- Click on the series legend color line to open the color selector\n- In the series color selector popup you can also move the series to the Right-Y axis\n- Multiple Y-Axis are great for showing to related series that have different magnitudes ","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Right Y-Axis"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"null","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"aliasByNode(apps.fakesite.web_server_01.counters.request_status.code_404.count,4)"}],"timezone":"browser","title":"Null point mode","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["none","short"],"zerofill":true},{"content":"#### Null point mode\n- This option under Display styles tab controls how null values are handled\n- The graph to left shows how the default \"null\" looks. \n- __null__ null values are left null and this leaves empty spaces in the graph\n- __null as zero__ null values are drawn as zero values\n- __connected__ null values are ignored and the line jumps directly to the next value.","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"test"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"requests":"#6ED0E0","upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":0,"grid":{"leftMax":700,"leftMin":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":400,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":600,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":false,"current":false,"max":false,"min":false,"show":true,"total":true,"values":true},"legend_counts":true,"lines":true,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":false,"steppedLine":false,"targets":[{"target":"aliasByNode(apps.fakesite.web_server_01.counters.requests.count,4)"}],"timezone":"browser","title":"Thresholds","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["none","short"],"zerofill":true},{"content":"#### Thresholds\n- You can define thresholds in the Grid \u0026 Axes tab in edit mode \n- You can define one or two thresholds, color is also changeable. \n- You can have lower bound thresholds as well. ","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Thresholds"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":true,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":false,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":5,"points":false,"renderer":"flot","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":true,"targets":[{"target":"aliasByNode(statsd.fakesite.timers.ads_timer.*,4)"}],"timezone":"browser","title":"Bars","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true},{"content":"#### Bars\n- In display styles tab you can switch from line to bars\n- The width of the bar is relative to the pixel width between two values\n","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Bars"},{"collapsable":true,"collapse":false,"editable":true,"height":"250px","notice":false,"panels":[{"aliasColors":{"upper_25":"#BA43A9","upper_50":"#7EB26D","upper_75":"#EAB839"},"aliasYAxis":{},"annotate":{"enable":false},"bars":false,"datasource":null,"editable":true,"fill":1,"grid":{"leftMax":null,"leftMin":null,"max":null,"min":0,"rightMax":null,"rightMin":null,"threshold1":null,"threshold1Color":"rgba(216, 200, 27, 0.27)","threshold2":null,"threshold2Color":"rgba(234, 112, 112, 0.22)"},"interactive":true,"legend":{"avg":true,"current":false,"max":false,"min":false,"show":true,"total":false,"values":true},"legend_counts":true,"lines":false,"linewidth":2,"nullPointMode":"connected","options":false,"percentage":false,"pointradius":2,"points":false,"renderer":"png","resolution":100,"scale":1,"span":6,"spyable":true,"stack":true,"steppedLine":false,"targets":[{"target":"aliasByNode(apps.backend.*.counters.requests.count,2)"}],"timezone":"browser","title":"Graphite PNG","tooltip":{"query_as_alias":true,"value_type":"cumulative"},"type":"graph","x-axis":true,"y-axis":true,"y_formats":["ms","short"],"zerofill":true},{"content":"#### Graphite PNG support\n- You can switch from client side rendering to graphite's server side PNG rendering\n- You cannot click and drag to zoom in in this render mode.\n","editable":true,"error":false,"loadingEditor":false,"mode":"markdown","span":6,"style":{},"title":"Description","type":"text"}],"title":"Graphite PNG"}],"style":"dark","tags":["showcase","annotations"],"templating":{"list":[]},"time":{"from":"now-15m","to":"now"},"timezone":"browser","title":"Testing save","version":2} \ No newline at end of file diff --git a/data/dashboards/white-theme.json b/data/dashboards/white-theme.json deleted file mode 100644 index 4ffa2fc1a60..00000000000 --- a/data/dashboards/white-theme.json +++ /dev/null @@ -1,519 +0,0 @@ -{ - "title": "White theme", - "services": { - "filter": { - "list": [], - "time": { - "from": "now-1h", - "to": "now" - } - } - }, - "rows": [ - { - "title": "test", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 3, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(scaleToSeconds(apps.fakesite.*.counters.requests.count,1),2)" - } - ], - "aliasColors": { - "web_server_04": "#3F6833", - "web_server_03": "#508642", - "web_server_02": "#7EB26D", - "web_server_01": "#B7DBAB" - }, - "aliasYAxis": {}, - "title": "server requests", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(movingAverage(scaleToSeconds(apps.fakesite.*.counters.request_status.code_302.count,1),4),2)" - } - ], - "aliasColors": { - "logins": "#7EB26D", - "logins (-1 day)": "#447EBC" - }, - "aliasYAxis": {}, - "title": "logins", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - } - ], - "notice": false - }, - { - "title": "", - "height": "300px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 4, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "bytes", - "none" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 0, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": true, - "max": false, - "current": true, - "total": false, - "avg": false - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "alias(scale(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count,1),1000000),'memory')" - }, - { - "target": "alias(scaleToSeconds(apps.fakesite.web_server_02.counters.requests.count,1),'cpu')" - } - ], - "aliasColors": { - "cpu": "#E24D42" - }, - "aliasYAxis": { - "cpu": 2 - }, - "title": "Memory / CPU", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "span": 8, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "ms", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": false, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": true, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(statsd.fakesite.timers.ads_timer.*,4)" - } - ], - "aliasColors": { - "upper_75": "#EAB839", - "upper_50": "#7EB26D", - "upper_25": "#BA43A9" - }, - "aliasYAxis": {}, - "title": "client side full page load", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - } - ], - "notice": false - }, - { - "title": "test", - "height": "250px", - "editable": true, - "collapse": false, - "collapsable": true, - "panels": [ - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(movingAverage(scaleToSeconds(apps.fakesite.*.counters.request_status.code_302.count,1),4),2)" - } - ], - "aliasColors": { - "logins": "#7EB26D", - "logins (-1 day)": "#447EBC", - "web_server_03": "#1F78C1", - "web_server_02": "#6ED0E0", - "web_server_01": "#64B0C8" - }, - "aliasYAxis": {}, - "title": "logins", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - }, - { - "span": 6, - "editable": true, - "type": "graphite", - "x-axis": true, - "y-axis": true, - "scale": 1, - "y_formats": [ - "short", - "short" - ], - "grid": { - "max": null, - "min": 0, - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "resolution": 100, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": true, - "spyable": true, - "options": false, - "legend": { - "show": true, - "values": true, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": true - }, - "interactive": true, - "legend_counts": true, - "timezone": "browser", - "percentage": false, - "zerofill": true, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "query_as_alias": true - }, - "targets": [ - { - "target": "aliasByNode(movingAverage(scaleToSeconds(apps.fakesite.*.counters.request_status.code_302.count,1),4),2)" - } - ], - "aliasColors": { - "logins": "#7EB26D", - "logins (-1 day)": "#447EBC", - "web_server_03": "#E24D42", - "web_server_02": "#EF843C", - "web_server_01": "#EAB839" - }, - "aliasYAxis": {}, - "title": "logins", - "datasource": null, - "renderer": "flot", - "annotate": { - "enable": false - } - } - ], - "notice": false - } - ], - "editable": true, - "failover": false, - "panel_hints": true, - "style": "light", - "pulldowns": [ - { - "type": "filtering", - "collapse": false, - "notice": false, - "enable": false - }, - { - "type": "annotations", - "enable": false - } - ], - "nav": [ - { - "type": "timepicker", - "collapse": false, - "notice": false, - "enable": true, - "status": "Stable", - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ], - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "now": true - } - ], - "loader": { - "save_gist": false, - "save_elasticsearch": true, - "save_local": true, - "save_default": true, - "save_temp": true, - "save_temp_ttl_enable": true, - "save_temp_ttl": "30d", - "load_gist": false, - "load_elasticsearch": true, - "load_elasticsearch_size": 20, - "load_local": false, - "hide": false - }, - "refresh": false, - "tags": [], - "timezone": "browser" -} \ No newline at end of file diff --git a/data/png/77dedee4f4d479cea6f9d608353ed098.png b/data/png/77dedee4f4d479cea6f9d608353ed098.png deleted file mode 100644 index 43598440c81..00000000000 Binary files a/data/png/77dedee4f4d479cea6f9d608353ed098.png and /dev/null differ diff --git a/data/png/9e956287bee2bc4eabd91f9bb06ab458.png b/data/png/9e956287bee2bc4eabd91f9bb06ab458.png deleted file mode 100644 index e43d8dc1906..00000000000 Binary files a/data/png/9e956287bee2bc4eabd91f9bb06ab458.png and /dev/null differ diff --git a/docker/blocks/influxdb/Dockerfile b/docker/blocks/influxdb/Dockerfile new file mode 100644 index 00000000000..69d10992464 --- /dev/null +++ b/docker/blocks/influxdb/Dockerfile @@ -0,0 +1,16 @@ +# influxdb + +FROM ubuntu + +RUN mkdir -p /opt/influxdb/shared/data + +ADD http://s3.amazonaws.com/influxdb/influxdb_0.8.8_amd64.deb /influx88.deb +RUN dpkg -i /influx88.deb +RUN rm -rf /opt/influxdb/shared/data + +ADD config.toml /opt/influxdb/shared/config.toml + +EXPOSE 8083 8086 2004 + +ENTRYPOINT ["/usr/bin/influxdb"] +CMD ["-config=/opt/influxdb/shared/config.toml"] diff --git a/docker/blocks/influxdb/config.toml b/docker/blocks/influxdb/config.toml new file mode 100644 index 00000000000..23834b22e3d --- /dev/null +++ b/docker/blocks/influxdb/config.toml @@ -0,0 +1,75 @@ +bind-address = "0.0.0.0" + +[logging] +level = "debug" +file = "/opt/influxdb/shared/data/influxdb.log" # stdout to log to standard out + +[admin] +port = 8083 # binding is disabled if the port isn't set +assets = "/opt/influxdb/current/admin" + +[api] +port = 8086 # binding is disabled if the port isn't set + +read-timeout = "5s" + +[input_plugins] + + [input_plugins.graphite] + enabled = true + port = 2004 + database = "graphite" # store graphite data in this database + + +[raft] +port = 8090 +dir = "/opt/influxdb/shared/data/raft" + +[storage] +dir = "/opt/influxdb/shared/data/db" +# How many requests to potentially buffer in memory. If the buffer gets filled then writes +# will still be logged and once the local storage has caught up (or compacted) the writes +# will be replayed from the WAL +write-buffer-size = 10000 +default-engine = "rocksdb" +max-open-shards = 0 +point-batch-size = 100 +write-batch-size = 5000000 +retention-sweep-period = "10m" + +[storage.engines.rocksdb] +max-open-files = 1000 +lru-cache-size = "200m" + +[storage.engines.leveldb] +max-open-files = 1000 +lru-cache-size = "200m" + +[cluster] +protobuf_port = 8099 +protobuf_timeout = "2s" # the write timeout on the protobuf conn any duration parseable by time.ParseDuration +protobuf_heartbeat = "200ms" # the heartbeat interval between the servers. must be parseable by time.ParseDuration +protobuf_min_backoff = "1s" # the minimum backoff after a failed heartbeat attempt +protobuf_max_backoff = "10s" # the maxmimum backoff after a failed heartbeat attempt +write-buffer-size = 10000 +ax-response-buffer-size = 100000 +oncurrent-shard-query-limit = 10 + +[sharding] + replication-factor = 1 + + [sharding.short-term] + duration = "7d" + split = 1 + + [sharding.long-term] + duration = "30d" + split = 1 + # split-random = "/^Hf.*/" + +[wal] +dir = "/opt/influxdb/shared/data/wal" +flush-after = 1000 # the number of writes after which wal will be flushed, 0 for flushing on every write +bookmark-after = 1000 # the number of writes after which a bookmark will be created +index-after = 1000 +requests-per-logfile = 10000 diff --git a/docker/blocks/influxdb/fig b/docker/blocks/influxdb/fig new file mode 100644 index 00000000000..3f247f756e2 --- /dev/null +++ b/docker/blocks/influxdb/fig @@ -0,0 +1,6 @@ +influxdb: + build: blocks/influxdb + ports: + - "2004:2004" + - "8083:8083" + - "8086:8086" diff --git a/docker/fig.yml b/docker/fig.yml deleted file mode 100644 index fa47a0ce914..00000000000 --- a/docker/fig.yml +++ /dev/null @@ -1,18 +0,0 @@ -mysqltests: - image: mysql:latest - environment: - MYSQL_ROOT_PASSWORD: rootpass - MYSQL_DATABASE: grafana_tests - MYSQL_USER: grafana - MYSQL_PASSWORD: password - ports: - - "3306:3306" - -postgrestest: - image: postgres:latest - environment: - POSTGRES_USER: grafanatest - POSTGRES_PASSWORD: grafanatest - ports: - - "5432:5432" - diff --git a/Dockerfile b/docker/production/Dockerfile similarity index 81% rename from Dockerfile rename to docker/production/Dockerfile index c1a2d3cfc62..5017c2c1a36 100644 --- a/Dockerfile +++ b/docker/production/Dockerfile @@ -1,4 +1,4 @@ -FROM phusion/baseimage +FROM debian:jessie RUN apt-get -y update RUN apt-get -y install libfontconfig @@ -10,6 +10,7 @@ ADD tmp/ /opt/grafana/ EXPOSE 3000 VOLUME ["/opt/grafana/data"] +VOLUME ["/opt/grafana/conf"] WORKDIR /opt/grafana/ ENTRYPOINT ["./grafana", "web"] diff --git a/docker/production/README.md b/docker/production/README.md new file mode 100644 index 00000000000..f6da10e62f6 --- /dev/null +++ b/docker/production/README.md @@ -0,0 +1,31 @@ + +# Grafana docker image + +This container currently only contains the in development alpha of Grafana 2.0 (ie non production use). The +`#develop` tag is constantly updated as we make progress torwards a beta release. + + +## Running your Grafana image +-------------------------- + +Start your image binding the external port `3000`. + + docker run -i -p 3000:3000 grafana/grafana + +Try it out, default admin user is admin/admin. + + +## Configuring your Grafana container + +All options defined in conf/grafana.ini can be overriden using environment variables, for example: + + +``` +docker run -i -p 3000:3000 \ + -e "GF_SERVER_ROOT_URL=http://grafana.server.name" \ + -e "GF_SECURITY_ADMIN_PASSWORD=secret \ + grafana/grafana:develop +``` + + + diff --git a/docker/production/build.sh b/docker/production/build.sh new file mode 100755 index 00000000000..d97b29de934 --- /dev/null +++ b/docker/production/build.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +cp Dockerfile ../../ +cd ../../ + +go run build.go build + +grunt release + +docker build --tag "grafana/grafana:develop" . + +rm Dockerfile +cd docker/production + + diff --git a/docker/production/test_container.sh b/docker/production/test_container.sh new file mode 100755 index 00000000000..aa8fdba4cbf --- /dev/null +++ b/docker/production/test_container.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +docker run -i -p 3001:3000 \ + -e "GF_SERVER_ROOT_URL=http://grafana.server.name" \ + grafana/grafana:develop diff --git a/main.go b/main.go index 997fbd38f0b..ff18e7b2bda 100644 --- a/main.go +++ b/main.go @@ -31,8 +31,22 @@ func main() { app.Name = "Grafana Backend" app.Usage = "grafana web" app.Version = version - app.Commands = []cli.Command{cmd.CmdWeb, cmd.CmdImportJson} - app.Flags = append(app.Flags, []cli.Flag{}...) + app.Commands = []cli.Command{ + cmd.ListOrgs, + cmd.CreateOrg, + cmd.DeleteOrg, + cmd.ImportDashboard, + cmd.ListDataSources, + cmd.CreateDataSource, + cmd.DescribeDataSource, + cmd.DeleteDataSource, + cmd.Web} + app.Flags = append(app.Flags, []cli.Flag{ + cli.StringFlag{ + Name: "config", + Usage: "path to grafana.ini config file", + }, + }...) app.Run(os.Args) log.Close() diff --git a/pkg/api/account.go b/pkg/api/account.go deleted file mode 100644 index 6c97b90c350..00000000000 --- a/pkg/api/account.go +++ /dev/null @@ -1,50 +0,0 @@ -package api - -import ( - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" - m "github.com/grafana/grafana/pkg/models" -) - -func GetAccount(c *middleware.Context) { - query := m.GetAccountByIdQuery{Id: c.AccountId} - - if err := bus.Dispatch(&query); err != nil { - if err == m.ErrAccountNotFound { - c.JsonApiErr(404, "Account not found", err) - return - } - - c.JsonApiErr(500, "Failed to get account", err) - return - } - - account := m.AccountDTO{ - Id: query.Result.Id, - Name: query.Result.Name, - } - - c.JSON(200, &account) -} - -func CreateAccount(c *middleware.Context, cmd m.CreateAccountCommand) { - cmd.UserId = c.UserId - - if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "Failed to create account", err) - return - } - - c.JsonOK("Account created") -} - -func UpdateAccount(c *middleware.Context, cmd m.UpdateAccountCommand) { - cmd.AccountId = c.AccountId - - if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "Failed to update account", err) - return - } - - c.JsonOK("Account updated") -} diff --git a/pkg/api/admin_settings.go b/pkg/api/admin_settings.go new file mode 100644 index 00000000000..21615219acd --- /dev/null +++ b/pkg/api/admin_settings.go @@ -0,0 +1,29 @@ +package api + +import ( + "strings" + + "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/setting" +) + +func AdminGetSettings(c *middleware.Context) { + settings := make(map[string]interface{}) + + for _, section := range setting.Cfg.Sections() { + jsonSec := make(map[string]interface{}) + settings[section.Name()] = jsonSec + + for _, key := range section.Keys() { + keyName := key.Name() + value := key.Value() + if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") { + value = "************" + } + + jsonSec[keyName] = value + } + } + + c.JSON(200, settings) +} diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index b202c5ce2a8..1b0c11543f5 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -1,20 +1,153 @@ package api import ( + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/util" ) func AdminSearchUsers(c *middleware.Context) { - // query := c.QueryStrings("q") - // page := c.QueryStrings("p") - - query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 20} + query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to fetch collaboratos", err) + c.JsonApiErr(500, "Failed to fetch users", err) return } c.JSON(200, query.Result) } + +func AdminGetUser(c *middleware.Context) { + userId := c.ParamsInt64(":id") + + query := m.GetUserByIdQuery{Id: userId} + + if err := bus.Dispatch(&query); err != nil { + c.JsonApiErr(500, "Failed to fetch user", err) + return + } + + result := m.UserDTO{ + Name: query.Result.Name, + Email: query.Result.Email, + Login: query.Result.Login, + IsGrafanaAdmin: query.Result.IsAdmin, + } + + c.JSON(200, result) +} + +func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) { + cmd := m.CreateUserCommand{ + Login: form.Login, + Email: form.Email, + Password: form.Password, + Name: form.Name, + } + + if len(cmd.Login) == 0 { + cmd.Login = cmd.Email + if len(cmd.Login) == 0 { + c.JsonApiErr(400, "Validation error, need specify either username or email", nil) + return + } + } + + if len(cmd.Password) < 4 { + c.JsonApiErr(400, "Password is missing or too short", nil) + return + } + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "failed to create user", err) + return + } + + c.JsonOK("User created") +} + +func AdminUpdateUser(c *middleware.Context, form dtos.AdminUpdateUserForm) { + userId := c.ParamsInt64(":id") + + cmd := m.UpdateUserCommand{ + UserId: userId, + Login: form.Login, + Email: form.Email, + Name: form.Name, + } + + if len(cmd.Login) == 0 { + cmd.Login = cmd.Email + if len(cmd.Login) == 0 { + c.JsonApiErr(400, "Validation error, need specify either username or email", nil) + return + } + } + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "failed to update user", err) + return + } + + c.JsonOK("User updated") +} + +func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPasswordForm) { + userId := c.ParamsInt64(":id") + + if len(form.Password) < 4 { + c.JsonApiErr(400, "New password too short", nil) + return + } + + userQuery := m.GetUserByIdQuery{Id: userId} + + if err := bus.Dispatch(&userQuery); err != nil { + c.JsonApiErr(500, "Could not read user from database", err) + return + } + + passwordHashed := util.EncodePassword(form.Password, userQuery.Result.Salt) + + cmd := m.ChangeUserPasswordCommand{ + UserId: userId, + NewPassword: passwordHashed, + } + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "Failed to update user password", err) + return + } + + c.JsonOK("User password updated") +} + +func AdminUpdateUserPermissions(c *middleware.Context, form dtos.AdminUpdateUserPermissionsForm) { + userId := c.ParamsInt64(":id") + + cmd := m.UpdateUserPermissionsCommand{ + UserId: userId, + IsGrafanaAdmin: form.IsGrafanaAdmin, + } + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "Failed to update user permissions", err) + return + } + + c.JsonOK("User permissions updated") +} + +func AdminDeleteUser(c *middleware.Context) { + userId := c.ParamsInt64(":id") + + cmd := m.DeleteUserCommand{UserId: userId} + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "Failed to delete user", err) + return + } + + c.JsonOK("User deleted") +} diff --git a/pkg/api/api.go b/pkg/api/api.go index 6214b60244d..c516b7c2225 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -25,12 +25,15 @@ func Register(r *macaron.Macaron) { // authed views r.Get("/profile/", reqSignedIn, Index) - r.Get("/account/", reqSignedIn, Index) - r.Get("/account/datasources/", reqSignedIn, Index) - r.Get("/account/users/", reqSignedIn, Index) - r.Get("/account/apikeys/", reqSignedIn, Index) - r.Get("/account/import/", reqSignedIn, Index) + r.Get("/org/", reqSignedIn, Index) + r.Get("/datasources/", reqSignedIn, Index) + r.Get("/org/users/", reqSignedIn, Index) + r.Get("/org/apikeys/", reqSignedIn, Index) + r.Get("/dashboard/import/", reqSignedIn, Index) + r.Get("/admin/settings", reqGrafanaAdmin, Index) r.Get("/admin/users", reqGrafanaAdmin, Index) + r.Get("/admin/users/create", reqGrafanaAdmin, Index) + r.Get("/admin/users/edit/:id", reqGrafanaAdmin, Index) r.Get("/dashboard/*", reqSignedIn, Index) // sign up @@ -43,20 +46,21 @@ func Register(r *macaron.Macaron) { r.Group("/user", func() { r.Get("/", GetUser) r.Put("/", bind(m.UpdateUserCommand{}), UpdateUser) - r.Post("/using/:id", SetUsingAccount) - r.Get("/accounts", GetUserAccounts) + r.Post("/using/:id", UserSetUsingOrg) + r.Get("/orgs", GetUserOrgList) r.Post("/stars/dashboard/:id", StarDashboard) r.Delete("/stars/dashboard/:id", UnstarDashboard) + r.Put("/password", bind(m.ChangeUserPasswordCommand{}), ChangeUserPassword) }) // account - r.Group("/account", func() { - r.Get("/", GetAccount) - r.Post("/", bind(m.CreateAccountCommand{}), CreateAccount) - r.Put("/", bind(m.UpdateAccountCommand{}), UpdateAccount) - r.Post("/users", bind(m.AddAccountUserCommand{}), AddAccountUser) - r.Get("/users", GetAccountUsers) - r.Delete("/users/:id", RemoveAccountUser) + r.Group("/org", func() { + r.Get("/", GetOrg) + r.Post("/", bind(m.CreateOrgCommand{}), CreateOrg) + r.Put("/", bind(m.UpdateOrgCommand{}), UpdateOrg) + r.Post("/users", bind(m.AddOrgUserCommand{}), AddOrgUser) + r.Get("/users", GetOrgUsers) + r.Delete("/users/:id", RemoveOrgUser) }, reqAccountAdmin) // auth api keys @@ -70,9 +74,12 @@ func Register(r *macaron.Macaron) { r.Group("/datasources", func() { r.Combo("/").Get(GetDataSources).Put(AddDataSource).Post(UpdateDataSource) r.Delete("/:id", DeleteDataSource) - r.Any("/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest) + r.Get("/:id", GetDataSourceById) }, reqAccountAdmin) + r.Get("/frontend/settings/", GetFrontendSettings) + r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest) + // Dashboard r.Group("/dashboards", func() { r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard) @@ -89,7 +96,14 @@ func Register(r *macaron.Macaron) { // admin api r.Group("/api/admin", func() { + r.Get("/settings", AdminGetSettings) r.Get("/users", AdminSearchUsers) + r.Get("/users/:id", AdminGetUser) + r.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser) + r.Put("/users/:id/details", bind(dtos.AdminUpdateUserForm{}), AdminUpdateUser) + r.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword) + r.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions) + r.Delete("/users/:id", AdminDeleteUser) }, reqGrafanaAdmin) // rendering diff --git a/pkg/api/apikey.go b/pkg/api/apikey.go index fdb004d314f..2ae0a1e89ae 100644 --- a/pkg/api/apikey.go +++ b/pkg/api/apikey.go @@ -8,7 +8,7 @@ import ( ) func GetApiKeys(c *middleware.Context) { - query := m.GetApiKeysQuery{AccountId: c.AccountId} + query := m.GetApiKeysQuery{OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { c.JsonApiErr(500, "Failed to list api keys", err) @@ -29,7 +29,7 @@ func GetApiKeys(c *middleware.Context) { func DeleteApiKey(c *middleware.Context) { id := c.ParamsInt64(":id") - cmd := &m.DeleteApiKeyCommand{Id: id, AccountId: c.AccountId} + cmd := &m.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId} err := bus.Dispatch(cmd) if err != nil { @@ -46,7 +46,7 @@ func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) { return } - cmd.AccountId = c.AccountId + cmd.OrgId = c.OrgId cmd.Key = util.GetRandomString(64) if err := bus.Dispatch(&cmd); err != nil { @@ -62,3 +62,20 @@ func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) { c.JSON(200, result) } + +func UpdateApiKey(c *middleware.Context, cmd m.UpdateApiKeyCommand) { + if !cmd.Role.IsValid() { + c.JsonApiErr(400, "Invalid role specified", nil) + return + } + + cmd.OrgId = c.OrgId + + err := bus.Dispatch(&cmd) + if err != nil { + c.JsonApiErr(500, "Failed to update api key", err) + return + } + + c.JsonOK("API key updated") +} diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 3fd3932e810..925a790305e 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -29,7 +29,7 @@ func isDasboardStarredByUser(c *middleware.Context, dashId int64) (bool, error) func GetDashboard(c *middleware.Context) { slug := c.Params(":slug") - query := m.GetDashboardQuery{Slug: slug, AccountId: c.AccountId} + query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId} err := bus.Dispatch(&query) if err != nil { c.JsonApiErr(404, "Dashboard not found", nil) @@ -54,13 +54,13 @@ func GetDashboard(c *middleware.Context) { func DeleteDashboard(c *middleware.Context) { slug := c.Params(":slug") - query := m.GetDashboardQuery{Slug: slug, AccountId: c.AccountId} + query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { c.JsonApiErr(404, "Dashboard not found", nil) return } - cmd := m.DeleteDashboardCommand{Slug: slug, AccountId: c.AccountId} + cmd := m.DeleteDashboardCommand{Slug: slug, OrgId: c.OrgId} if err := bus.Dispatch(&cmd); err != nil { c.JsonApiErr(500, "Failed to delete dashboard", err) return @@ -72,7 +72,7 @@ func DeleteDashboard(c *middleware.Context) { } func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) { - cmd.AccountId = c.AccountId + cmd.OrgId = c.OrgId err := bus.Dispatch(&cmd) if err != nil { diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 636fbe4d1fa..7aaf029c05c 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -17,6 +17,7 @@ func NewReverseProxy(ds *m.DataSource, proxyPath string) *httputil.ReverseProxy director := func(req *http.Request) { req.URL.Scheme = target.Scheme req.URL.Host = target.Host + req.Host = target.Host reqQueryVals := req.URL.Query() @@ -25,6 +26,12 @@ func NewReverseProxy(ds *m.DataSource, proxyPath string) *httputil.ReverseProxy reqQueryVals.Add("u", ds.User) reqQueryVals.Add("p", ds.Password) req.URL.RawQuery = reqQueryVals.Encode() + } else if ds.Type == m.DS_INFLUXDB_08 { + req.URL.Path = util.JoinUrlFragments(target.Path, proxyPath) + reqQueryVals.Add("db", ds.Database) + reqQueryVals.Add("u", ds.User) + reqQueryVals.Add("p", ds.Password) + req.URL.RawQuery = reqQueryVals.Encode() } else { req.URL.Path = util.JoinUrlFragments(target.Path, proxyPath) } @@ -38,8 +45,8 @@ func ProxyDataSourceRequest(c *middleware.Context) { id := c.ParamsInt64(":id") query := m.GetDataSourceByIdQuery{ - Id: id, - AccountId: c.AccountId, + Id: id, + OrgId: c.OrgId, } err := bus.Dispatch(&query) diff --git a/pkg/api/dataproxy_test.go b/pkg/api/dataproxy_test.go index adf0b645a18..2ec7b868038 100644 --- a/pkg/api/dataproxy_test.go +++ b/pkg/api/dataproxy_test.go @@ -10,7 +10,7 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func TestAccountDataAccess(t *testing.T) { +func TestDataSourceProxy(t *testing.T) { Convey("When getting graphite datasource proxy", t, func() { ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE} diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index c4a7ae4d657..f4f406504e7 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -8,10 +8,9 @@ import ( ) func GetDataSources(c *middleware.Context) { - query := m.GetDataSourcesQuery{AccountId: c.AccountId} - err := bus.Dispatch(&query) + query := m.GetDataSourcesQuery{OrgId: c.OrgId} - if err != nil { + if err := bus.Dispatch(&query); err != nil { c.JsonApiErr(500, "Failed to query datasources", err) return } @@ -20,7 +19,7 @@ func GetDataSources(c *middleware.Context) { for i, ds := range query.Result { result[i] = &dtos.DataSource{ Id: ds.Id, - AccountId: ds.AccountId, + OrgId: ds.OrgId, Name: ds.Name, Url: ds.Url, Type: ds.Type, @@ -36,6 +35,34 @@ func GetDataSources(c *middleware.Context) { c.JSON(200, result) } +func GetDataSourceById(c *middleware.Context) { + query := m.GetDataSourceByIdQuery{ + Id: c.ParamsInt64(":id"), + OrgId: c.OrgId, + } + + if err := bus.Dispatch(&query); err != nil { + c.JsonApiErr(500, "Failed to query datasources", err) + return + } + + ds := query.Result + + c.JSON(200, &dtos.DataSource{ + Id: ds.Id, + OrgId: ds.OrgId, + Name: ds.Name, + Url: ds.Url, + Type: ds.Type, + Access: ds.Access, + Password: ds.Password, + Database: ds.Database, + User: ds.User, + BasicAuth: ds.BasicAuth, + IsDefault: ds.IsDefault, + }) +} + func DeleteDataSource(c *middleware.Context) { id := c.ParamsInt64(":id") @@ -44,7 +71,7 @@ func DeleteDataSource(c *middleware.Context) { return } - cmd := &m.DeleteDataSourceCommand{Id: id, AccountId: c.AccountId} + cmd := &m.DeleteDataSourceCommand{Id: id, OrgId: c.OrgId} err := bus.Dispatch(cmd) if err != nil { @@ -63,7 +90,7 @@ func AddDataSource(c *middleware.Context) { return } - cmd.AccountId = c.AccountId + cmd.OrgId = c.OrgId if err := bus.Dispatch(&cmd); err != nil { c.JsonApiErr(500, "Failed to add datasource", err) @@ -81,7 +108,7 @@ func UpdateDataSource(c *middleware.Context) { return } - cmd.AccountId = c.AccountId + cmd.OrgId = c.OrgId err := bus.Dispatch(&cmd) if err != nil { diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 12d72314f8d..efcdd2e1c60 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -19,8 +19,8 @@ type CurrentUser struct { Login string `json:"login"` Email string `json:"email"` Name string `json:"name"` - AccountRole m.RoleType `json:"accountRole"` - AccountName string `json:"acountName"` + OrgRole m.RoleType `json:"orgRole"` + OrgName string `json:"orgName"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"` GravatarUrl string `json:"gravatarUrl"` } @@ -38,7 +38,7 @@ type Dashboard struct { type DataSource struct { Id int64 `json:"id"` - AccountId int64 `json:"accountId"` + OrgId int64 `json:"orgId"` Name string `json:"name"` Type m.DsType `json:"type"` Access m.DsAccess `json:"access"` diff --git a/pkg/api/dtos/user.go b/pkg/api/dtos/user.go new file mode 100644 index 00000000000..0047b3c5a98 --- /dev/null +++ b/pkg/api/dtos/user.go @@ -0,0 +1,22 @@ +package dtos + +type AdminCreateUserForm struct { + Email string `json:"email"` + Login string `json:"login"` + Name string `json:"name"` + Password string `json:"password" binding:"Required"` +} + +type AdminUpdateUserForm struct { + Email string `json:"email"` + Login string `json:"login"` + Name string `json:"name"` +} + +type AdminUpdateUserPasswordForm struct { + Password string `json:"password" binding:"Required"` +} + +type AdminUpdateUserPermissionsForm struct { + IsGrafanaAdmin bool `json:"IsGrafanaAdmin" binding:"Required"` +} diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 071852a5e8e..568155d5d43 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -9,23 +9,23 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func getFrontendSettings(c *middleware.Context) (map[string]interface{}, error) { - accountDataSources := make([]*m.DataSource, 0) +func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, error) { + orgDataSources := make([]*m.DataSource, 0) if c.IsSignedIn { - query := m.GetDataSourcesQuery{AccountId: c.AccountId} + query := m.GetDataSourcesQuery{OrgId: c.OrgId} err := bus.Dispatch(&query) if err != nil { return nil, err } - accountDataSources = query.Result + orgDataSources = query.Result } datasources := make(map[string]interface{}) - for _, ds := range accountDataSources { + for _, ds := range orgDataSources { url := ds.Url if ds.Access == m.DS_ACCESS_PROXY { @@ -38,7 +38,7 @@ func getFrontendSettings(c *middleware.Context) (map[string]interface{}, error) "default": ds.IsDefault, } - if ds.Type == m.DS_INFLUXDB { + if ds.Type == m.DS_INFLUXDB_08 { if ds.Access == m.DS_ACCESS_DIRECT { dsMap["username"] = ds.User dsMap["password"] = ds.Password @@ -46,6 +46,15 @@ func getFrontendSettings(c *middleware.Context) (map[string]interface{}, error) } } + if ds.Type == m.DS_INFLUXDB { + if ds.Access == m.DS_ACCESS_DIRECT { + dsMap["username"] = ds.User + dsMap["password"] = ds.Password + dsMap["database"] = ds.Database + dsMap["url"] = url + } + } + if ds.Type == m.DS_ES { dsMap["index"] = ds.Database } @@ -71,3 +80,13 @@ func getFrontendSettings(c *middleware.Context) (map[string]interface{}, error) return jsonObj, nil } + +func GetFrontendSettings(c *middleware.Context) { + settings, err := getFrontendSettingsMap(c) + if err != nil { + c.JsonApiErr(400, "Failed to get frontend settings", err) + return + } + + c.JSON(200, settings) +} diff --git a/pkg/api/index.go b/pkg/api/index.go index 98860b26e6a..5cd161bb3d2 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -7,7 +7,7 @@ import ( ) func setIndexViewData(c *middleware.Context) error { - settings, err := getFrontendSettings(c) + settings, err := getFrontendSettingsMap(c) if err != nil { return err } @@ -17,8 +17,8 @@ func setIndexViewData(c *middleware.Context) error { Login: c.Login, Email: c.Email, Name: c.Name, - AccountName: c.AccountName, - AccountRole: c.AccountRole, + OrgName: c.OrgName, + OrgRole: c.OrgRole, GravatarUrl: dtos.GetGravatarUrl(c.Email), IsGrafanaAdmin: c.IsGrafanaAdmin, } diff --git a/pkg/api/org.go b/pkg/api/org.go new file mode 100644 index 00000000000..8b41b0e3f5f --- /dev/null +++ b/pkg/api/org.go @@ -0,0 +1,50 @@ +package api + +import ( + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/middleware" + m "github.com/grafana/grafana/pkg/models" +) + +func GetOrg(c *middleware.Context) { + query := m.GetOrgByIdQuery{Id: c.OrgId} + + if err := bus.Dispatch(&query); err != nil { + if err == m.ErrOrgNotFound { + c.JsonApiErr(404, "Organization not found", err) + return + } + + c.JsonApiErr(500, "Failed to get organization", err) + return + } + + org := m.OrgDTO{ + Id: query.Result.Id, + Name: query.Result.Name, + } + + c.JSON(200, &org) +} + +func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) { + cmd.UserId = c.UserId + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "Failed to create organization", err) + return + } + + c.JsonOK("Organization created") +} + +func UpdateOrg(c *middleware.Context, cmd m.UpdateOrgCommand) { + cmd.OrgId = c.OrgId + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "Failed to update organization", err) + return + } + + c.JsonOK("Organization updated") +} diff --git a/pkg/api/account_users.go b/pkg/api/org_users.go similarity index 58% rename from pkg/api/account_users.go rename to pkg/api/org_users.go index ef2ad1a84db..123ba08c4c7 100644 --- a/pkg/api/account_users.go +++ b/pkg/api/org_users.go @@ -6,7 +6,7 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func AddAccountUser(c *middleware.Context, cmd m.AddAccountUserCommand) { +func AddOrgUser(c *middleware.Context, cmd m.AddOrgUserCommand) { if !cmd.Role.IsValid() { c.JsonApiErr(400, "Invalid role specified", nil) return @@ -26,19 +26,19 @@ func AddAccountUser(c *middleware.Context, cmd m.AddAccountUserCommand) { return } - cmd.AccountId = c.AccountId + cmd.OrgId = c.OrgId cmd.UserId = userToAdd.Id if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "Could not add user to account", err) + c.JsonApiErr(500, "Could not add user to organization", err) return } - c.JsonOK("User added to account") + c.JsonOK("User added to organization") } -func GetAccountUsers(c *middleware.Context) { - query := m.GetAccountUsersQuery{AccountId: c.AccountId} +func GetOrgUsers(c *middleware.Context) { + query := m.GetOrgUsersQuery{OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { c.JsonApiErr(500, "Failed to get account user", err) @@ -48,18 +48,18 @@ func GetAccountUsers(c *middleware.Context) { c.JSON(200, query.Result) } -func RemoveAccountUser(c *middleware.Context) { +func RemoveOrgUser(c *middleware.Context) { userId := c.ParamsInt64(":id") - cmd := m.RemoveAccountUserCommand{AccountId: c.AccountId, UserId: userId} + cmd := m.RemoveOrgUserCommand{OrgId: c.OrgId, UserId: userId} if err := bus.Dispatch(&cmd); err != nil { - if err == m.ErrLastAccountAdmin { - c.JsonApiErr(400, "Cannot remove last account admin", nil) + if err == m.ErrLastOrgAdmin { + c.JsonApiErr(400, "Cannot remove last organization admin", nil) return } - c.JsonApiErr(500, "Failed to remove user from account", err) + c.JsonApiErr(500, "Failed to remove user from organization", err) } - c.JsonOK("User removed from account") + c.JsonOK("User removed from organization") } diff --git a/pkg/api/search.go b/pkg/api/search.go index d0f93851d79..c37bba7b669 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -4,7 +4,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" ) // TODO: this needs to be cached or improved somehow @@ -45,7 +44,7 @@ func Search(c *middleware.Context) { if tagcloud == "true" { - query := m.GetDashboardTagsQuery{AccountId: c.AccountId} + query := m.GetDashboardTagsQuery{OrgId: c.OrgId} err := bus.Dispatch(&query) if err != nil { c.JsonApiErr(500, "Failed to get tags from database", err) @@ -61,7 +60,7 @@ func Search(c *middleware.Context) { UserId: c.UserId, Limit: limit, IsStarred: starred == "true", - AccountId: c.AccountId, + OrgId: c.OrgId, } err := bus.Dispatch(&query) @@ -76,9 +75,6 @@ func Search(c *middleware.Context) { } result.Dashboards = query.Result - for _, dash := range result.Dashboards { - dash.Url = setting.ToAbsUrl("dashboard/db/" + dash.Slug) - } } c.JSON(200, result) diff --git a/pkg/api/user.go b/pkg/api/user.go index 306f7c7b3d4..f6fb0e41034 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -4,13 +4,14 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/util" ) func GetUser(c *middleware.Context) { query := m.GetUserInfoQuery{UserId: c.UserId} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to get account", err) + c.JsonApiErr(500, "Failed to get user", err) return } @@ -21,23 +22,23 @@ func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) { cmd.UserId = c.UserId if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(400, "Failed to update account", err) + c.JsonApiErr(400, "Failed to update user", err) return } - c.JsonOK("Account updated") + c.JsonOK("User updated") } -func GetUserAccounts(c *middleware.Context) { - query := m.GetUserAccountsQuery{UserId: c.UserId} +func GetUserOrgList(c *middleware.Context) { + query := m.GetUserOrgListQuery{UserId: c.UserId} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to get user accounts", err) + c.JsonApiErr(500, "Failed to get user organizations", err) return } for _, ac := range query.Result { - if ac.AccountId == c.AccountId { + if ac.OrgId == c.OrgId { ac.IsUsing = true break } @@ -46,17 +47,17 @@ func GetUserAccounts(c *middleware.Context) { c.JSON(200, query.Result) } -func validateUsingAccount(userId int64, accountId int64) bool { - query := m.GetUserAccountsQuery{UserId: userId} +func validateUsingOrg(userId int64, orgId int64) bool { + query := m.GetUserOrgListQuery{UserId: userId} if err := bus.Dispatch(&query); err != nil { return false } - // validate that the account id in the list + // validate that the org id in the list valid := false for _, other := range query.Result { - if other.AccountId == accountId { + if other.OrgId == orgId { valid = true } } @@ -64,23 +65,53 @@ func validateUsingAccount(userId int64, accountId int64) bool { return valid } -func SetUsingAccount(c *middleware.Context) { - usingAccountId := c.ParamsInt64(":id") +func UserSetUsingOrg(c *middleware.Context) { + orgId := c.ParamsInt64(":id") - if !validateUsingAccount(c.UserId, usingAccountId) { - c.JsonApiErr(401, "Not a valid account", nil) + if !validateUsingOrg(c.UserId, orgId) { + c.JsonApiErr(401, "Not a valid organization", nil) return } - cmd := m.SetUsingAccountCommand{ - UserId: c.UserId, - AccountId: usingAccountId, + cmd := m.SetUsingOrgCommand{ + UserId: c.UserId, + OrgId: orgId, } if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "Failed change active account", err) + c.JsonApiErr(500, "Failed change active organization", err) return } - c.JsonOK("Active account changed") + c.JsonOK("Active organization changed") +} + +func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) { + userQuery := m.GetUserByIdQuery{Id: c.UserId} + + if err := bus.Dispatch(&userQuery); err != nil { + c.JsonApiErr(500, "Could not read user from database", err) + return + } + + passwordHashed := util.EncodePassword(cmd.OldPassword, userQuery.Result.Salt) + if passwordHashed != userQuery.Result.Password { + c.JsonApiErr(401, "Invalid old password", nil) + return + } + + if len(cmd.NewPassword) < 4 { + c.JsonApiErr(400, "New password too short", nil) + return + } + + cmd.UserId = c.UserId + cmd.NewPassword = util.EncodePassword(cmd.NewPassword, userQuery.Result.Salt) + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "Failed to change user password", err) + return + } + + c.JsonOK("User password changed") } diff --git a/pkg/cmd/common.go b/pkg/cmd/common.go new file mode 100644 index 00000000000..338baa36aac --- /dev/null +++ b/pkg/cmd/common.go @@ -0,0 +1,13 @@ +package cmd + +import ( + "github.com/codegangsta/cli" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" +) + +func initRuntime(c *cli.Context) { + setting.NewConfigContext(c.GlobalString("config")) + sqlstore.NewEngine() + sqlstore.EnsureAdminUser() +} diff --git a/pkg/cmd/dashboard.go b/pkg/cmd/dashboard.go new file mode 100644 index 00000000000..04645fb618e --- /dev/null +++ b/pkg/cmd/dashboard.go @@ -0,0 +1,104 @@ +package cmd + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + + "github.com/codegangsta/cli" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" +) + +var ImportDashboard = cli.Command{ + Name: "dashboards:import", + Usage: "imports dashboards in JSON from a directory", + Description: "Starts Grafana import process", + Action: runImport, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "dir", + Usage: "path to folder containing json dashboards", + }, + }, +} + +func runImport(c *cli.Context) { + dir := c.String("dir") + if len(dir) == 0 { + log.ConsoleFatalf("Missing command flag --dir") + } + + file, err := os.Stat(dir) + if os.IsNotExist(err) { + log.ConsoleFatalf("Directory does not exist: %v", dir) + } + + if !file.IsDir() { + log.ConsoleFatalf("%v is not a directory", dir) + } + + if !c.Args().Present() { + log.ConsoleFatal("Organization name arg is required") + } + + orgName := c.Args().First() + + initRuntime(c) + + orgQuery := m.GetOrgByNameQuery{Name: orgName} + if err := bus.Dispatch(&orgQuery); err != nil { + log.ConsoleFatalf("Failed to find account", err) + } + + orgId := orgQuery.Result.Id + + visitor := func(path string, f os.FileInfo, err error) error { + if err != nil { + return err + } + if f.IsDir() { + return nil + } + if strings.HasSuffix(f.Name(), ".json") { + if err := importDashboard(path, orgId); err != nil { + log.ConsoleFatalf("Failed to import dashboard file: %v, err: %v", path, err) + } + } + return nil + } + + if err := filepath.Walk(dir, visitor); err != nil { + log.ConsoleFatalf("Failed to scan dir for json files: %v", err) + } +} + +func importDashboard(path string, orgId int64) error { + log.ConsoleInfof("Importing %v", path) + + reader, err := os.Open(path) + if err != nil { + return err + } + + dash := m.NewDashboard("temp") + jsonParser := json.NewDecoder(reader) + + if err := jsonParser.Decode(&dash.Data); err != nil { + return err + } + dash.Data["id"] = nil + + cmd := m.SaveDashboardCommand{ + OrgId: orgId, + Dashboard: dash.Data, + } + + if err := bus.Dispatch(&cmd); err != nil { + return err + } + + return nil +} diff --git a/pkg/cmd/datasource.go b/pkg/cmd/datasource.go new file mode 100644 index 00000000000..072756521fb --- /dev/null +++ b/pkg/cmd/datasource.go @@ -0,0 +1,230 @@ +package cmd + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/codegangsta/cli" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" +) + +var ( + ListDataSources = cli.Command{ + Name: "datasources", + Usage: "list datasources", + Description: "Lists the datasources in the system", + Action: listDatasources, + } + CreateDataSource = cli.Command{ + Name: "datasources:create", + Usage: "creates a new datasource", + Description: "Creates a new datasource", + Action: createDataSource, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "type", + Value: "graphite", + Usage: fmt.Sprintf("Datasource type [%s,%s,%s,%s]", + m.DS_GRAPHITE, m.DS_INFLUXDB, m.DS_ES, m.DS_OPENTSDB), + }, + cli.StringFlag{ + Name: "access", + Value: "proxy", + Usage: "Datasource access [proxy,direct]", + }, + cli.BoolFlag{ + Name: "default", + Usage: "Make this the default datasource", + }, + cli.StringFlag{ + Name: "db", + Usage: "InfluxDB DB", + }, + cli.StringFlag{ + Name: "user", + Usage: "InfluxDB username", + }, + cli.StringFlag{ + Name: "password", + Usage: "InfluxDB password", + }, + }, + } + DescribeDataSource = cli.Command{ + Name: "datasources:info", + Usage: "describe the details of a datasource", + Description: "Describes the details of a datasource", + Action: describeDataSource, + } + DeleteDataSource = cli.Command{ + Name: "datasources:delete", + Usage: "Deletes a datasource", + Description: "Deletes a datasource", + Action: deleteDataSource, + } +) + +func createDataSource(c *cli.Context) { + initRuntime(c) + + if len(c.Args()) != 3 { + log.ConsoleFatal("Missing required arguments") + } + + name := c.Args().First() + ds := c.Args()[1] + url := c.Args()[2] + dsType := c.String("type") + dsAccess := c.String("access") + dsDefault := c.Bool("default") + + orgQuery := m.GetOrgByNameQuery{Name: name} + if err := bus.Dispatch(&orgQuery); err != nil { + log.ConsoleFatalf("Failed to find organization: %s", err) + } + + orgId := orgQuery.Result.Id + + query := m.GetDataSourceByNameQuery{OrgId: orgId, Name: ds} + if err := bus.Dispatch(&query); err != nil { + if err != m.ErrDataSourceNotFound { + log.ConsoleFatalf("Failed to query for existing datasource: %s", err) + } + } + + if query.Result.Id > 0 { + log.ConsoleFatalf("DataSource %s already exists", ds) + } + + cmd := m.AddDataSourceCommand{ + OrgId: orgId, + Name: ds, + Url: url, + Type: m.DsType(dsType), + Access: m.DsAccess(dsAccess), + IsDefault: dsDefault, + } + + switch dsType { + case m.DS_INFLUXDB: + db := c.String("db") + if db == "" { + log.ConsoleFatal("db name is required for influxdb datasources") + } + cmd.Database = db + cmd.User = c.String("user") + cmd.Password = c.String("password") + } + + if err := bus.Dispatch(&cmd); err != nil { + log.ConsoleFatalf("Failed to create datasource: %s", err) + } + datasource := cmd.Result + + log.ConsoleInfof("Datasource %s created", datasource.Name) +} + +func listDatasources(c *cli.Context) { + initRuntime(c) + + if !c.Args().Present() { + log.ConsoleFatal("Account name arg is required") + } + + name := c.Args().First() + orgQuery := m.GetOrgByNameQuery{Name: name} + if err := bus.Dispatch(&orgQuery); err != nil { + log.ConsoleFatalf("Failed to find organization: %s", err) + } + + orgId := orgQuery.Result.Id + + query := m.GetDataSourcesQuery{OrgId: orgId} + if err := bus.Dispatch(&query); err != nil { + log.ConsoleFatalf("Failed to find datasources: %s", err) + } + + w := tabwriter.NewWriter(os.Stdout, 8, 1, 4, ' ', 0) + + fmt.Fprintf(w, "ID\tNAME\tURL\tTYPE\tACCESS\tDEFAULT\n") + for _, ds := range query.Result { + fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\n", ds.Id, ds.Name, ds.Url, ds.Type, + ds.Access, ds.IsDefault) + } + w.Flush() +} + +func describeDataSource(c *cli.Context) { + initRuntime(c) + + if len(c.Args()) != 2 { + log.ConsoleFatal("Organization and datasource name args are required") + } + + name := c.Args().First() + ds := c.Args()[1] + + orgQuery := m.GetOrgByNameQuery{Name: name} + if err := bus.Dispatch(&orgQuery); err != nil { + log.ConsoleFatalf("Failed to find organization: %s", err) + } + + orgId := orgQuery.Result.Id + + query := m.GetDataSourceByNameQuery{OrgId: orgId, Name: ds} + if err := bus.Dispatch(&query); err != nil { + log.ConsoleFatalf("Failed to find datasource: %s", err) + } + datasource := query.Result + + w := tabwriter.NewWriter(os.Stdout, 8, 1, 4, ' ', 0) + fmt.Fprintf(w, "NAME\t%s\n", datasource.Name) + fmt.Fprintf(w, "URL\t%s\n", datasource.Url) + fmt.Fprintf(w, "DEFAULT\t%t\n", datasource.IsDefault) + fmt.Fprintf(w, "ACCESS\t%s\n", datasource.Access) + fmt.Fprintf(w, "TYPE\t%s\n", datasource.Type) + + switch datasource.Type { + case m.DS_INFLUXDB: + fmt.Fprintf(w, "DATABASE\t%s\n", datasource.Database) + fmt.Fprintf(w, "DB USER\t%s\n", datasource.User) + fmt.Fprintf(w, "DB PASSWORD\t%s\n", datasource.Password) + case m.DS_ES: + fmt.Fprintf(w, "INDEX\t%s\n", datasource.Database) + } + w.Flush() +} + +func deleteDataSource(c *cli.Context) { + initRuntime(c) + + if len(c.Args()) != 2 { + log.ConsoleFatal("Account and datasource name args are required") + } + + name := c.Args().First() + ds := c.Args()[1] + + orgQuery := m.GetOrgByNameQuery{Name: name} + if err := bus.Dispatch(&orgQuery); err != nil { + log.ConsoleFatalf("Failed to find organization: %s", err) + } + + orgId := orgQuery.Result.Id + + query := m.GetDataSourceByNameQuery{OrgId: orgId, Name: ds} + if err := bus.Dispatch(&query); err != nil { + log.ConsoleFatalf("Failed to find datasource: %s", err) + } + datasource := query.Result + + cmd := m.DeleteDataSourceCommand{OrgId: orgId, Id: datasource.Id} + if err := bus.Dispatch(&cmd); err != nil { + log.ConsoleFatalf("Failed to delete datasource: %s", err) + } + + log.ConsoleInfof("DataSource %s deleted", ds) +} diff --git a/pkg/cmd/import.go b/pkg/cmd/import.go deleted file mode 100644 index 1c3545904a2..00000000000 --- a/pkg/cmd/import.go +++ /dev/null @@ -1,120 +0,0 @@ -package cmd - -import ( - "encoding/json" - "os" - "path/filepath" - "strings" - - "github.com/codegangsta/cli" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/log" - m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" -) - -var CmdImportJson = cli.Command{ - Name: "import-json", - Usage: "grafana import", - Description: "Starts Grafana import process", - Action: runImport, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "dir", - Usage: "path to folder containing json dashboards", - }, - cli.StringFlag{ - Name: "account", - Usage: "Account name to save dashboards under", - }, - cli.StringFlag{ - Name: "config", - Value: "grafana.ini", - Usage: "path to config file", - }, - }, -} - -func runImport(c *cli.Context) { - dir := c.String("dir") - if len(dir) == 0 { - log.Error(3, "Missing command flag --dir") - return - } - - file, err := os.Stat(dir) - if os.IsNotExist(err) { - log.Error(3, "Directory does not exist: %v", dir) - return - } - - if !file.IsDir() { - log.Error(3, "%v is not a directory", dir) - return - } - - accountName := c.String("account") - if len(accountName) == 0 { - log.Error(3, "Missing command flag --account") - return - } - - setting.NewConfigContext() - sqlstore.NewEngine() - sqlstore.EnsureAdminUser() - - accountQuery := m.GetAccountByNameQuery{Name: accountName} - if err := bus.Dispatch(&accountQuery); err != nil { - log.Error(3, "Failed to find account", err) - return - } - - accountId := accountQuery.Result.Id - - visitor := func(path string, f os.FileInfo, err error) error { - if err != nil { - return err - } - if f.IsDir() { - return nil - } - if strings.HasSuffix(f.Name(), ".json") { - if err := importDashboard(path, accountId); err != nil { - log.Error(3, "Failed to import dashboard file: %v, err: %v", path, err) - } - } - return nil - } - - if err := filepath.Walk(dir, visitor); err != nil { - log.Error(3, "failed to scan dir for json files: %v", err) - } -} - -func importDashboard(path string, accountId int64) error { - log.Info("Importing %v", path) - - reader, err := os.Open(path) - if err != nil { - return err - } - - dash := m.NewDashboard("temp") - jsonParser := json.NewDecoder(reader) - - if err := jsonParser.Decode(&dash.Data); err != nil { - return err - } - - cmd := m.SaveDashboardCommand{ - AccountId: accountId, - Dashboard: dash.Data, - } - - if err := bus.Dispatch(&cmd); err != nil { - return err - } - - return nil -} diff --git a/pkg/cmd/orgs.go b/pkg/cmd/orgs.go new file mode 100644 index 00000000000..bcc2eeec38a --- /dev/null +++ b/pkg/cmd/orgs.go @@ -0,0 +1,99 @@ +package cmd + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/codegangsta/cli" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" +) + +var ListOrgs = cli.Command{ + Name: "orgs", + Usage: "list organizations", + Description: "Lists the organizations in the system", + Action: listOrgs, +} + +var CreateOrg = cli.Command{ + Name: "orgs:create", + Usage: "Creates a new organization", + Description: "Creates a new organization", + Action: createOrg, +} + +var DeleteOrg = cli.Command{ + Name: "orgs:delete", + Usage: "Delete an existing organization", + Description: "Deletes an existing organization", + Action: deleteOrg, +} + +func listOrgs(c *cli.Context) { + initRuntime(c) + + orgsQuery := m.GetOrgListQuery{} + if err := bus.Dispatch(&orgsQuery); err != nil { + log.ConsoleFatalf("Failed to find organizations: %s", err) + } + + w := tabwriter.NewWriter(os.Stdout, 8, 1, 4, ' ', 0) + + fmt.Fprintf(w, "ID\tNAME\n") + for _, org := range orgsQuery.Result { + fmt.Fprintf(w, "%d\t%s\n", org.Id, org.Name) + } + w.Flush() +} + +func createOrg(c *cli.Context) { + initRuntime(c) + + if !c.Args().Present() { + log.ConsoleFatal("Organization name arg is required") + } + + name := c.Args().First() + + adminQuery := m.GetUserByLoginQuery{LoginOrEmail: setting.AdminUser} + + if err := bus.Dispatch(&adminQuery); err == m.ErrUserNotFound { + log.ConsoleFatalf("Failed to find default admin user: %s", err) + } + + adminUser := adminQuery.Result + + cmd := m.CreateOrgCommand{Name: name, UserId: adminUser.Id} + if err := bus.Dispatch(&cmd); err != nil { + log.ConsoleFatalf("Failed to create organization: %s", err) + } + + log.ConsoleInfof("Organization %s created for admin user %s\n", name, adminUser.Email) +} + +func deleteOrg(c *cli.Context) { + initRuntime(c) + + if !c.Args().Present() { + log.ConsoleFatal("Organization name arg is required") + } + + name := c.Args().First() + orgQuery := m.GetOrgByNameQuery{Name: name} + if err := bus.Dispatch(&orgQuery); err != nil { + log.ConsoleFatalf("Failed to find organization: %s", err) + } + + orgId := orgQuery.Result.Id + cmd := m.DeleteOrgCommand{Id: orgId} + if err := bus.Dispatch(&cmd); err != nil { + log.ConsoleFatalf("Failed to delete organization: %s", err) + } + + log.ConsoleInfof("Organization %s deleted", name) +} diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index d76e7858302..ad3cd28f1af 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -17,23 +17,15 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/services/eventpublisher" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/social" ) -var CmdWeb = cli.Command{ +var Web = cli.Command{ Name: "web", - Usage: "grafana web", + Usage: "Starts Grafana backend & web server", Description: "Starts Grafana backend & web server", Action: runWeb, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "config", - Value: "grafana.ini", - Usage: "path to config file", - }, - }, } func newMacaron() *macaron.Macaron { @@ -78,10 +70,9 @@ func runWeb(c *cli.Context) { log.Info("Starting Grafana") log.Info("Version: %v, Commit: %v, Build date: %v", setting.BuildVersion, setting.BuildCommit, time.Unix(setting.BuildStamp, 0)) - setting.NewConfigContext() + initRuntime(c) + social.NewOAuthService() - sqlstore.NewEngine() - sqlstore.EnsureAdminUser() eventpublisher.Init() var err error diff --git a/pkg/events/events.go b/pkg/events/events.go index 2477bbef13e..c3dcac3e2b5 100644 --- a/pkg/events/events.go +++ b/pkg/events/events.go @@ -50,13 +50,13 @@ func ToOnWriteEvent(event interface{}) (*OnTheWireEvent, error) { return &wireEvent, nil } -type AccountCreated struct { +type OrgCreated struct { Timestamp time.Time `json:"timestamp"` Id int64 `json:"id"` Name string `json:"name"` } -type AccountUpdated struct { +type OrgUpdated struct { Timestamp time.Time `json:"timestamp"` Id int64 `json:"id"` Name string `json:"name"` diff --git a/pkg/log/console.go b/pkg/log/console.go index f5a8b96fd76..2a6bbf6a3bb 100644 --- a/pkg/log/console.go +++ b/pkg/log/console.go @@ -6,6 +6,7 @@ package log import ( "encoding/json" + "fmt" "log" "os" "runtime" @@ -21,15 +22,26 @@ func NewBrush(color string) Brush { } } -var colors = []Brush{ - NewBrush("1;36"), // Trace cyan - NewBrush("1;34"), // Debug blue - NewBrush("1;32"), // Info green - NewBrush("1;33"), // Warn yellow - NewBrush("1;31"), // Error red - NewBrush("1;35"), // Critical purple - NewBrush("1;31"), // Fatal red -} +var ( + Red = NewBrush("1;31") + Purple = NewBrush("1;35") + Yellow = NewBrush("1;33") + Green = NewBrush("1;32") + Blue = NewBrush("1;34") + Cyan = NewBrush("1;36") + + colors = []Brush{ + Cyan, // Trace cyan + Blue, // Debug blue + Green, // Info green + Yellow, // Warn yellow + Red, // Error red + Purple, // Critical purple + Red, // Fatal red + } + consoleWriter = &ConsoleWriter{lg: log.New(os.Stdout, "", 0), + Level: TRACE} +) // ConsoleWriter implements LoggerInterface and writes messages to terminal. type ConsoleWriter struct { @@ -40,7 +52,7 @@ type ConsoleWriter struct { // create ConsoleWriter returning as LoggerInterface. func NewConsole() LoggerInterface { return &ConsoleWriter{ - lg: log.New(os.Stdout, "", log.Ldate|log.Ltime), + lg: log.New(os.Stderr, "", log.Ldate|log.Ltime), Level: TRACE, } } @@ -68,6 +80,76 @@ func (_ *ConsoleWriter) Flush() { func (_ *ConsoleWriter) Destroy() { } +func printConsole(level int, msg string) { + consoleWriter.WriteMsg(msg, 0, level) +} + +func printfConsole(level int, format string, v ...interface{}) { + consoleWriter.WriteMsg(fmt.Sprintf(format, v...), 0, level) +} + +// ConsoleTrace prints to stdout using TRACE colors +func ConsoleTrace(s string) { + printConsole(TRACE, s) +} + +// ConsoleTracef prints a formatted string to stdout using TRACE colors +func ConsoleTracef(format string, v ...interface{}) { + printfConsole(TRACE, format, v...) +} + +// ConsoleDebug prints to stdout using DEBUG colors +func ConsoleDebug(s string) { + printConsole(DEBUG, s) +} + +// ConsoleDebugf prints a formatted string to stdout using DEBUG colors +func ConsoleDebugf(format string, v ...interface{}) { + printfConsole(DEBUG, format, v...) +} + +// ConsoleInfo prints to stdout using INFO colors +func ConsoleInfo(s string) { + printConsole(INFO, s) +} + +// ConsoleInfof prints a formatted string to stdout using INFO colors +func ConsoleInfof(format string, v ...interface{}) { + printfConsole(INFO, format, v...) +} + +// ConsoleWarn prints to stdout using WARN colors +func ConsoleWarn(s string) { + printConsole(WARN, s) +} + +// ConsoleWarnf prints a formatted string to stdout using WARN colors +func ConsoleWarnf(format string, v ...interface{}) { + printfConsole(WARN, format, v...) +} + +// ConsoleError prints to stdout using ERROR colors +func ConsoleError(s string) { + printConsole(ERROR, s) +} + +// ConsoleErrorf prints a formatted string to stdout using ERROR colors +func ConsoleErrorf(format string, v ...interface{}) { + printfConsole(ERROR, format, v...) +} + +// ConsoleFatal prints to stdout using FATAL colors +func ConsoleFatal(s string) { + printConsole(FATAL, s) + os.Exit(1) +} + +// ConsoleFatalf prints a formatted string to stdout using FATAL colors +func ConsoleFatalf(format string, v ...interface{}) { + printfConsole(FATAL, format, v...) + os.Exit(1) +} + func init() { Register("console", NewConsole) } diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 3a94f2f9bbf..9a86059064d 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -56,7 +56,7 @@ func RoleAuth(roles ...m.RoleType) macaron.Handler { return func(c *Context) { ok := false for _, role := range roles { - if role == c.AccountRole { + if role == c.OrgRole { ok = true break } diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index d6a86de5283..61b0bb570ab 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -56,22 +56,23 @@ func GetContextHandler() macaron.Handler { ctx.SignedInUser = &m.SignedInUser{} // TODO: fix this - ctx.AccountRole = keyInfo.Role + ctx.OrgRole = keyInfo.Role ctx.ApiKeyId = keyInfo.Id - ctx.AccountId = keyInfo.AccountId + ctx.OrgId = keyInfo.OrgId } } else if setting.AnonymousEnabled { - accountQuery := m.GetAccountByNameQuery{Name: setting.AnonymousAccountName} - if err := bus.Dispatch(&accountQuery); err != nil { - if err == m.ErrAccountNotFound { - log.Error(3, "Anonymous access account name does not exist", nil) + orgQuery := m.GetOrgByNameQuery{Name: setting.AnonymousOrgName} + if err := bus.Dispatch(&orgQuery); err != nil { + if err == m.ErrOrgNotFound { + log.Error(3, "Anonymous access organization name does not exist", nil) } } else { ctx.IsSignedIn = false ctx.HasAnonymousAccess = true ctx.SignedInUser = &m.SignedInUser{} - ctx.AccountRole = m.RoleType(setting.AnonymousAccountRole) - ctx.AccountId = accountQuery.Result.Id + ctx.OrgRole = m.RoleType(setting.AnonymousOrgRole) + ctx.OrgId = orgQuery.Result.Id + ctx.OrgName = orgQuery.Result.Name } } diff --git a/pkg/models/account.go b/pkg/models/account.go deleted file mode 100644 index ad4cdc47e53..00000000000 --- a/pkg/models/account.go +++ /dev/null @@ -1,62 +0,0 @@ -package models - -import ( - "errors" - "time" -) - -// Typed errors -var ( - ErrAccountNotFound = errors.New("Account not found") -) - -type Account struct { - Id int64 - Version int - Name string - Created time.Time - Updated time.Time -} - -// --------------------- -// COMMANDS - -type CreateAccountCommand struct { - Name string `json:"name" binding:"Required"` - - // initial admin user for account - UserId int64 `json:"-"` - Result Account `json:"-"` -} - -type UpdateAccountCommand struct { - Name string `json:"name" binding:"Required"` - AccountId int64 `json:"-"` -} - -type GetUserAccountsQuery struct { - UserId int64 - Result []*UserAccountDTO -} - -type GetAccountByIdQuery struct { - Id int64 - Result *Account -} - -type GetAccountByNameQuery struct { - Name string - Result *Account -} - -type AccountDTO struct { - Id int64 `json:"id"` - Name string `json:"name"` -} - -type UserAccountDTO struct { - AccountId int64 `json:"accountId"` - Name string `json:"name"` - Role RoleType `json:"role"` - IsUsing bool `json:"isUsing"` -} diff --git a/pkg/models/account_user.go b/pkg/models/account_user.go deleted file mode 100644 index de57573262d..00000000000 --- a/pkg/models/account_user.go +++ /dev/null @@ -1,67 +0,0 @@ -package models - -import ( - "errors" - "time" -) - -// Typed errors -var ( - ErrInvalidRoleType = errors.New("Invalid role type") - ErrLastAccountAdmin = errors.New("Cannot remove last account admin") -) - -type RoleType string - -const ( - ROLE_VIEWER RoleType = "Viewer" - ROLE_EDITOR RoleType = "Editor" - ROLE_ADMIN RoleType = "Admin" -) - -func (r RoleType) IsValid() bool { - return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR -} - -type AccountUser struct { - AccountId int64 - UserId int64 - Role RoleType - Created time.Time - Updated time.Time -} - -// --------------------- -// COMMANDS - -type RemoveAccountUserCommand struct { - UserId int64 - AccountId int64 -} - -type AddAccountUserCommand struct { - LoginOrEmail string `json:"loginOrEmail" binding:"Required"` - Role RoleType `json:"role" binding:"Required"` - - AccountId int64 `json:"-"` - UserId int64 `json:"-"` -} - -// ---------------------- -// QUERIES - -type GetAccountUsersQuery struct { - AccountId int64 - Result []*AccountUserDTO -} - -// ---------------------- -// Projections and DTOs - -type AccountUserDTO struct { - AccountId int64 `json:"accountId"` - UserId int64 `json:"userId"` - Email string `json:"email"` - Login string `json:"login"` - Role string `json:"role"` -} diff --git a/pkg/models/apikey.go b/pkg/models/apikey.go index ddaaaa3e4ef..148e64b4946 100644 --- a/pkg/models/apikey.go +++ b/pkg/models/apikey.go @@ -8,22 +8,22 @@ import ( var ErrInvalidApiKey = errors.New("Invalid API Key") type ApiKey struct { - Id int64 - AccountId int64 - Name string - Key string - Role RoleType - Created time.Time - Updated time.Time + Id int64 + OrgId int64 + Name string + Key string + Role RoleType + Created time.Time + Updated time.Time } // --------------------- // COMMANDS type AddApiKeyCommand struct { - Name string `json:"name" binding:"Required"` - Role RoleType `json:"role" binding:"Required"` - AccountId int64 `json:"-"` - Key string `json:"-"` + Name string `json:"name" binding:"Required"` + Role RoleType `json:"role" binding:"Required"` + OrgId int64 `json:"-"` + Key string `json:"-"` Result *ApiKey `json:"-"` } @@ -33,20 +33,20 @@ type UpdateApiKeyCommand struct { Name string `json:"name"` Role RoleType `json:"role"` - AccountId int64 `json:"-"` + OrgId int64 `json:"-"` } type DeleteApiKeyCommand struct { - Id int64 `json:"id"` - AccountId int64 `json:"-"` + Id int64 `json:"id"` + OrgId int64 `json:"-"` } // ---------------------- // QUERIES type GetApiKeysQuery struct { - AccountId int64 - Result []*ApiKey + OrgId int64 + Result []*ApiKey } type GetApiKeyByKeyQuery struct { diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 0ebde7aa163..a5fd415ac0b 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -14,10 +14,10 @@ var ( ) type Dashboard struct { - Id int64 - Slug string - AccountId int64 - Version int + Id int64 + Slug string + OrgId int64 + Version int Created time.Time Updated time.Time @@ -53,7 +53,7 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { dash := &Dashboard{} dash.Data = cmd.Dashboard dash.Title = dash.Data["title"].(string) - dash.AccountId = cmd.AccountId + dash.OrgId = cmd.OrgId dash.UpdateSlug() if dash.Data["id"] != nil { @@ -80,14 +80,14 @@ func (dash *Dashboard) UpdateSlug() { type SaveDashboardCommand struct { Dashboard map[string]interface{} `json:"dashboard"` - AccountId int64 `json:"-"` + OrgId int64 `json:"-"` Result *Dashboard } type DeleteDashboardCommand struct { - Slug string - AccountId int64 + Slug string + OrgId int64 } // @@ -95,8 +95,8 @@ type DeleteDashboardCommand struct { // type GetDashboardQuery struct { - Slug string - AccountId int64 + Slug string + OrgId int64 Result *Dashboard } diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index cd43f8d66f2..f4e090fb9e5 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -8,7 +8,9 @@ import ( const ( DS_GRAPHITE = "graphite" DS_INFLUXDB = "influxdb" + DS_INFLUXDB_08 = "influxdb_08" DS_ES = "elasticsearch" + DS_OPENTSDB = "opentsdb" DS_ACCESS_DIRECT = "direct" DS_ACCESS_PROXY = "proxy" ) @@ -22,9 +24,9 @@ type DsType string type DsAccess string type DataSource struct { - Id int64 - AccountId int64 - Version int + Id int64 + OrgId int64 + Version int Name string Type DsType @@ -47,7 +49,7 @@ type DataSource struct { // Also acts as api DTO type AddDataSourceCommand struct { - AccountId int64 `json:"-"` + OrgId int64 `json:"-"` Name string Type DsType Access DsAccess @@ -63,7 +65,7 @@ type AddDataSourceCommand struct { // Also acts as api DTO type UpdateDataSourceCommand struct { Id int64 - AccountId int64 + OrgId int64 Name string Type DsType Access DsAccess @@ -75,22 +77,28 @@ type UpdateDataSourceCommand struct { } type DeleteDataSourceCommand struct { - Id int64 - AccountId int64 + Id int64 + OrgId int64 } // --------------------- // QUERIES type GetDataSourcesQuery struct { - AccountId int64 - Result []*DataSource + OrgId int64 + Result []*DataSource } type GetDataSourceByIdQuery struct { - Id int64 - AccountId int64 - Result DataSource + Id int64 + OrgId int64 + Result DataSource +} + +type GetDataSourceByNameQuery struct { + Name string + OrgId int64 + Result DataSource } // --------------------- diff --git a/pkg/models/org.go b/pkg/models/org.go new file mode 100644 index 00000000000..ab6d97b9ae8 --- /dev/null +++ b/pkg/models/org.go @@ -0,0 +1,65 @@ +package models + +import ( + "errors" + "time" +) + +// Typed errors +var ( + ErrOrgNotFound = errors.New("Organization not found") +) + +type Org struct { + Id int64 + Version int + Name string + Created time.Time + Updated time.Time +} + +// --------------------- +// COMMANDS + +type CreateOrgCommand struct { + Name string `json:"name" binding:"Required"` + + // initial admin user for account + UserId int64 `json:"-"` + Result Org `json:"-"` +} + +type DeleteOrgCommand struct { + Id int64 +} + +type UpdateOrgCommand struct { + Name string `json:"name" binding:"Required"` + OrgId int64 `json:"-"` +} + +type GetOrgByIdQuery struct { + Id int64 + Result *Org +} + +type GetOrgByNameQuery struct { + Name string + Result *Org +} + +type GetOrgListQuery struct { + Result []*Org +} + +type OrgDTO struct { + Id int64 `json:"id"` + Name string `json:"name"` +} + +type UserOrgDTO struct { + OrgId int64 `json:"orgId"` + Name string `json:"name"` + Role RoleType `json:"role"` + IsUsing bool `json:"isUsing"` +} diff --git a/pkg/models/org_user.go b/pkg/models/org_user.go new file mode 100644 index 00000000000..811d02e1afe --- /dev/null +++ b/pkg/models/org_user.go @@ -0,0 +1,67 @@ +package models + +import ( + "errors" + "time" +) + +// Typed errors +var ( + ErrInvalidRoleType = errors.New("Invalid role type") + ErrLastOrgAdmin = errors.New("Cannot remove last organization admin") +) + +type RoleType string + +const ( + ROLE_VIEWER RoleType = "Viewer" + ROLE_EDITOR RoleType = "Editor" + ROLE_ADMIN RoleType = "Admin" +) + +func (r RoleType) IsValid() bool { + return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR +} + +type OrgUser struct { + OrgId int64 + UserId int64 + Role RoleType + Created time.Time + Updated time.Time +} + +// --------------------- +// COMMANDS + +type RemoveOrgUserCommand struct { + UserId int64 + OrgId int64 +} + +type AddOrgUserCommand struct { + LoginOrEmail string `json:"loginOrEmail" binding:"Required"` + Role RoleType `json:"role" binding:"Required"` + + OrgId int64 `json:"-"` + UserId int64 `json:"-"` +} + +// ---------------------- +// QUERIES + +type GetOrgUsersQuery struct { + OrgId int64 + Result []*OrgUserDTO +} + +// ---------------------- +// Projections and DTOs + +type OrgUserDTO struct { + OrgId int64 `json:"orgId"` + UserId int64 `json:"userId"` + Email string `json:"email"` + Login string `json:"login"` + Role string `json:"role"` +} diff --git a/pkg/models/search.go b/pkg/models/search.go index 4f5b24ff084..e5c69f0038e 100644 --- a/pkg/models/search.go +++ b/pkg/models/search.go @@ -11,7 +11,6 @@ type DashboardSearchHit struct { Title string `json:"title"` Slug string `json:"slug"` Tags []string `json:"tags"` - Url string `json:"url"` IsStarred bool `json:"isStarred"` } @@ -23,7 +22,7 @@ type DashboardTagCloudItem struct { type SearchDashboardsQuery struct { Title string Tag string - AccountId int64 + OrgId int64 UserId int64 Limit int IsStarred bool @@ -32,6 +31,6 @@ type SearchDashboardsQuery struct { } type GetDashboardTagsQuery struct { - AccountId int64 - Result []*DashboardTagCloudItem + OrgId int64 + Result []*DashboardTagCloudItem } diff --git a/pkg/models/user.go b/pkg/models/user.go index 0a3de0c9a4d..d5a3dbfacb6 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -11,18 +11,20 @@ var ( ) type User struct { - Id int64 - Version int - Email string - Name string - Login string - Password string - Salt string - Rands string - Company string + Id int64 + Version int + Email string + Name string + Login string + Password string + Salt string + Rands string + Company string + EmailVerified bool + Theme string - IsAdmin bool - AccountId int64 + IsAdmin bool + OrgId int64 Created time.Time Updated time.Time @@ -50,9 +52,25 @@ type UpdateUserCommand struct { UserId int64 `json:"-"` } -type SetUsingAccountCommand struct { - UserId int64 - AccountId int64 +type ChangeUserPasswordCommand struct { + OldPassword string `json:"oldPassword"` + NewPassword string `json:"newPassword"` + + UserId int64 `json:"-"` +} + +type UpdateUserPermissionsCommand struct { + IsGrafanaAdmin bool + UserId int64 `json:"-"` +} + +type DeleteUserCommand struct { + UserId int64 +} + +type SetUsingOrgCommand struct { + UserId int64 + OrgId int64 } // ---------------------- @@ -63,6 +81,11 @@ type GetUserByLoginQuery struct { Result *User } +type GetUserByIdQuery struct { + Id int64 + Result *User +} + type GetSignedInUserQuery struct { UserId int64 Result *SignedInUser @@ -81,14 +104,19 @@ type SearchUsersQuery struct { Result []*UserSearchHitDTO } +type GetUserOrgListQuery struct { + UserId int64 + Result []*UserOrgDTO +} + // ------------------------ // DTO & Projections type SignedInUser struct { UserId int64 - AccountId int64 - AccountName string - AccountRole RoleType + OrgId int64 + OrgName string + OrgRole RoleType Login string Name string Email string @@ -97,9 +125,10 @@ type SignedInUser struct { } type UserDTO struct { - Email string `json:"email"` - Name string `json:"name"` - Login string `json:"login"` + Email string `json:"email"` + Name string `json:"name"` + Login string `json:"login"` + IsGrafanaAdmin bool `json:"isGrafanaAdmin"` } type UserSearchHitDTO struct { diff --git a/pkg/services/sqlstore/account.go b/pkg/services/sqlstore/account.go deleted file mode 100644 index ff1f2fa0546..00000000000 --- a/pkg/services/sqlstore/account.go +++ /dev/null @@ -1,103 +0,0 @@ -package sqlstore - -import ( - "time" - - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/events" - m "github.com/grafana/grafana/pkg/models" -) - -func init() { - bus.AddHandler("sql", GetAccountById) - bus.AddHandler("sql", CreateAccount) - bus.AddHandler("sql", SetUsingAccount) - bus.AddHandler("sql", UpdateAccount) - bus.AddHandler("sql", GetAccountByName) -} - -func GetAccountById(query *m.GetAccountByIdQuery) error { - var account m.Account - exists, err := x.Id(query.Id).Get(&account) - if err != nil { - return err - } - - if !exists { - return m.ErrAccountNotFound - } - - query.Result = &account - return nil -} - -func GetAccountByName(query *m.GetAccountByNameQuery) error { - var account m.Account - exists, err := x.Where("name=?", query.Name).Get(&account) - if err != nil { - return err - } - - if !exists { - return m.ErrAccountNotFound - } - - query.Result = &account - return nil -} - -func CreateAccount(cmd *m.CreateAccountCommand) error { - return inTransaction2(func(sess *session) error { - - account := m.Account{ - Name: cmd.Name, - Created: time.Now(), - Updated: time.Now(), - } - - if _, err := sess.Insert(&account); err != nil { - return err - } - - user := m.AccountUser{ - AccountId: account.Id, - UserId: cmd.UserId, - Role: m.ROLE_ADMIN, - Created: time.Now(), - Updated: time.Now(), - } - - _, err := sess.Insert(&user) - cmd.Result = account - - sess.publishAfterCommit(&events.AccountCreated{ - Timestamp: account.Created, - Id: account.Id, - Name: account.Name, - }) - - return err - }) -} - -func UpdateAccount(cmd *m.UpdateAccountCommand) error { - return inTransaction2(func(sess *session) error { - - account := m.Account{ - Name: cmd.Name, - Updated: time.Now(), - } - - if _, err := sess.Id(cmd.AccountId).Update(&account); err != nil { - return err - } - - sess.publishAfterCommit(&events.AccountUpdated{ - Timestamp: account.Updated, - Id: account.Id, - Name: account.Name, - }) - - return nil - }) -} diff --git a/pkg/services/sqlstore/account_users.go b/pkg/services/sqlstore/account_users.go deleted file mode 100644 index 7aa5de4db3f..00000000000 --- a/pkg/services/sqlstore/account_users.go +++ /dev/null @@ -1,67 +0,0 @@ -package sqlstore - -import ( - "fmt" - "time" - - "github.com/go-xorm/xorm" - - "github.com/grafana/grafana/pkg/bus" - m "github.com/grafana/grafana/pkg/models" -) - -func init() { - bus.AddHandler("sql", AddAccountUser) - bus.AddHandler("sql", RemoveAccountUser) - bus.AddHandler("sql", GetAccountUsers) -} - -func AddAccountUser(cmd *m.AddAccountUserCommand) error { - return inTransaction(func(sess *xorm.Session) error { - - entity := m.AccountUser{ - AccountId: cmd.AccountId, - UserId: cmd.UserId, - Role: cmd.Role, - Created: time.Now(), - Updated: time.Now(), - } - - _, err := sess.Insert(&entity) - return err - }) -} - -func GetAccountUsers(query *m.GetAccountUsersQuery) error { - query.Result = make([]*m.AccountUserDTO, 0) - sess := x.Table("account_user") - sess.Join("INNER", "user", fmt.Sprintf("account_user.user_id=%s.id", x.Dialect().Quote("user"))) - sess.Where("account_user.account_id=?", query.AccountId) - sess.Cols("account_user.account_id", "account_user.user_id", "user.email", "user.login", "account_user.role") - sess.Asc("user.email", "user.login") - - err := sess.Find(&query.Result) - return err -} - -func RemoveAccountUser(cmd *m.RemoveAccountUserCommand) error { - return inTransaction(func(sess *xorm.Session) error { - var rawSql = "DELETE FROM account_user WHERE account_id=? and user_id=?" - _, err := sess.Exec(rawSql, cmd.AccountId, cmd.UserId) - if err != nil { - return err - } - - // validate that there is an admin user left - res, err := sess.Query("SELECT 1 from account_user WHERE account_id=? and role='Admin'", cmd.AccountId) - if err != nil { - return err - } - - if len(res) == 0 { - return m.ErrLastAccountAdmin - } - - return err - }) -} diff --git a/pkg/services/sqlstore/apikey.go b/pkg/services/sqlstore/apikey.go index 458548bd680..d696dbbfa27 100644 --- a/pkg/services/sqlstore/apikey.go +++ b/pkg/services/sqlstore/apikey.go @@ -17,7 +17,7 @@ func init() { } func GetApiKeys(query *m.GetApiKeysQuery) error { - sess := x.Limit(100, 0).Where("account_id=?", query.AccountId).Asc("name") + sess := x.Limit(100, 0).Where("org_id=?", query.OrgId).Asc("name") query.Result = make([]*m.ApiKey, 0) return sess.Find(&query.Result) @@ -25,8 +25,8 @@ func GetApiKeys(query *m.GetApiKeysQuery) error { func DeleteApiKey(cmd *m.DeleteApiKeyCommand) error { return inTransaction(func(sess *xorm.Session) error { - var rawSql = "DELETE FROM api_key WHERE id=? and account_id=?" - _, err := sess.Exec(rawSql, cmd.Id, cmd.AccountId) + var rawSql = "DELETE FROM api_key WHERE id=? and org_id=?" + _, err := sess.Exec(rawSql, cmd.Id, cmd.OrgId) return err }) } @@ -34,12 +34,12 @@ func DeleteApiKey(cmd *m.DeleteApiKeyCommand) error { func AddApiKey(cmd *m.AddApiKeyCommand) error { return inTransaction(func(sess *xorm.Session) error { t := m.ApiKey{ - AccountId: cmd.AccountId, - Name: cmd.Name, - Role: cmd.Role, - Key: cmd.Key, - Created: time.Now(), - Updated: time.Now(), + OrgId: cmd.OrgId, + Name: cmd.Name, + Role: cmd.Role, + Key: cmd.Key, + Created: time.Now(), + Updated: time.Now(), } if _, err := sess.Insert(&t); err != nil { @@ -53,20 +53,20 @@ func AddApiKey(cmd *m.AddApiKeyCommand) error { func UpdateApiKey(cmd *m.UpdateApiKeyCommand) error { return inTransaction(func(sess *xorm.Session) error { t := m.ApiKey{ - Id: cmd.Id, - AccountId: cmd.AccountId, - Name: cmd.Name, - Role: cmd.Role, - Updated: time.Now(), + Id: cmd.Id, + OrgId: cmd.OrgId, + Name: cmd.Name, + Role: cmd.Role, + Updated: time.Now(), } - _, err := sess.Where("id=? and account_id=?", t.Id, t.AccountId).Update(&t) + _, err := sess.Where("id=? and org_id=?", t.Id, t.OrgId).Update(&t) return err }) } func GetApiKeyByKey(query *m.GetApiKeyByKeyQuery) error { var apikey m.ApiKey - has, err := x.Where("key=?", query.Key).Get(&apikey) + has, err := x.Where("`key`=?", query.Key).Get(&apikey) if err != nil { return err diff --git a/pkg/services/sqlstore/apikey_test.go b/pkg/services/sqlstore/apikey_test.go new file mode 100644 index 00000000000..6f54d6b8627 --- /dev/null +++ b/pkg/services/sqlstore/apikey_test.go @@ -0,0 +1,31 @@ +package sqlstore + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" + + m "github.com/grafana/grafana/pkg/models" +) + +func TestApiKeyDataAccess(t *testing.T) { + + Convey("Testing API Key data access", t, func() { + InitTestDB(t) + + Convey("Given saved api key", func() { + cmd := m.AddApiKeyCommand{OrgId: 1, Key: "hello"} + err := AddApiKey(&cmd) + So(err, ShouldBeNil) + + Convey("Should be able to get key by key", func() { + query := m.GetApiKeyByKeyQuery{Key: "hello"} + err = GetApiKeyByKey(&query) + So(err, ShouldBeNil) + + So(query.Result, ShouldNotBeNil) + }) + + }) + }) +} diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 89b7e254fea..243def6f4c2 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -22,7 +22,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { dash := cmd.GetDashboardModel() // try get existing dashboard - existing := m.Dashboard{Slug: dash.Slug, AccountId: dash.AccountId} + existing := m.Dashboard{Slug: dash.Slug, OrgId: dash.OrgId} hasExisting, err := sess.Get(&existing) if err != nil { return err @@ -61,7 +61,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { } func GetDashboard(query *m.GetDashboardQuery) error { - dashboard := m.Dashboard{Slug: query.Slug, AccountId: query.AccountId} + dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId} has, err := x.Get(&dashboard) if err != nil { return err @@ -98,9 +98,9 @@ func SearchDashboards(query *m.SearchDashboardsQuery) error { sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id") } - sql.WriteString(` WHERE dashboard.account_id=?`) + sql.WriteString(` WHERE dashboard.org_id=?`) - params = append(params, query.AccountId) + params = append(params, query.OrgId) if query.IsStarred { sql.WriteString(` AND star.user_id=?`) @@ -158,11 +158,11 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error { term FROM dashboard INNER JOIN dashboard_tag on dashboard_tag.dashboard_id = dashboard.id - WHERE dashboard.account_id=? + WHERE dashboard.org_id=? GROUP BY term` query.Result = make([]*m.DashboardTagCloudItem, 0) - sess := x.Sql(sql, query.AccountId) + sess := x.Sql(sql, query.OrgId) err := sess.Find(&query.Result) return err } @@ -171,8 +171,8 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { sess := x.NewSession() defer sess.Close() - rawSql := "DELETE FROM Dashboard WHERE account_id=? and slug=?" - _, err := sess.Exec(rawSql, cmd.AccountId, cmd.Slug) + rawSql := "DELETE FROM dashboard WHERE org_id=? and slug=?" + _, err := sess.Exec(rawSql, cmd.OrgId, cmd.Slug) return err } diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index e4830693459..c7a8053d528 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -8,9 +8,9 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func insertTestDashboard(title string, accountId int64, tags ...interface{}) *m.Dashboard { +func insertTestDashboard(title string, orgId int64, tags ...interface{}) *m.Dashboard { cmd := m.SaveDashboardCommand{ - AccountId: accountId, + OrgId: orgId, Dashboard: map[string]interface{}{ "id": nil, "title": title, @@ -40,8 +40,8 @@ func TestDashboardDataAccess(t *testing.T) { Convey("Should be able to get dashboard", func() { query := m.GetDashboardQuery{ - Slug: "test-dash-23", - AccountId: 1, + Slug: "test-dash-23", + OrgId: 1, } err := GetDashboard(&query) @@ -53,8 +53,8 @@ func TestDashboardDataAccess(t *testing.T) { Convey("Should be able to search for dashboard", func() { query := m.SearchDashboardsQuery{ - Title: "test", - AccountId: 1, + Title: "test", + OrgId: 1, } err := SearchDashboards(&query) @@ -66,8 +66,8 @@ func TestDashboardDataAccess(t *testing.T) { }) Convey("Should be able to search for dashboards using tags", func() { - query1 := m.SearchDashboardsQuery{Tag: "webapp", AccountId: 1} - query2 := m.SearchDashboardsQuery{Tag: "tagdoesnotexist", AccountId: 1} + query1 := m.SearchDashboardsQuery{Tag: "webapp", OrgId: 1} + query2 := m.SearchDashboardsQuery{Tag: "tagdoesnotexist", OrgId: 1} err := SearchDashboards(&query1) err = SearchDashboards(&query2) @@ -79,7 +79,7 @@ func TestDashboardDataAccess(t *testing.T) { Convey("Should not be able to save dashboard with same name", func() { cmd := m.SaveDashboardCommand{ - AccountId: 1, + OrgId: 1, Dashboard: map[string]interface{}{ "id": nil, "title": "test dash 23", @@ -92,7 +92,7 @@ func TestDashboardDataAccess(t *testing.T) { }) Convey("Should be able to get dashboard tags", func() { - query := m.GetDashboardTagsQuery{AccountId: 1} + query := m.GetDashboardTagsQuery{OrgId: 1} err := GetDashboardTags(&query) So(err, ShouldBeNil) @@ -113,7 +113,7 @@ func TestDashboardDataAccess(t *testing.T) { }) Convey("Should be able to search for starred dashboards", func() { - query := m.SearchDashboardsQuery{AccountId: 1, UserId: 10, IsStarred: true} + query := m.SearchDashboardsQuery{OrgId: 1, UserId: 10, IsStarred: true} err := SearchDashboards(&query) So(err, ShouldBeNil) diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index 842b2bfb1c6..7da46ae72da 100644 --- a/pkg/services/sqlstore/datasource.go +++ b/pkg/services/sqlstore/datasource.go @@ -15,10 +15,21 @@ func init() { bus.AddHandler("sql", DeleteDataSource) bus.AddHandler("sql", UpdateDataSource) bus.AddHandler("sql", GetDataSourceById) + bus.AddHandler("sql", GetDataSourceByName) } func GetDataSourceById(query *m.GetDataSourceByIdQuery) error { - sess := x.Limit(100, 0).Where("account_id=? AND id=?", query.AccountId, query.Id) + sess := x.Limit(100, 0).Where("org_id=? AND id=?", query.OrgId, query.Id) + has, err := sess.Get(&query.Result) + + if !has { + return m.ErrDataSourceNotFound + } + return err +} + +func GetDataSourceByName(query *m.GetDataSourceByNameQuery) error { + sess := x.Limit(100, 0).Where("org_id=? AND name=?", query.OrgId, query.Name) has, err := sess.Get(&query.Result) if !has { @@ -28,7 +39,7 @@ func GetDataSourceById(query *m.GetDataSourceByIdQuery) error { } func GetDataSources(query *m.GetDataSourcesQuery) error { - sess := x.Limit(100, 0).Where("account_id=?", query.AccountId).Asc("name") + sess := x.Limit(100, 0).Where("org_id=?", query.OrgId).Asc("name") query.Result = make([]*m.DataSource, 0) return sess.Find(&query.Result) @@ -36,8 +47,8 @@ func GetDataSources(query *m.GetDataSourcesQuery) error { func DeleteDataSource(cmd *m.DeleteDataSourceCommand) error { return inTransaction(func(sess *xorm.Session) error { - var rawSql = "DELETE FROM data_source WHERE id=? and account_id=?" - _, err := sess.Exec(rawSql, cmd.Id, cmd.AccountId) + var rawSql = "DELETE FROM data_source WHERE id=? and org_id=?" + _, err := sess.Exec(rawSql, cmd.Id, cmd.OrgId) return err }) } @@ -46,7 +57,7 @@ func AddDataSource(cmd *m.AddDataSourceCommand) error { return inTransaction(func(sess *xorm.Session) error { ds := &m.DataSource{ - AccountId: cmd.AccountId, + OrgId: cmd.OrgId, Name: cmd.Name, Type: cmd.Type, Access: cmd.Access, @@ -74,8 +85,8 @@ func AddDataSource(cmd *m.AddDataSourceCommand) error { func updateIsDefaultFlag(ds *m.DataSource, sess *xorm.Session) error { // Handle is default flag if ds.IsDefault { - rawSql := "UPDATE data_source SET is_default = 0 WHERE account_id=? AND id <> ?" - if _, err := sess.Exec(rawSql, ds.AccountId, ds.Id); err != nil { + rawSql := "UPDATE data_source SET is_default = 0 WHERE org_id=? AND id <> ?" + if _, err := sess.Exec(rawSql, ds.OrgId, ds.Id); err != nil { return err } } @@ -87,7 +98,7 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error { return inTransaction(func(sess *xorm.Session) error { ds := &m.DataSource{ Id: cmd.Id, - AccountId: cmd.AccountId, + OrgId: cmd.OrgId, Name: cmd.Name, Type: cmd.Type, Access: cmd.Access, @@ -101,7 +112,7 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error { sess.UseBool("is_default") - _, err := sess.Where("id=? and account_id=?", ds.Id, ds.AccountId).Update(ds) + _, err := sess.Where("id=? and org_id=?", ds.Id, ds.OrgId).Update(ds) if err != nil { return err } diff --git a/pkg/services/sqlstore/datasource_test.go b/pkg/services/sqlstore/datasource_test.go index 17873196d8f..4142602c472 100644 --- a/pkg/services/sqlstore/datasource_test.go +++ b/pkg/services/sqlstore/datasource_test.go @@ -42,16 +42,16 @@ func TestDataAccess(t *testing.T) { Convey("Can add datasource", func() { err := AddDataSource(&m.AddDataSourceCommand{ - AccountId: 10, - Type: m.DS_INFLUXDB, - Access: m.DS_ACCESS_DIRECT, - Url: "http://test", - Database: "site", + OrgId: 10, + Type: m.DS_INFLUXDB, + Access: m.DS_ACCESS_DIRECT, + Url: "http://test", + Database: "site", }) So(err, ShouldBeNil) - query := m.GetDataSourcesQuery{AccountId: 10} + query := m.GetDataSourcesQuery{OrgId: 10} err = GetDataSources(&query) So(err, ShouldBeNil) @@ -59,33 +59,33 @@ func TestDataAccess(t *testing.T) { ds := query.Result[0] - So(ds.AccountId, ShouldEqual, 10) + So(ds.OrgId, ShouldEqual, 10) So(ds.Database, ShouldEqual, "site") }) Convey("Given a datasource", func() { AddDataSource(&m.AddDataSourceCommand{ - AccountId: 10, - Type: m.DS_GRAPHITE, - Access: m.DS_ACCESS_DIRECT, - Url: "http://test", + OrgId: 10, + Type: m.DS_GRAPHITE, + Access: m.DS_ACCESS_DIRECT, + Url: "http://test", }) - query := m.GetDataSourcesQuery{AccountId: 10} + query := m.GetDataSourcesQuery{OrgId: 10} GetDataSources(&query) ds := query.Result[0] Convey("Can delete datasource", func() { - err := DeleteDataSource(&m.DeleteDataSourceCommand{Id: ds.Id, AccountId: ds.AccountId}) + err := DeleteDataSource(&m.DeleteDataSourceCommand{Id: ds.Id, OrgId: ds.OrgId}) So(err, ShouldBeNil) GetDataSources(&query) So(len(query.Result), ShouldEqual, 0) }) - Convey("Can not delete datasource with wrong accountId", func() { - err := DeleteDataSource(&m.DeleteDataSourceCommand{Id: ds.Id, AccountId: 123123}) + Convey("Can not delete datasource with wrong orgId", func() { + err := DeleteDataSource(&m.DeleteDataSourceCommand{Id: ds.Id, OrgId: 123123}) So(err, ShouldBeNil) GetDataSources(&query) diff --git a/pkg/services/sqlstore/migrations.go b/pkg/services/sqlstore/migrations.go deleted file mode 100644 index fcc5769053d..00000000000 --- a/pkg/services/sqlstore/migrations.go +++ /dev/null @@ -1,175 +0,0 @@ -package sqlstore - -import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - -// --- Migration Guide line --- -// 1. Never change a migration that is committed and pushed to master -// 2. Always add new migrations (to change or undo previous migrations) -// 3. Some migraitons are not yet written (rename column, table, drop table, index etc) - -func addMigrations(mg *Migrator) { - addMigrationLogMigrations(mg) - addUserMigrations(mg) - addStarMigrations(mg) - addAccountMigrations(mg) - addDashboardMigration(mg) - addDataSourceMigration(mg) - addApiKeyMigrations(mg) -} - -func addMigrationLogMigrations(mg *Migrator) { - mg.AddMigration("create migration_log table", new(AddTableMigration). - Name("migration_log").WithColumns( - &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - &Column{Name: "migration_id", Type: DB_NVarchar, Length: 255}, - &Column{Name: "sql", Type: DB_Text}, - &Column{Name: "success", Type: DB_Bool}, - &Column{Name: "error", Type: DB_Text}, - &Column{Name: "timestamp", Type: DB_DateTime}, - )) -} - -func addUserMigrations(mg *Migrator) { - mg.AddMigration("create user table", new(AddTableMigration). - Name("user").WithColumns( - &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - &Column{Name: "version", Type: DB_Int, Nullable: false}, - &Column{Name: "login", Type: DB_NVarchar, Length: 255, Nullable: false}, - &Column{Name: "email", Type: DB_NVarchar, Length: 255, Nullable: false}, - &Column{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: true}, - &Column{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true}, - &Column{Name: "salt", Type: DB_NVarchar, Length: 50, Nullable: true}, - &Column{Name: "rands", Type: DB_NVarchar, Length: 50, Nullable: true}, - &Column{Name: "company", Type: DB_NVarchar, Length: 255, Nullable: true}, - &Column{Name: "account_id", Type: DB_BigInt, Nullable: false}, - &Column{Name: "is_admin", Type: DB_Bool, Nullable: false}, - &Column{Name: "created", Type: DB_DateTime, Nullable: false}, - &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, - )) - - //------- user table indexes ------------------ - mg.AddMigration("add unique index user.login", new(AddIndexMigration). - Table("user").Columns("login").Unique()) - mg.AddMigration("add unique index user.email", new(AddIndexMigration). - Table("user").Columns("email").Unique()) -} - -func addStarMigrations(mg *Migrator) { - mg.AddMigration("create star table", new(AddTableMigration). - Name("star").WithColumns( - &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - &Column{Name: "user_id", Type: DB_BigInt, Nullable: false}, - &Column{Name: "dashboard_id", Type: DB_BigInt, Nullable: false}, - )) - - mg.AddMigration("add unique index star.user_id_dashboard_id", new(AddIndexMigration). - Table("star").Columns("user_id", "dashboard_id").Unique()) -} - -func addAccountMigrations(mg *Migrator) { - mg.AddMigration("create account table", new(AddTableMigration). - Name("account").WithColumns( - &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - &Column{Name: "version", Type: DB_Int, Nullable: false}, - &Column{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, - &Column{Name: "created", Type: DB_DateTime, Nullable: false}, - &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, - )) - - mg.AddMigration("add unique index account.name", new(AddIndexMigration). - Table("account").Columns("name").Unique()) - - //------- account_user table ------------------- - mg.AddMigration("create account_user table", new(AddTableMigration). - Name("account_user").WithColumns( - &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - &Column{Name: "account_id", Type: DB_BigInt}, - &Column{Name: "user_id", Type: DB_BigInt}, - &Column{Name: "role", Type: DB_NVarchar, Length: 20}, - &Column{Name: "created", Type: DB_DateTime}, - &Column{Name: "updated", Type: DB_DateTime}, - )) - - mg.AddMigration("add unique index account_user_aid_uid", new(AddIndexMigration). - Name("aid_uid").Table("account_user").Columns("account_id", "user_id").Unique()) -} - -func addDashboardMigration(mg *Migrator) { - mg.AddMigration("create dashboard table", new(AddTableMigration). - Name("dashboard").WithColumns( - &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - &Column{Name: "version", Type: DB_Int, Nullable: false}, - &Column{Name: "slug", Type: DB_NVarchar, Length: 255, Nullable: false}, - &Column{Name: "title", Type: DB_NVarchar, Length: 255, Nullable: false}, - &Column{Name: "data", Type: DB_Text, Nullable: false}, - &Column{Name: "account_id", Type: DB_BigInt, Nullable: false}, - &Column{Name: "created", Type: DB_DateTime, Nullable: false}, - &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, - )) - - mg.AddMigration("create dashboard_tag table", new(AddTableMigration). - Name("dashboard_tag").WithColumns( - &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - &Column{Name: "dashboard_id", Type: DB_BigInt, Nullable: false}, - &Column{Name: "term", Type: DB_NVarchar, Length: 50, Nullable: false}, - )) - - //------- indexes ------------------ - mg.AddMigration("add index dashboard.account_id", new(AddIndexMigration). - Table("dashboard").Columns("account_id")) - - mg.AddMigration("add unique index dashboard_account_id_slug", new(AddIndexMigration). - Table("dashboard").Columns("account_id", "slug").Unique()) - - mg.AddMigration("add unique index dashboard_tag.dasboard_id_term", new(AddIndexMigration). - Table("dashboard_tag").Columns("dashboard_id", "term").Unique()) -} - -func addDataSourceMigration(mg *Migrator) { - mg.AddMigration("create data_source table", new(AddTableMigration). - Name("data_source").WithColumns( - &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - &Column{Name: "account_id", Type: DB_BigInt, Nullable: false}, - &Column{Name: "version", Type: DB_Int, Nullable: false}, - &Column{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false}, - &Column{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, - &Column{Name: "access", Type: DB_NVarchar, Length: 255, Nullable: false}, - &Column{Name: "url", Type: DB_NVarchar, Length: 255, Nullable: false}, - &Column{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true}, - &Column{Name: "user", Type: DB_NVarchar, Length: 255, Nullable: true}, - &Column{Name: "database", Type: DB_NVarchar, Length: 255, Nullable: true}, - &Column{Name: "basic_auth", Type: DB_Bool, Nullable: false}, - &Column{Name: "basic_auth_user", Type: DB_NVarchar, Length: 255, Nullable: true}, - &Column{Name: "basic_auth_password", Type: DB_NVarchar, Length: 255, Nullable: true}, - &Column{Name: "is_default", Type: DB_Bool, Nullable: false}, - &Column{Name: "created", Type: DB_DateTime, Nullable: false}, - &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, - )) - - //------- indexes ------------------ - mg.AddMigration("add index data_source.account_id", new(AddIndexMigration). - Table("data_source").Columns("account_id")) - - mg.AddMigration("add unique index data_source.account_id_name", new(AddIndexMigration). - Table("data_source").Columns("account_id", "name").Unique()) -} - -func addApiKeyMigrations(mg *Migrator) { - mg.AddMigration("create api_key table", new(AddTableMigration). - Name("api_key").WithColumns( - &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - &Column{Name: "account_id", Type: DB_BigInt, Nullable: false}, - &Column{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, - &Column{Name: "key", Type: DB_Varchar, Length: 64, Nullable: false}, - &Column{Name: "role", Type: DB_NVarchar, Length: 255, Nullable: false}, - &Column{Name: "created", Type: DB_DateTime, Nullable: false}, - &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, - )) - - //------- indexes ------------------ - mg.AddMigration("add index api_key.account_id", new(AddIndexMigration). - Table("api_key").Columns("account_id")) - - mg.AddMigration("add index api_key.account_id_name", new(AddIndexMigration). - Table("api_key").Columns("account_id", "name").Unique()) -} diff --git a/pkg/services/sqlstore/migrations/apikey_mig.go b/pkg/services/sqlstore/migrations/apikey_mig.go new file mode 100644 index 00000000000..1b2dc433941 --- /dev/null +++ b/pkg/services/sqlstore/migrations/apikey_mig.go @@ -0,0 +1,75 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addApiKeyMigrations(mg *Migrator) { + apiKeyV1 := Table{ + Name: "api_key", + Columns: []*Column{ + &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + &Column{Name: "account_id", Type: DB_BigInt, Nullable: false}, + &Column{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "key", Type: DB_Varchar, Length: 64, Nullable: false}, + &Column{Name: "role", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "created", Type: DB_DateTime, Nullable: false}, + &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + &Index{Cols: []string{"account_id"}}, + &Index{Cols: []string{"key"}, Type: UniqueIndex}, + &Index{Cols: []string{"account_id", "name"}, Type: UniqueIndex}, + }, + } + + // create table + mg.AddMigration("create api_key table", NewAddTableMigration(apiKeyV1)) + // create indices + mg.AddMigration("add index api_key.account_id", NewAddIndexMigration(apiKeyV1, apiKeyV1.Indices[0])) + mg.AddMigration("add index api_key.key", NewAddIndexMigration(apiKeyV1, apiKeyV1.Indices[1])) + mg.AddMigration("add index api_key.account_id_name", NewAddIndexMigration(apiKeyV1, apiKeyV1.Indices[2])) + + // --------------------- + // account -> org changes + + // drop indexes + addDropAllIndicesMigrations(mg, "v1", apiKeyV1) + // rename table + addTableRenameMigration(mg, "api_key", "api_key_v1", "v1") + + apiKeyV2 := Table{ + Name: "api_key", + Columns: []*Column{ + &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + &Column{Name: "org_id", Type: DB_BigInt, Nullable: false}, + &Column{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "key", Type: DB_Varchar, Length: 64, Nullable: false}, + &Column{Name: "role", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "created", Type: DB_DateTime, Nullable: false}, + &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + &Index{Cols: []string{"org_id"}}, + &Index{Cols: []string{"key"}, Type: UniqueIndex}, + &Index{Cols: []string{"org_id", "name"}, Type: UniqueIndex}, + }, + } + + // create v2 table + mg.AddMigration("create api_key table v2", NewAddTableMigration(apiKeyV2)) + + // add v2 indíces + addTableIndicesMigrations(mg, "v2", apiKeyV2) + + //------- copy data from v1 to v2 ------------------- + mg.AddMigration("copy api_key v1 to v2", NewCopyTableDataMigration("api_key", "api_key_v1", map[string]string{ + "id": "id", + "org_id": "account_id", + "name": "name", + "key": "key", + "role": "role", + "created": "created", + "updated": "updated", + })) + + mg.AddMigration("Drop old table api_key_v1", NewDropTableMigration("api_key_v1")) +} diff --git a/pkg/services/sqlstore/migrations/common.go b/pkg/services/sqlstore/migrations/common.go new file mode 100644 index 00000000000..cf0b39f1a35 --- /dev/null +++ b/pkg/services/sqlstore/migrations/common.go @@ -0,0 +1,26 @@ +package migrations + +import ( + "fmt" + + . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" +) + +func addDropAllIndicesMigrations(mg *Migrator, versionSuffix string, table Table) { + for _, index := range table.Indices { + migrationId := fmt.Sprintf("drop index %s - %s", index.XName(table.Name), versionSuffix) + mg.AddMigration(migrationId, NewDropIndexMigration(table, index)) + } +} + +func addTableIndicesMigrations(mg *Migrator, versionSuffix string, table Table) { + for _, index := range table.Indices { + migrationId := fmt.Sprintf("create index %s - %s", index.XName(table.Name), versionSuffix) + mg.AddMigration(migrationId, NewAddIndexMigration(table, index)) + } +} + +func addTableRenameMigration(mg *Migrator, oldName string, newName string, versionSuffix string) { + migrationId := fmt.Sprintf("Rename table %s to %s - %s", oldName, newName, versionSuffix) + mg.AddMigration(migrationId, NewRenameTableMigration(oldName, newName)) +} diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go new file mode 100644 index 00000000000..d1579a8da2a --- /dev/null +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -0,0 +1,89 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addDashboardMigration(mg *Migrator) { + var dashboardV1 = Table{ + Name: "dashboard", + Columns: []*Column{ + &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + &Column{Name: "version", Type: DB_Int, Nullable: false}, + &Column{Name: "slug", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "title", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "data", Type: DB_Text, Nullable: false}, + &Column{Name: "account_id", Type: DB_BigInt, Nullable: false}, + &Column{Name: "created", Type: DB_DateTime, Nullable: false}, + &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + &Index{Cols: []string{"account_id"}}, + &Index{Cols: []string{"account_id", "slug"}, Type: UniqueIndex}, + }, + } + + mg.AddMigration("create dashboard table", NewAddTableMigration(dashboardV1)) + + //------- indexes ------------------ + mg.AddMigration("add index dashboard.account_id", NewAddIndexMigration(dashboardV1, dashboardV1.Indices[0])) + mg.AddMigration("add unique index dashboard_account_id_slug", NewAddIndexMigration(dashboardV1, dashboardV1.Indices[1])) + + dashboardTagV1 := Table{ + Name: "dashboard_tag", + Columns: []*Column{ + &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + &Column{Name: "dashboard_id", Type: DB_BigInt, Nullable: false}, + &Column{Name: "term", Type: DB_NVarchar, Length: 50, Nullable: false}, + }, + Indices: []*Index{ + &Index{Cols: []string{"dashboard_id", "term"}, Type: UniqueIndex}, + }, + } + + mg.AddMigration("create dashboard_tag table", NewAddTableMigration(dashboardTagV1)) + mg.AddMigration("add unique index dashboard_tag.dasboard_id_term", NewAddIndexMigration(dashboardTagV1, dashboardTagV1.Indices[0])) + + // --------------------- + // account -> org changes + + //------- drop dashboard indexes ------------------ + addDropAllIndicesMigrations(mg, "v1", dashboardTagV1) + //------- rename table ------------------ + addTableRenameMigration(mg, "dashboard", "dashboard_v1", "v1") + + // dashboard v2 + var dashboardV2 = Table{ + Name: "dashboard", + Columns: []*Column{ + &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + &Column{Name: "version", Type: DB_Int, Nullable: false}, + &Column{Name: "slug", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "title", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "data", Type: DB_Text, Nullable: false}, + &Column{Name: "org_id", Type: DB_BigInt, Nullable: false}, + &Column{Name: "created", Type: DB_DateTime, Nullable: false}, + &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + &Index{Cols: []string{"org_id"}}, + &Index{Cols: []string{"org_id", "slug"}, Type: UniqueIndex}, + }, + } + + // recreate table + mg.AddMigration("create dashboard v2", NewAddTableMigration(dashboardV2)) + // recreate indices + addTableIndicesMigrations(mg, "v2", dashboardV2) + // copy data + mg.AddMigration("copy dashboard v1 to v2", NewCopyTableDataMigration("dashboard", "dashboard_v1", map[string]string{ + "id": "id", + "version": "version", + "slug": "slug", + "title": "title", + "data": "data", + "org_id": "account_id", + "created": "created", + "updated": "updated", + })) + + mg.AddMigration("drop table dashboard_v1", NewDropTableMigration("dashboard_v1")) +} diff --git a/pkg/services/sqlstore/migrations/datasource_mig.go b/pkg/services/sqlstore/migrations/datasource_mig.go new file mode 100644 index 00000000000..64ab496298d --- /dev/null +++ b/pkg/services/sqlstore/migrations/datasource_mig.go @@ -0,0 +1,98 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addDataSourceMigration(mg *Migrator) { + var tableV1 = Table{ + Name: "data_source", + Columns: []*Column{ + &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + &Column{Name: "account_id", Type: DB_BigInt, Nullable: false}, + &Column{Name: "version", Type: DB_Int, Nullable: false}, + &Column{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "access", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "url", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "user", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "database", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "basic_auth", Type: DB_Bool, Nullable: false}, + &Column{Name: "basic_auth_user", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "basic_auth_password", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "is_default", Type: DB_Bool, Nullable: false}, + &Column{Name: "created", Type: DB_DateTime, Nullable: false}, + &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + &Index{Cols: []string{"account_id"}}, + &Index{Cols: []string{"account_id", "name"}, Type: UniqueIndex}, + }, + } + + mg.AddMigration("create data_source table", NewAddTableMigration(tableV1)) + mg.AddMigration("add index data_source.account_id", NewAddIndexMigration(tableV1, tableV1.Indices[0])) + mg.AddMigration("add unique index data_source.account_id_name", NewAddIndexMigration(tableV1, tableV1.Indices[1])) + + // --------------------- + // account -> org changes + + // drop v1 indices + addDropAllIndicesMigrations(mg, "v1", tableV1) + // rename table + addTableRenameMigration(mg, "data_source", "data_source_v1", "v1") + + // new table + var tableV2 = Table{ + Name: "data_source", + Columns: []*Column{ + &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + &Column{Name: "org_id", Type: DB_BigInt, Nullable: false}, + &Column{Name: "version", Type: DB_Int, Nullable: false}, + &Column{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "access", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "url", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "user", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "database", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "basic_auth", Type: DB_Bool, Nullable: false}, + &Column{Name: "basic_auth_user", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "basic_auth_password", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "is_default", Type: DB_Bool, Nullable: false}, + &Column{Name: "json_data", Type: DB_Text, Nullable: true}, + &Column{Name: "created", Type: DB_DateTime, Nullable: false}, + &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + &Index{Cols: []string{"org_id"}}, + &Index{Cols: []string{"org_id", "name"}, Type: UniqueIndex}, + }, + } + + // create v2 table + mg.AddMigration("create data_source table v2", NewAddTableMigration(tableV2)) + + // add v2 indíces + addTableIndicesMigrations(mg, "v2", tableV2) + + //------- copy data from v1 to v2 ------------------- + mg.AddMigration("copy data_source v1 to v2", NewCopyTableDataMigration("data_source", "data_source_v1", map[string]string{ + "id": "id", + "org_id": "account_id", + "version": "version", + "type": "type", + "name": "name", + "access": "access", + "url": "password", + "user": "user", + "database": "database", + "basic_auth": "basic_auth", + "basic_auth_user": "basic_auth_user", + "basic_auth_password": "basic_auth_password", + "is_default": "is_default", + "created": "created", + "updated": "updated", + })) + + mg.AddMigration("Drop old table data_source_v1", NewDropTableMigration("data_source_old")) +} diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go new file mode 100644 index 00000000000..963088ca0d9 --- /dev/null +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -0,0 +1,51 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +// --- Migration Guide line --- +// 1. Never change a migration that is committed and pushed to master +// 2. Always add new migrations (to change or undo previous migrations) +// 3. Some migraitons are not yet written (rename column, table, drop table, index etc) + +func AddMigrations(mg *Migrator) { + addMigrationLogMigrations(mg) + addUserMigrations(mg) + addStarMigrations(mg) + addOrgMigrations(mg) + addDashboardMigration(mg) + addDataSourceMigration(mg) + addApiKeyMigrations(mg) +} + +func addMigrationLogMigrations(mg *Migrator) { + migrationLogV1 := Table{ + Name: "migration_log", + Columns: []*Column{ + &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + &Column{Name: "migration_id", Type: DB_NVarchar, Length: 255}, + &Column{Name: "sql", Type: DB_Text}, + &Column{Name: "success", Type: DB_Bool}, + &Column{Name: "error", Type: DB_Text}, + &Column{Name: "timestamp", Type: DB_DateTime}, + }, + } + + mg.AddMigration("create migration_log table", NewAddTableMigration(migrationLogV1)) +} + +func addStarMigrations(mg *Migrator) { + starV1 := Table{ + Name: "star", + Columns: []*Column{ + &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + &Column{Name: "user_id", Type: DB_BigInt, Nullable: false}, + &Column{Name: "dashboard_id", Type: DB_BigInt, Nullable: false}, + }, + Indices: []*Index{ + &Index{Cols: []string{"user_id", "dashboard_id"}, Type: UniqueIndex}, + }, + } + + mg.AddMigration("create star table", NewAddTableMigration(starV1)) + mg.AddMigration("add unique index star.user_id_dashboard_id", NewAddIndexMigration(starV1, starV1.Indices[0])) +} diff --git a/pkg/services/sqlstore/migrations_test.go b/pkg/services/sqlstore/migrations/migrations_test.go similarity index 97% rename from pkg/services/sqlstore/migrations_test.go rename to pkg/services/sqlstore/migrations/migrations_test.go index 6e1f55d8f6f..d4ba97450f7 100644 --- a/pkg/services/sqlstore/migrations_test.go +++ b/pkg/services/sqlstore/migrations/migrations_test.go @@ -1,4 +1,4 @@ -package sqlstore +package migrations import ( "fmt" @@ -32,7 +32,7 @@ func TestMigrations(t *testing.T) { mg := NewMigrator(x) mg.LogLevel = log.DEBUG - addMigrations(mg) + AddMigrations(mg) err = mg.Start() So(err, ShouldBeNil) diff --git a/pkg/services/sqlstore/migrations/org_mig.go b/pkg/services/sqlstore/migrations/org_mig.go new file mode 100644 index 00000000000..4935955c4f5 --- /dev/null +++ b/pkg/services/sqlstore/migrations/org_mig.go @@ -0,0 +1,71 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addOrgMigrations(mg *Migrator) { + orgV1 := Table{ + Name: "org", + Columns: []*Column{ + &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + &Column{Name: "version", Type: DB_Int, Nullable: false}, + &Column{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "address1", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "address2", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "city", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "state", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "zip_code", Type: DB_NVarchar, Length: 50, Nullable: true}, + &Column{Name: "country", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "billing_email", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "created", Type: DB_DateTime, Nullable: false}, + &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + &Index{Cols: []string{"name"}, Type: UniqueIndex}, + }, + } + + // add org v1 + mg.AddMigration("create org table v1", NewAddTableMigration(orgV1)) + addTableIndicesMigrations(mg, "v1", orgV1) + + orgUserV1 := Table{ + Name: "org_user", + Columns: []*Column{ + &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + &Column{Name: "org_id", Type: DB_BigInt}, + &Column{Name: "user_id", Type: DB_BigInt}, + &Column{Name: "role", Type: DB_NVarchar, Length: 20}, + &Column{Name: "created", Type: DB_DateTime}, + &Column{Name: "updated", Type: DB_DateTime}, + }, + Indices: []*Index{ + &Index{Cols: []string{"org_id"}}, + &Index{Cols: []string{"org_id", "user_id"}, Type: UniqueIndex}, + }, + } + + //------- org_user table ------------------- + mg.AddMigration("create org_user table v1", NewAddTableMigration(orgUserV1)) + addTableIndicesMigrations(mg, "v1", orgUserV1) + + //------- copy data from old table------------------- + mg.AddMigration("copy data account to org", NewCopyTableDataMigration("org", "account", map[string]string{ + "id": "id", + "version": "version", + "name": "name", + "created": "created", + "updated": "updated", + }).IfTableExists("account")) + + mg.AddMigration("copy data account_user to org_user", NewCopyTableDataMigration("org_user", "account_user", map[string]string{ + "id": "id", + "org_id": "account_id", + "user_id": "user_id", + "role": "role", + "created": "created", + "updated": "updated", + }).IfTableExists("account_user")) + + mg.AddMigration("Drop old table account", NewDropTableMigration("account")) + mg.AddMigration("Drop old table account_user", NewDropTableMigration("account_user")) +} diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go new file mode 100644 index 00000000000..d2672917e20 --- /dev/null +++ b/pkg/services/sqlstore/migrations/user_mig.go @@ -0,0 +1,91 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addUserMigrations(mg *Migrator) { + userV1 := Table{ + Name: "user", + Columns: []*Column{ + &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + &Column{Name: "version", Type: DB_Int, Nullable: false}, + &Column{Name: "login", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "email", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "salt", Type: DB_NVarchar, Length: 50, Nullable: true}, + &Column{Name: "rands", Type: DB_NVarchar, Length: 50, Nullable: true}, + &Column{Name: "company", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "account_id", Type: DB_BigInt, Nullable: false}, + &Column{Name: "is_admin", Type: DB_Bool, Nullable: false}, + &Column{Name: "created", Type: DB_DateTime, Nullable: false}, + &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + &Index{Cols: []string{"login"}, Type: UniqueIndex}, + &Index{Cols: []string{"email"}, Type: UniqueIndex}, + }, + } + + // create table + mg.AddMigration("create user table", NewAddTableMigration(userV1)) + // add indices + mg.AddMigration("add unique index user.login", NewAddIndexMigration(userV1, userV1.Indices[0])) + mg.AddMigration("add unique index user.email", NewAddIndexMigration(userV1, userV1.Indices[1])) + + // --------------------- + // account -> org changes + + //------- drop indexes ------------------ + addDropAllIndicesMigrations(mg, "v1", userV1) + + //------- rename table ------------------ + addTableRenameMigration(mg, "user", "user_v1", "v1") + + //------- recreate table with new column names ------------------ + userV2 := Table{ + Name: "user", + Columns: []*Column{ + &Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + &Column{Name: "version", Type: DB_Int, Nullable: false}, + &Column{Name: "login", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "email", Type: DB_NVarchar, Length: 255, Nullable: false}, + &Column{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "salt", Type: DB_NVarchar, Length: 50, Nullable: true}, + &Column{Name: "rands", Type: DB_NVarchar, Length: 50, Nullable: true}, + &Column{Name: "company", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "org_id", Type: DB_BigInt, Nullable: false}, + &Column{Name: "is_admin", Type: DB_Bool, Nullable: false}, + &Column{Name: "email_verified", Type: DB_Bool, Nullable: true}, + &Column{Name: "theme", Type: DB_NVarchar, Length: 255, Nullable: true}, + &Column{Name: "created", Type: DB_DateTime, Nullable: false}, + &Column{Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + &Index{Cols: []string{"login"}, Type: UniqueIndex}, + &Index{Cols: []string{"email"}, Type: UniqueIndex}, + }, + } + + mg.AddMigration("create user table v2", NewAddTableMigration(userV2)) + addTableIndicesMigrations(mg, "v2", userV2) + + //------- copy data from v1 to v2 ------------------- + mg.AddMigration("copy data_source v1 to v2", NewCopyTableDataMigration("user", "user_v1", map[string]string{ + "id": "id", + "version": "version", + "login": "login", + "email": "email", + "name": "name", + "password": "password", + "salt": "salt", + "rands": "rands", + "company": "company", + "org_id": "account_id", + "is_admin": "is_admin", + "created": "created", + "updated": "updated", + })) + + mg.AddMigration("Drop old table user_v1", NewDropTableMigration("user_v1")) +} diff --git a/pkg/services/sqlstore/migrator/builder.go b/pkg/services/sqlstore/migrator/builder.go deleted file mode 100644 index 3afa4e2f1a5..00000000000 --- a/pkg/services/sqlstore/migrator/builder.go +++ /dev/null @@ -1,131 +0,0 @@ -package migrator - -import ( - "fmt" - "strings" -) - -type MigrationBase struct { - id string -} - -func (m *MigrationBase) Id() string { - return m.id -} - -func (m *MigrationBase) SetId(id string) { - m.id = id -} - -type RawSqlMigration struct { - MigrationBase - - sqlite string - mysql string -} - -func (m *RawSqlMigration) Sql(dialect Dialect) string { - switch dialect.DriverName() { - case MYSQL: - return m.mysql - case SQLITE: - return m.sqlite - } - - panic("db type not supported") -} - -func (m *RawSqlMigration) Sqlite(sql string) *RawSqlMigration { - m.sqlite = sql - return m -} - -func (m *RawSqlMigration) Mysql(sql string) *RawSqlMigration { - m.mysql = sql - return m -} - -type AddColumnMigration struct { - MigrationBase - tableName string - column *Column -} - -func (m *AddColumnMigration) Table(tableName string) *AddColumnMigration { - m.tableName = tableName - return m -} - -func (m *AddColumnMigration) Column(col *Column) *AddColumnMigration { - m.column = col - return m -} - -func (m *AddColumnMigration) Sql(dialect Dialect) string { - return dialect.AddColumnSql(m.tableName, m.column) -} - -type AddIndexMigration struct { - MigrationBase - tableName string - index Index -} - -func (m *AddIndexMigration) Name(name string) *AddIndexMigration { - m.index.Name = name - return m -} - -func (m *AddIndexMigration) Table(tableName string) *AddIndexMigration { - m.tableName = tableName - return m -} - -func (m *AddIndexMigration) Unique() *AddIndexMigration { - m.index.Type = UniqueIndex - return m -} - -func (m *AddIndexMigration) Columns(columns ...string) *AddIndexMigration { - m.index.Cols = columns - return m -} - -func (m *AddIndexMigration) Sql(dialect Dialect) string { - if m.index.Name == "" { - m.index.Name = fmt.Sprintf("%s", strings.Join(m.index.Cols, "_")) - } - return dialect.CreateIndexSql(m.tableName, &m.index) -} - -type AddTableMigration struct { - MigrationBase - table Table -} - -func (m *AddTableMigration) Sql(d Dialect) string { - return d.CreateTableSql(&m.table) -} - -func (m *AddTableMigration) Name(name string) *AddTableMigration { - m.table.Name = name - return m -} - -func (m *AddTableMigration) WithColumns(columns ...*Column) *AddTableMigration { - for _, col := range columns { - m.table.Columns = append(m.table.Columns, col) - if col.IsPrimaryKey { - m.table.PrimaryKeys = append(m.table.PrimaryKeys, col.Name) - } - } - return m -} - -func (m *AddTableMigration) WithColumn(col *Column) *AddTableMigration { - m.table.Columns = append(m.table.Columns, col) - if col.IsPrimaryKey { - m.table.PrimaryKeys = append(m.table.PrimaryKeys, col.Name) - } - return m -} diff --git a/pkg/services/sqlstore/migrator/conditions.go b/pkg/services/sqlstore/migrator/conditions.go new file mode 100644 index 00000000000..79bf7c7ed27 --- /dev/null +++ b/pkg/services/sqlstore/migrator/conditions.go @@ -0,0 +1,13 @@ +package migrator + +type MigrationCondition interface { + Sql(dialect Dialect) (string, []interface{}) +} + +type IfTableExistsCondition struct { + TableName string +} + +func (c *IfTableExistsCondition) Sql(dialect Dialect) (string, []interface{}) { + return dialect.TableCheckSql(c.TableName) +} diff --git a/pkg/services/sqlstore/migrator/dialect.go b/pkg/services/sqlstore/migrator/dialect.go index 803e8eafccc..9e3c3a11e16 100644 --- a/pkg/services/sqlstore/migrator/dialect.go +++ b/pkg/services/sqlstore/migrator/dialect.go @@ -20,8 +20,12 @@ type Dialect interface { CreateIndexSql(tableName string, index *Index) string CreateTableSql(table *Table) string AddColumnSql(tableName string, Col *Column) string + CopyTableData(sourceTable string, targetTable string, sourceCols []string, targetCols []string) string + DropTable(tableName string) string + DropIndexSql(tableName string, index *Index) string TableCheckSql(tableName string) (string, []interface{}) + RenameTable(oldName string, newName string) string } func NewDialect(name string) Dialect { @@ -101,15 +105,47 @@ func (db *BaseDialect) AddColumnSql(tableName string, col *Column) string { func (db *BaseDialect) CreateIndexSql(tableName string, index *Index) string { quote := db.dialect.Quote var unique string - var idxName string if index.Type == UniqueIndex { unique = " UNIQUE" - idxName = fmt.Sprintf("UQE_%v_%v", tableName, index.Name) - } else { - idxName = fmt.Sprintf("IDX_%v_%v", tableName, index.Name) } + idxName := index.XName(tableName) + return fmt.Sprintf("CREATE%s INDEX %v ON %v (%v);", unique, quote(idxName), quote(tableName), quote(strings.Join(index.Cols, quote(",")))) } + +func (db *BaseDialect) QuoteColList(cols []string) string { + var sourceColsSql = "" + for _, col := range cols { + sourceColsSql += db.dialect.Quote(col) + sourceColsSql += "\n, " + } + return strings.TrimSuffix(sourceColsSql, "\n, ") +} + +func (db *BaseDialect) CopyTableData(sourceTable string, targetTable string, sourceCols []string, targetCols []string) string { + sourceColsSql := db.QuoteColList(sourceCols) + targetColsSql := db.QuoteColList(targetCols) + + quote := db.dialect.Quote + return fmt.Sprintf("INSERT INTO %s (%s) SELECT %s FROM %s", quote(targetTable), targetColsSql, sourceColsSql, quote(sourceTable)) +} + +func (db *BaseDialect) DropTable(tableName string) string { + quote := db.dialect.Quote + return fmt.Sprintf("DROP TABLE IF EXISTS %s", quote(tableName)) +} + +func (db *BaseDialect) RenameTable(oldName string, newName string) string { + quote := db.dialect.Quote + return fmt.Sprintf("ALTER TABLE %s RENAME TO %s", quote(oldName), quote(newName)) +} + +func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string { + quote := db.dialect.Quote + var name string + name = index.XName(tableName) + return fmt.Sprintf("DROP INDEX %v ON %s", quote(name), quote(tableName)) +} diff --git a/pkg/services/sqlstore/migrator/migrations.go b/pkg/services/sqlstore/migrator/migrations.go new file mode 100644 index 00000000000..e596ef6c171 --- /dev/null +++ b/pkg/services/sqlstore/migrator/migrations.go @@ -0,0 +1,190 @@ +package migrator + +import ( + "fmt" + "strings" +) + +type MigrationBase struct { + id string + Condition MigrationCondition +} + +func (m *MigrationBase) Id() string { + return m.id +} + +func (m *MigrationBase) SetId(id string) { + m.id = id +} + +func (m *MigrationBase) GetCondition() MigrationCondition { + return m.Condition +} + +type RawSqlMigration struct { + MigrationBase + + sqlite string + mysql string +} + +func (m *RawSqlMigration) Sql(dialect Dialect) string { + switch dialect.DriverName() { + case MYSQL: + return m.mysql + case SQLITE: + return m.sqlite + } + + panic("db type not supported") +} + +func (m *RawSqlMigration) Sqlite(sql string) *RawSqlMigration { + m.sqlite = sql + return m +} + +func (m *RawSqlMigration) Mysql(sql string) *RawSqlMigration { + m.mysql = sql + return m +} + +type AddColumnMigration struct { + MigrationBase + tableName string + column *Column +} + +func (m *AddColumnMigration) Table(tableName string) *AddColumnMigration { + m.tableName = tableName + return m +} + +func (m *AddColumnMigration) Column(col *Column) *AddColumnMigration { + m.column = col + return m +} + +func (m *AddColumnMigration) Sql(dialect Dialect) string { + return dialect.AddColumnSql(m.tableName, m.column) +} + +type AddIndexMigration struct { + MigrationBase + tableName string + index *Index +} + +func NewAddIndexMigration(table Table, index *Index) *AddIndexMigration { + return &AddIndexMigration{tableName: table.Name, index: index} +} + +func (m *AddIndexMigration) Table(tableName string) *AddIndexMigration { + m.tableName = tableName + return m +} + +func (m *AddIndexMigration) Sql(dialect Dialect) string { + return dialect.CreateIndexSql(m.tableName, m.index) +} + +type DropIndexMigration struct { + MigrationBase + tableName string + index *Index +} + +func NewDropIndexMigration(table Table, index *Index) *DropIndexMigration { + return &DropIndexMigration{tableName: table.Name, index: index} +} + +func (m *DropIndexMigration) Sql(dialect Dialect) string { + if m.index.Name == "" { + m.index.Name = fmt.Sprintf("%s", strings.Join(m.index.Cols, "_")) + } + return dialect.DropIndexSql(m.tableName, m.index) +} + +type AddTableMigration struct { + MigrationBase + table Table +} + +func NewAddTableMigration(table Table) *AddTableMigration { + for _, col := range table.Columns { + if col.IsPrimaryKey { + table.PrimaryKeys = append(table.PrimaryKeys, col.Name) + } + } + return &AddTableMigration{table: table} +} + +func (m *AddTableMigration) Sql(d Dialect) string { + return d.CreateTableSql(&m.table) +} + +type DropTableMigration struct { + MigrationBase + tableName string +} + +func NewDropTableMigration(tableName string) *DropTableMigration { + return &DropTableMigration{tableName: tableName} +} + +func (m *DropTableMigration) Sql(d Dialect) string { + return d.DropTable(m.tableName) +} + +type RenameTableMigration struct { + MigrationBase + oldName string + newName string +} + +func NewRenameTableMigration(oldName string, newName string) *RenameTableMigration { + return &RenameTableMigration{oldName: oldName, newName: newName} +} + +func (m *RenameTableMigration) IfTableExists(tableName string) *RenameTableMigration { + m.Condition = &IfTableExistsCondition{TableName: tableName} + return m +} + +func (m *RenameTableMigration) Rename(oldName string, newName string) *RenameTableMigration { + m.oldName = oldName + m.newName = newName + return m +} + +func (m *RenameTableMigration) Sql(d Dialect) string { + return d.RenameTable(m.oldName, m.newName) +} + +type CopyTableDataMigration struct { + MigrationBase + sourceTable string + targetTable string + sourceCols []string + targetCols []string + colMap map[string]string +} + +func NewCopyTableDataMigration(targetTable string, sourceTable string, colMap map[string]string) *CopyTableDataMigration { + m := &CopyTableDataMigration{sourceTable: sourceTable, targetTable: targetTable} + for key, value := range colMap { + m.targetCols = append(m.targetCols, key) + m.sourceCols = append(m.sourceCols, value) + } + return m +} + +func (m *CopyTableDataMigration) IfTableExists(tableName string) *CopyTableDataMigration { + m.Condition = &IfTableExistsCondition{TableName: tableName} + return m +} + +func (m *CopyTableDataMigration) Sql(d Dialect) string { + return d.CopyTableData(m.sourceTable, m.targetTable, m.sourceCols, m.targetCols) +} diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index 53e61f3d1c9..2461d9dab0c 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -5,9 +5,9 @@ import ( _ "github.com/go-sql-driver/mysql" "github.com/go-xorm/xorm" + "github.com/grafana/grafana/pkg/log" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" - "github.com/grafana/grafana/pkg/log" ) type Migrator struct { @@ -70,7 +70,7 @@ func (mg *Migrator) GetMigrationLog() (map[string]MigrationLog, error) { func (mg *Migrator) Start() error { if mg.LogLevel <= log.INFO { - log.Info("Migrator:: Starting DB migration") + log.Info("Migrator: Starting DB migration") } logMap, err := mg.GetMigrationLog() @@ -82,7 +82,7 @@ func (mg *Migrator) Start() error { _, exists := logMap[m.Id()] if exists { if mg.LogLevel <= log.DEBUG { - log.Debug("Migrator:: Skipping migration: %v, Already executed", m.Id()) + log.Debug("Migrator: Skipping migration: %v, Already executed", m.Id()) } continue } @@ -114,13 +114,24 @@ func (mg *Migrator) Start() error { func (mg *Migrator) exec(m Migration) error { if mg.LogLevel <= log.INFO { - log.Info("Migrator::exec migration id: %v", m.Id()) + log.Info("Migrator: exec migration id: %v", m.Id()) } err := mg.inTransaction(func(sess *xorm.Session) error { + + condition := m.GetCondition() + if condition != nil { + sql, args := condition.Sql(mg.dialect) + results, err := sess.Query(sql, args...) + if err != nil || len(results) == 0 { + log.Info("Migrator: skipping migration id: %v, condition not fulfilled", m.Id()) + return sess.Rollback() + } + } + _, err := sess.Exec(m.Sql(mg.dialect)) if err != nil { - log.Error(3, "Migrator::exec FAILED migration id: %v, err: %v", m.Id(), err) + log.Error(3, "Migrator: exec FAILED migration id: %v, err: %v", m.Id(), err) return err } return nil diff --git a/pkg/services/sqlstore/migrator/postgres_dialect.go b/pkg/services/sqlstore/migrator/postgres_dialect.go index f4697d15664..64c6772c2d1 100644 --- a/pkg/services/sqlstore/migrator/postgres_dialect.go +++ b/pkg/services/sqlstore/migrator/postgres_dialect.go @@ -1,6 +1,9 @@ package migrator -import "strconv" +import ( + "fmt" + "strconv" +) type Postgres struct { BaseDialect @@ -84,3 +87,9 @@ func (db *Postgres) TableCheckSql(tableName string) (string, []interface{}) { sql := "SELECT `TABLE_NAME` from `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? and `TABLE_NAME`=?" return sql, args } + +func (db *Postgres) DropIndexSql(tableName string, index *Index) string { + quote := db.Quote + idxName := index.XName(tableName) + return fmt.Sprintf("DROP INDEX %v", quote(idxName)) +} diff --git a/pkg/services/sqlstore/migrator/sqlite_dialect.go b/pkg/services/sqlstore/migrator/sqlite_dialect.go index 4a4d3f475b1..8555754ab92 100644 --- a/pkg/services/sqlstore/migrator/sqlite_dialect.go +++ b/pkg/services/sqlstore/migrator/sqlite_dialect.go @@ -1,5 +1,7 @@ package migrator +import "fmt" + type Sqlite3 struct { BaseDialect } @@ -57,3 +59,10 @@ func (db *Sqlite3) TableCheckSql(tableName string) (string, []interface{}) { args := []interface{}{tableName} return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args } + +func (db *Sqlite3) DropIndexSql(tableName string, index *Index) string { + quote := db.Quote + //var unique string + idxName := index.XName(tableName) + return fmt.Sprintf("DROP INDEX %v", quote(idxName)) +} diff --git a/pkg/services/sqlstore/migrator/types.go b/pkg/services/sqlstore/migrator/types.go index 64f1aa6fa8c..d42eba0f58a 100644 --- a/pkg/services/sqlstore/migrator/types.go +++ b/pkg/services/sqlstore/migrator/types.go @@ -1,5 +1,10 @@ package migrator +import ( + "fmt" + "strings" +) + const ( POSTGRES = "postgres" SQLITE = "sqlite3" @@ -10,6 +15,7 @@ type Migration interface { Sql(dialect Dialect) string Id() string SetId(string) + GetCondition() MigrationCondition } type SQLType string @@ -24,6 +30,7 @@ type Table struct { Name string Columns []*Column PrimaryKeys []string + Indices []*Index } const ( @@ -37,6 +44,21 @@ type Index struct { Cols []string } +func (index *Index) XName(tableName string) string { + if index.Name == "" { + index.Name = fmt.Sprintf("%s", strings.Join(index.Cols, "_")) + } + + if !strings.HasPrefix(index.Name, "UQE_") && + !strings.HasPrefix(index.Name, "IDX_") { + if index.Type == UniqueIndex { + return fmt.Sprintf("UQE_%v_%v", tableName, index.Name) + } + return fmt.Sprintf("IDX_%v_%v", tableName, index.Name) + } + return index.Name +} + var ( DB_Bit = "BIT" DB_TinyInt = "TINYINT" diff --git a/pkg/services/sqlstore/org.go b/pkg/services/sqlstore/org.go new file mode 100644 index 00000000000..18284feccac --- /dev/null +++ b/pkg/services/sqlstore/org.go @@ -0,0 +1,134 @@ +package sqlstore + +import ( + "time" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/events" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" +) + +func init() { + bus.AddHandler("sql", GetOrgById) + bus.AddHandler("sql", CreateOrg) + bus.AddHandler("sql", UpdateOrg) + bus.AddHandler("sql", GetOrgByName) + bus.AddHandler("sql", GetOrgList) + bus.AddHandler("sql", DeleteOrg) +} + +func GetOrgList(query *m.GetOrgListQuery) error { + return x.Find(&query.Result) +} + +func GetOrgById(query *m.GetOrgByIdQuery) error { + var org m.Org + exists, err := x.Id(query.Id).Get(&org) + if err != nil { + return err + } + + if !exists { + return m.ErrOrgNotFound + } + + query.Result = &org + return nil +} + +func GetOrgByName(query *m.GetOrgByNameQuery) error { + var org m.Org + exists, err := x.Where("name=?", query.Name).Get(&org) + if err != nil { + return err + } + + if !exists { + return m.ErrOrgNotFound + } + + query.Result = &org + return nil +} + +func CreateOrg(cmd *m.CreateOrgCommand) error { + return inTransaction2(func(sess *session) error { + + org := m.Org{ + Name: cmd.Name, + Created: time.Now(), + Updated: time.Now(), + } + + if _, err := sess.Insert(&org); err != nil { + return err + } + + user := m.OrgUser{ + OrgId: org.Id, + UserId: cmd.UserId, + Role: m.ROLE_ADMIN, + Created: time.Now(), + Updated: time.Now(), + } + + _, err := sess.Insert(&user) + cmd.Result = org + + sess.publishAfterCommit(&events.OrgCreated{ + Timestamp: org.Created, + Id: org.Id, + Name: org.Name, + }) + + return err + }) +} + +func UpdateOrg(cmd *m.UpdateOrgCommand) error { + return inTransaction2(func(sess *session) error { + + org := m.Org{ + Name: cmd.Name, + Updated: time.Now(), + } + + if _, err := sess.Id(cmd.OrgId).Update(&org); err != nil { + return err + } + + sess.publishAfterCommit(&events.OrgUpdated{ + Timestamp: org.Updated, + Id: org.Id, + Name: org.Name, + }) + + return nil + }) +} + +func DeleteOrg(cmd *m.DeleteOrgCommand) error { + return inTransaction2(func(sess *session) error { + + deletes := []string{ + "DELETE FROM star WHERE EXISTS (SELECT 1 FROM dashboard WHERE org_id = ?)", + "DELETE FROM dashboard_tag WHERE EXISTS (SELECT 1 FROM dashboard WHERE org_id = ?)", + "DELETE FROM dashboard WHERE org_id = ?", + "DELETE FROM api_key WHERE org_id = ?", + "DELETE FROM data_source WHERE org_id = ?", + "DELETE FROM org_user WHERE org_id = ?", + "DELETE FROM org WHERE id = ?", + } + + for _, sql := range deletes { + log.Trace(sql) + _, err := sess.Exec(sql, cmd.Id) + if err != nil { + return err + } + } + + return nil + }) +} diff --git a/pkg/services/sqlstore/account_test.go b/pkg/services/sqlstore/org_test.go similarity index 60% rename from pkg/services/sqlstore/account_test.go rename to pkg/services/sqlstore/org_test.go index f0c9f5cbd9a..f839d554c34 100644 --- a/pkg/services/sqlstore/account_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -14,12 +14,12 @@ func TestAccountDataAccess(t *testing.T) { Convey("Testing Account DB Access", t, func() { InitTestDB(t) - Convey("Given single account mode", func() { - setting.SingleAccountMode = true - setting.DefaultAccountName = "test" - setting.DefaultAccountRole = "Viewer" + Convey("Given single org mode", func() { + setting.SingleOrgMode = true + setting.DefaultOrgName = "test" + setting.DefaultOrgRole = "Viewer" - Convey("Users should be added to default account", func() { + Convey("Users should be added to default organization", func() { ac1cmd := m.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"} ac2cmd := m.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name"} @@ -28,20 +28,20 @@ func TestAccountDataAccess(t *testing.T) { err = CreateUser(&ac2cmd) So(err, ShouldBeNil) - q1 := m.GetUserAccountsQuery{UserId: ac1cmd.Result.Id} - q2 := m.GetUserAccountsQuery{UserId: ac2cmd.Result.Id} - GetUserAccounts(&q1) - GetUserAccounts(&q2) + q1 := m.GetUserOrgListQuery{UserId: ac1cmd.Result.Id} + q2 := m.GetUserOrgListQuery{UserId: ac2cmd.Result.Id} + GetUserOrgList(&q1) + GetUserOrgList(&q2) - So(q1.Result[0].AccountId, ShouldEqual, q2.Result[0].AccountId) + So(q1.Result[0].OrgId, ShouldEqual, q2.Result[0].OrgId) So(q1.Result[0].Role, ShouldEqual, "Viewer") }) }) Convey("Given two saved users", func() { - setting.SingleAccountMode = false + setting.SingleOrgMode = false + setting.DefaultOrgName = "test" - setting.DefaultAccountName = "test" ac1cmd := m.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"} ac2cmd := m.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name", IsAdmin: true} @@ -70,14 +70,14 @@ func TestAccountDataAccess(t *testing.T) { So(query.Result[1].Email, ShouldEqual, "ac2@test.com") }) - Convey("Given an added account user", func() { - cmd := m.AddAccountUserCommand{ - AccountId: ac1.AccountId, - UserId: ac2.Id, - Role: m.ROLE_VIEWER, + Convey("Given an added org user", func() { + cmd := m.AddOrgUserCommand{ + OrgId: ac1.OrgId, + UserId: ac2.Id, + Role: m.ROLE_VIEWER, } - err := AddAccountUser(&cmd) + err := AddOrgUser(&cmd) Convey("Should have been saved without error", func() { So(err, ShouldBeNil) }) @@ -88,54 +88,54 @@ func TestAccountDataAccess(t *testing.T) { So(err, ShouldBeNil) So(query.Result.Email, ShouldEqual, "ac2@test.com") - So(query.Result.AccountId, ShouldEqual, ac2.AccountId) + So(query.Result.OrgId, ShouldEqual, ac2.OrgId) So(query.Result.Name, ShouldEqual, "ac2 name") So(query.Result.Login, ShouldEqual, "ac2") - So(query.Result.AccountRole, ShouldEqual, "Admin") - So(query.Result.AccountName, ShouldEqual, "ac2@test.com") + So(query.Result.OrgRole, ShouldEqual, "Admin") + So(query.Result.OrgName, ShouldEqual, "ac2@test.com") So(query.Result.IsGrafanaAdmin, ShouldBeTrue) }) - Convey("Can get user accounts", func() { - query := m.GetUserAccountsQuery{UserId: ac2.Id} - err := GetUserAccounts(&query) + Convey("Can get user organizations", func() { + query := m.GetUserOrgListQuery{UserId: ac2.Id} + err := GetUserOrgList(&query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 2) }) - Convey("Can get account users", func() { - query := m.GetAccountUsersQuery{AccountId: ac1.AccountId} - err := GetAccountUsers(&query) + Convey("Can get organization users", func() { + query := m.GetOrgUsersQuery{OrgId: ac1.OrgId} + err := GetOrgUsers(&query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 2) So(query.Result[0].Role, ShouldEqual, "Admin") }) - Convey("Can set using account", func() { - cmd := m.SetUsingAccountCommand{UserId: ac2.Id, AccountId: ac1.Id} - err := SetUsingAccount(&cmd) + Convey("Can set using org", func() { + cmd := m.SetUsingOrgCommand{UserId: ac2.Id, OrgId: ac1.Id} + err := SetUsingOrg(&cmd) So(err, ShouldBeNil) - Convey("SignedInUserQuery with a different account", func() { + Convey("SignedInUserQuery with a different org", func() { query := m.GetSignedInUserQuery{UserId: ac2.Id} err := GetSignedInUser(&query) So(err, ShouldBeNil) - So(query.Result.AccountId, ShouldEqual, ac1.Id) + So(query.Result.OrgId, ShouldEqual, ac1.Id) So(query.Result.Email, ShouldEqual, "ac2@test.com") So(query.Result.Name, ShouldEqual, "ac2 name") So(query.Result.Login, ShouldEqual, "ac2") - So(query.Result.AccountName, ShouldEqual, "ac1@test.com") - So(query.Result.AccountRole, ShouldEqual, "Viewer") + So(query.Result.OrgName, ShouldEqual, "ac1@test.com") + So(query.Result.OrgRole, ShouldEqual, "Viewer") }) }) Convey("Cannot delete last admin account user", func() { - cmd := m.RemoveAccountUserCommand{AccountId: ac1.AccountId, UserId: ac1.Id} - err := RemoveAccountUser(&cmd) - So(err, ShouldEqual, m.ErrLastAccountAdmin) + cmd := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac1.Id} + err := RemoveOrgUser(&cmd) + So(err, ShouldEqual, m.ErrLastOrgAdmin) }) }) }) diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go new file mode 100644 index 00000000000..eaca01ce12c --- /dev/null +++ b/pkg/services/sqlstore/org_users.go @@ -0,0 +1,67 @@ +package sqlstore + +import ( + "fmt" + "time" + + "github.com/go-xorm/xorm" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +func init() { + bus.AddHandler("sql", AddOrgUser) + bus.AddHandler("sql", RemoveOrgUser) + bus.AddHandler("sql", GetOrgUsers) +} + +func AddOrgUser(cmd *m.AddOrgUserCommand) error { + return inTransaction(func(sess *xorm.Session) error { + + entity := m.OrgUser{ + OrgId: cmd.OrgId, + UserId: cmd.UserId, + Role: cmd.Role, + Created: time.Now(), + Updated: time.Now(), + } + + _, err := sess.Insert(&entity) + return err + }) +} + +func GetOrgUsers(query *m.GetOrgUsersQuery) error { + query.Result = make([]*m.OrgUserDTO, 0) + sess := x.Table("org_user") + sess.Join("INNER", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user"))) + sess.Where("org_user.org_id=?", query.OrgId) + sess.Cols("org_user.org_id", "org_user.user_id", "user.email", "user.login", "org_user.role") + sess.Asc("user.email", "user.login") + + err := sess.Find(&query.Result) + return err +} + +func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error { + return inTransaction(func(sess *xorm.Session) error { + var rawSql = "DELETE FROM org_user WHERE org_id=? and user_id=?" + _, err := sess.Exec(rawSql, cmd.OrgId, cmd.UserId) + if err != nil { + return err + } + + // validate that there is an admin user left + res, err := sess.Query("SELECT 1 from org_user WHERE org_id=? and role='Admin'", cmd.OrgId) + if err != nil { + return err + } + + if len(res) == 0 { + return m.ErrLastOrgAdmin + } + + return err + }) +} diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index e2bfc1bddd8..218f24f0142 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore/migrations" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/setting" @@ -73,7 +74,7 @@ func SetEngine(engine *xorm.Engine, enableLog bool) (err error) { migrator := migrator.NewMigrator(x) migrator.LogLevel = log.INFO - addMigrations(migrator) + migrations.AddMigrations(migrator) if err := migrator.Start(); err != nil { return fmt.Errorf("Sqlstore::Migration failed err: %v\n", err) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index f56a64c8c0e..5c3b69a2bb0 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -1,6 +1,7 @@ package sqlstore import ( + "fmt" "strings" "time" @@ -15,59 +16,64 @@ import ( func init() { bus.AddHandler("sql", CreateUser) + bus.AddHandler("sql", GetUserById) bus.AddHandler("sql", UpdateUser) + bus.AddHandler("sql", ChangeUserPassword) bus.AddHandler("sql", GetUserByLogin) - bus.AddHandler("sql", SetUsingAccount) + bus.AddHandler("sql", SetUsingOrg) bus.AddHandler("sql", GetUserInfo) bus.AddHandler("sql", GetSignedInUser) bus.AddHandler("sql", SearchUsers) - bus.AddHandler("sql", GetUserAccounts) + bus.AddHandler("sql", GetUserOrgList) + bus.AddHandler("sql", DeleteUser) + bus.AddHandler("sql", SetUsingOrg) + bus.AddHandler("sql", UpdateUserPermissions) } -func getAccountIdForNewUser(userEmail string, sess *session) (int64, error) { - var account m.Account +func getOrgIdForNewUser(userEmail string, sess *session) (int64, error) { + var org m.Org - if setting.SingleAccountMode { - has, err := sess.Where("name=?", setting.DefaultAccountName).Get(&account) + if setting.SingleOrgMode { + has, err := sess.Where("name=?", setting.DefaultOrgName).Get(&org) if err != nil { return 0, err } if has { - return account.Id, nil + return org.Id, nil } else { - account.Name = setting.DefaultAccountName + org.Name = setting.DefaultOrgName } } else { - account.Name = userEmail + org.Name = userEmail } - account.Created = time.Now() - account.Updated = time.Now() + org.Created = time.Now() + org.Updated = time.Now() - if _, err := sess.Insert(&account); err != nil { + if _, err := sess.Insert(&org); err != nil { return 0, err } - return account.Id, nil + return org.Id, nil } func CreateUser(cmd *m.CreateUserCommand) error { return inTransaction2(func(sess *session) error { - accountId, err := getAccountIdForNewUser(cmd.Email, sess) + orgId, err := getOrgIdForNewUser(cmd.Email, sess) if err != nil { return err } // create user user := m.User{ - Email: cmd.Email, - Name: cmd.Name, - Login: cmd.Login, - Company: cmd.Company, - IsAdmin: cmd.IsAdmin, - AccountId: accountId, - Created: time.Now(), - Updated: time.Now(), + Email: cmd.Email, + Name: cmd.Name, + Login: cmd.Login, + Company: cmd.Company, + IsAdmin: cmd.IsAdmin, + OrgId: orgId, + Created: time.Now(), + Updated: time.Now(), } if len(cmd.Password) > 0 { @@ -82,20 +88,20 @@ func CreateUser(cmd *m.CreateUserCommand) error { return err } - // create account user link - accountUser := m.AccountUser{ - AccountId: accountId, - UserId: user.Id, - Role: m.ROLE_ADMIN, - Created: time.Now(), - Updated: time.Now(), + // create org user link + orgUser := m.OrgUser{ + OrgId: orgId, + UserId: user.Id, + Role: m.ROLE_ADMIN, + Created: time.Now(), + Updated: time.Now(), } - if setting.SingleAccountMode && !user.IsAdmin { - accountUser.Role = m.RoleType(setting.DefaultAccountRole) + if setting.SingleOrgMode && !user.IsAdmin { + orgUser.Role = m.RoleType(setting.DefaultOrgRole) } - if _, err = sess.Insert(&accountUser); err != nil { + if _, err = sess.Insert(&orgUser); err != nil { return err } @@ -112,16 +118,31 @@ func CreateUser(cmd *m.CreateUserCommand) error { }) } +func GetUserById(query *m.GetUserByIdQuery) error { + user := new(m.User) + has, err := x.Id(query.Id).Get(user) + + if err != nil { + return err + } else if has == false { + return m.ErrUserNotFound + } + + query.Result = user + + return nil +} + func GetUserByLogin(query *m.GetUserByLoginQuery) error { if query.LoginOrEmail == "" { - return m.ErrAccountNotFound + return m.ErrUserNotFound } user := new(m.User) if strings.Contains(query.LoginOrEmail, "@") { user = &m.User{Email: query.LoginOrEmail} } else { - user = &m.User{Login: strings.ToLower(query.LoginOrEmail)} + user = &m.User{Login: query.LoginOrEmail} } has, err := x.Get(user) @@ -163,12 +184,28 @@ func UpdateUser(cmd *m.UpdateUserCommand) error { }) } -func SetUsingAccount(cmd *m.SetUsingAccountCommand) error { +func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error { + return inTransaction2(func(sess *session) error { + + user := m.User{ + Password: cmd.NewPassword, + Updated: time.Now(), + } + + if _, err := sess.Id(cmd.UserId).Update(&user); err != nil { + return err + } + + return nil + }) +} + +func SetUsingOrg(cmd *m.SetUsingOrgCommand) error { return inTransaction(func(sess *xorm.Session) error { user := m.User{} sess.Id(cmd.UserId).Get(&user) - user.AccountId = cmd.AccountId + user.OrgId = cmd.OrgId _, err := sess.Id(user.Id).Update(&user) return err }) @@ -193,12 +230,12 @@ func GetUserInfo(query *m.GetUserInfoQuery) error { return err } -func GetUserAccounts(query *m.GetUserAccountsQuery) error { - query.Result = make([]*m.UserAccountDTO, 0) - sess := x.Table("account_user") - sess.Join("INNER", "account", "account_user.account_id=account.id") - sess.Where("account_user.user_id=?", query.UserId) - sess.Cols("account.name", "account_user.role", "account_user.account_id") +func GetUserOrgList(query *m.GetUserOrgListQuery) error { + query.Result = make([]*m.UserOrgDTO, 0) + sess := x.Table("org_user") + sess.Join("INNER", "org", "org_user.org_id=org.id") + sess.Where("org_user.user_id=?", query.UserId) + sess.Cols("org.name", "org_user.role", "org_user.org_id") err := sess.Find(&query.Result) return err } @@ -210,12 +247,12 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { u.email as email, u.login as login, u.name as name, - account.name as account_name, - account_user.role as account_role, - account.id as account_id + org.name as org_name, + org_user.role as org_role, + org.id as org_id FROM ` + dialect.Quote("user") + ` as u - LEFT OUTER JOIN account_user on account_user.account_id = u.account_id and account_user.user_id = u.id - LEFT OUTER JOIN account on account.id = u.account_id + LEFT OUTER JOIN org_user on org_user.org_id = u.org_id and org_user.user_id = u.id + LEFT OUTER JOIN org on org.id = u.org_id WHERE u.id=?` var user m.SignedInUser @@ -240,3 +277,23 @@ func SearchUsers(query *m.SearchUsersQuery) error { err := sess.Find(&query.Result) return err } + +func DeleteUser(cmd *m.DeleteUserCommand) error { + return inTransaction(func(sess *xorm.Session) error { + var rawSql = fmt.Sprintf("DELETE FROM %s WHERE id=?", x.Dialect().Quote("user")) + _, err := sess.Exec(rawSql, cmd.UserId) + return err + }) +} + +func UpdateUserPermissions(cmd *m.UpdateUserPermissionsCommand) error { + return inTransaction(func(sess *xorm.Session) error { + user := m.User{} + sess.Id(cmd.UserId).Get(&user) + + user.IsAdmin = cmd.IsGrafanaAdmin + sess.UseBool("is_admin") + _, err := sess.Id(user.Id).Update(&user) + return err + }) +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index a2db1a6fec8..36a7d7fcf1c 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -4,6 +4,7 @@ package setting import ( + "fmt" "net/url" "os" "path" @@ -65,18 +66,18 @@ var ( CookieRememberName string DisableUserSignUp bool - // single account - SingleAccountMode bool - DefaultAccountName string - DefaultAccountRole string + // single organization + SingleOrgMode bool + DefaultOrgName string + DefaultOrgRole string // Http auth AdminUser string AdminPassword string - AnonymousEnabled bool - AnonymousAccountName string - AnonymousAccountRole string + AnonymousEnabled bool + AnonymousOrgName string + AnonymousOrgRole string // Session settings. SessionOptions session.Options @@ -98,15 +99,10 @@ var ( func init() { IsWindows = runtime.GOOS == "windows" log.NewLogger(0, "console", `{"level": 0}`) -} - -func getWorkDir() string { - p, _ := filepath.Abs(".") - return p + WorkDir, _ = filepath.Abs(".") } func findConfigFiles() []string { - WorkDir = getWorkDir() ConfRootPath = path.Join(WorkDir, "conf") filenames := make([]string, 0) @@ -152,10 +148,29 @@ func ToAbsUrl(relativeUrl string) string { return AppUrl + relativeUrl } -func NewConfigContext() { +func loadEnvVariableOverrides() { + for _, section := range Cfg.Sections() { + for _, key := range section.Keys() { + sectionName := strings.ToUpper(strings.Replace(section.Name(), ".", "_", -1)) + keyName := strings.ToUpper(strings.Replace(key.Name(), ".", "_", -1)) + envKey := fmt.Sprintf("GF_%s_%s", sectionName, keyName) + envValue := os.Getenv(envKey) + + if len(envValue) > 0 { + log.Info("Setting: ENV override found: %s", envKey) + key.SetValue(envValue) + } + } + } +} + +func NewConfigContext(config string) { configFiles := findConfigFiles() - //log.Info("Loading config files: %v", configFiles) + if config != "" { + configFiles = append(configFiles, config) + } + var err error for i, file := range configFiles { @@ -170,6 +185,8 @@ func NewConfigContext() { } } + loadEnvVariableOverrides() + AppName = Cfg.Section("").Key("app_name").MustString("Grafana") Env = Cfg.Section("").Key("app_mode").MustString("development") @@ -187,11 +204,6 @@ func NewConfigContext() { HttpAddr = server.Key("http_addr").MustString("0.0.0.0") HttpPort = server.Key("http_port").MustString("3000") - port := os.Getenv("PORT") - if port != "" { - HttpPort = port - } - StaticRootPath = server.Key("static_root_path").MustString(path.Join(WorkDir, "webapp")) RouterLogging = server.Key("router_logging").MustBool(false) EnableGzip = server.Key("enable_gzip").MustBool(false) @@ -208,14 +220,14 @@ func NewConfigContext() { AdminPassword = security.Key("admin_password").String() // single account - SingleAccountMode = Cfg.Section("account.single").Key("enabled").MustBool(false) - DefaultAccountName = Cfg.Section("account.single").Key("account_name").MustString("main") - DefaultAccountRole = Cfg.Section("account.single").Key("default_role").In("Editor", []string{"Editor", "Admin", "Viewer"}) + SingleOrgMode = Cfg.Section("organization.single").Key("enabled").MustBool(false) + DefaultOrgName = Cfg.Section("organization.single").Key("org_name").MustString("main") + DefaultOrgRole = Cfg.Section("organization.single").Key("default_role").In("Editor", []string{"Editor", "Admin", "Viewer"}) // anonymous access AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false) - AnonymousAccountName = Cfg.Section("auth.anonymous").Key("account_name").String() - AnonymousAccountRole = Cfg.Section("auth.anonymous").Key("account_role").String() + AnonymousOrgName = Cfg.Section("auth.anonymous").Key("org_name").String() + AnonymousOrgRole = Cfg.Section("auth.anonymous").Key("org_role").String() // PhantomJS rendering ImagesDir = "data/png" diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go new file mode 100644 index 00000000000..253eb94f123 --- /dev/null +++ b/pkg/setting/setting_test.go @@ -0,0 +1,32 @@ +package setting + +import ( + "os" + "path/filepath" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestLoadingSettings(t *testing.T) { + + WorkDir, _ = filepath.Abs("../../") + + Convey("Testing loading settings from ini file", t, func() { + + Convey("Given the default ini files", func() { + NewConfigContext("") + + So(AppName, ShouldEqual, "Grafana") + So(AdminUser, ShouldEqual, "admin") + }) + + Convey("Should be able to override via environment variables", func() { + os.Setenv("GF_SECURITY_ADMIN_USER", "superduper") + NewConfigContext("") + + So(AdminUser, ShouldEqual, "superduper") + }) + + }) +} diff --git a/src/app/components/kbn.js b/src/app/components/kbn.js index fa375f83f6e..5ccd5b9508b 100644 --- a/src/app/components/kbn.js +++ b/src/app/components/kbn.js @@ -168,14 +168,22 @@ function($, _, moment) { if(_.isDate(text)) { return text; } - var time, - mathString = "", - index, - parseString; + + var time; + var mathString = ""; + var index; + var parseString; + if (text.substring(0,3) === "now") { time = new Date(); - mathString = text.substring("now".length); - } else { + mathString = text.substring(3); + } + else if (text.substring(0,5) === 'today') { + time = new Date(); + time.setHours(0,0,0,0); + mathString = text.substring(5); + } + else { index = text.indexOf("||"); parseString; if (index === -1) { @@ -197,6 +205,11 @@ function($, _, moment) { return kbn.parseDateMath(mathString, time); }; + kbn._timespanRegex = /^\d+[h,m,M,w,s,H,d]$/; + kbn.isValidTimeSpan = function(str) { + return kbn._timespanRegex.test(str); + }; + kbn.parseDateMath = function(mathString, time, roundUp) { var dateTime = moment(time); for (var i = 0; i < mathString.length;) { @@ -321,11 +334,15 @@ function($, _, moment) { } var steps = 0; + var limit = extArray.length; while (Math.abs(size) >= factor) { steps++; size /= factor; + + if (steps >= limit) { return "NA"; } } + if (steps > 0) { decimals = scaledDecimals + (3 * steps); } @@ -367,10 +384,21 @@ function($, _, moment) { kbn.valueFormats.bps = kbn.formatFuncCreator(1000, [' bps', ' Kbps', ' Mbps', ' Gbps', ' Tbps', ' Pbps', ' Ebps', ' Zbps', ' Ybps']); kbn.valueFormats.Bps = kbn.formatFuncCreator(1000, [' Bps', ' KBps', ' MBps', ' GBps', ' TBps', ' PBps', ' EBps', ' ZBps', ' YBps']); kbn.valueFormats.short = kbn.formatFuncCreator(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Qaudr', ' Quint', ' Sext', ' Sept']); - kbn.valueFormats.joule = kbn.formatFuncCreator(1000, [' J', ' kJ', ' MJ', 'GJ', 'TJ', 'PJ', 'EJ', 'ZJ', 'YJ']); - kbn.valueFormats.watt = kbn.formatFuncCreator(1000, [' W', ' kW', ' MW', 'GW', 'TW', 'PW', 'EW', 'ZW', 'YW']); + kbn.valueFormats.joule = kbn.formatFuncCreator(1000, [' J', ' kJ', ' MJ', ' GJ', ' TJ', ' PJ', ' EJ', ' ZJ', ' YJ']); + kbn.valueFormats.watt = kbn.formatFuncCreator(1000, [' W', ' kW', ' MW', ' GW', ' TW', ' PW', ' EW', ' ZW', ' YW']); + kbn.valueFormats.kwatt = kbn.formatFuncCreator(1000, [' kW', ' MW', ' GW', ' TW', ' PW', ' EW', ' ZW', ' YW']); + kbn.valueFormats.watth = kbn.formatFuncCreator(1000, [' Wh', ' kWh', ' MWh', ' GWh', ' TWh', ' PWh', ' EWh', ' ZWh', ' YWh']); + kbn.valueFormats.kwatth = kbn.formatFuncCreator(1000, [' kWh', ' MWh', ' GWh', ' TWh', ' PWh', ' EWh', ' ZWh', ' YWh']); kbn.valueFormats.ev = kbn.formatFuncCreator(1000, [' eV', ' keV', ' MeV', 'GeV', 'TeV', 'PeV', 'EeV', 'ZeV', 'YeV']); kbn.valueFormats.none = kbn.toFixed; + kbn.valueFormats.celsius = function(value, decimals) { return kbn.toFixed(value, decimals) + ' °C'; }; + kbn.valueFormats.farenheit = function(value, decimals) { return kbn.toFixed(value, decimals) + ' °F'; }; + kbn.valueFormats.humidity = function(value, decimals) { return kbn.toFixed(value, decimals) + ' %H'; }; + kbn.valueFormats.ppm = function(value, decimals) { return kbn.toFixed(value, decimals) + ' ppm'; }; + kbn.valueFormats.velocityms = function(value, decimals) { return kbn.toFixed(value, decimals) + ' m/s'; }; + kbn.valueFormats.velocitykmh = function(value, decimals) { return kbn.toFixed(value, decimals) + ' km/h'; }; + kbn.valueFormats.velocitymph = function(value, decimals) { return kbn.toFixed(value, decimals) + ' mph'; }; + kbn.valueFormats.velocityknot = function(value, decimals) { return kbn.toFixed(value, decimals) + ' kn'; }; kbn.valueFormats.ms = function(size, decimals, scaledDecimals) { if (size === null) { return ""; } @@ -493,6 +521,7 @@ function($, _, moment) { {text: 'none' , value: 'none'}, {text: 'short', value: 'short'}, {text: 'percent', value: 'percent'}, + {text: 'ppm', value: 'ppm'}, ] }, { @@ -509,8 +538,8 @@ function($, _, moment) { submenu: [ {text: 'bits', value: 'bits'}, {text: 'bytes', value: 'bytes'}, - {text: 'kilo bytes', value: 'kbytes'}, - {text: 'mega bytes', value: 'mbytes'}, + {text: 'kilobytes', value: 'kbytes'}, + {text: 'megabytes', value: 'mbytes'}, ] }, { @@ -523,9 +552,29 @@ function($, _, moment) { { text: 'energy', submenu: [ - {text: 'watt', value: 'watt'}, - {text: 'joule', value: 'joule'}, - {text: 'eV', value: 'ev'}, + {text: 'watt (W)', value: 'watt'}, + {text: 'kilowatt (kW)', value: 'kwatt'}, + {text: 'watt-hour (Wh)', value: 'watth'}, + {text: 'kilowatt-hour (kWh)', value: 'kwatth'}, + {text: 'joule (J)', value: 'joule'}, + {text: 'electron volt (eV)', value: 'ev'}, + ] + }, + { + text: 'weather', + submenu: [ + {text: 'Celcius (°C)', value: 'celsius' }, + {text: 'Farenheit (°F)', value: 'farenheit'}, + {text: 'Humidity (%H)', value: 'humidity' }, + ] + }, + { + text: 'velocity', + submenu: [ + {text: 'm/s', value: 'velocityms' }, + {text: 'km/h', value: 'velocitykmh' }, + {text: 'mph', value: 'velocitymph' }, + {text: 'knot (kn)', value: 'velocityknot' }, ] }, ]; diff --git a/src/app/components/settings.js b/src/app/components/settings.js index 7e896b475ea..5530d34f8fa 100644 --- a/src/app/components/settings.js +++ b/src/app/components/settings.js @@ -1,8 +1,7 @@ define([ 'lodash', - 'crypto', ], -function (_, crypto) { +function (_) { "use strict"; return function Settings (options) { @@ -27,55 +26,31 @@ function (_, crypto) { playlist_timespan: "1m", unsaved_changes_warning: true, search: { max_results: 100 }, - admin: {}, appSubUrl: "" }; var settings = _.extend({}, defaults, options); - var parseBasicAuth = function(datasource) { - var passwordEnd = datasource.url.indexOf('@'); - if (passwordEnd > 0) { - var userStart = datasource.url.indexOf('//') + 2; - var userAndPassword = datasource.url.substring(userStart, passwordEnd); - var bytes = crypto.charenc.Binary.stringToBytes(userAndPassword); - datasource.basicAuth = crypto.util.bytesToBase64(bytes); - - var urlHead = datasource.url.substring(0, userStart); - datasource.url = urlHead + datasource.url.substring(passwordEnd + 1); - } - - return datasource; - }; - - var parseMultipleHosts = function(datasource) { - datasource.urls = _.map(datasource.url.split(","), function (url) { return url.trim(); }); - return datasource; - }; - - // backward compatible with old config - if (options.graphiteUrl) { - settings.datasources.graphite = { - type: 'graphite', - url: options.graphiteUrl, - default: true - }; - } - - if (options.elasticsearch) { - settings.datasources.elasticsearch = { - type: 'elasticsearch', - url: options.elasticsearch, - index: options.grafana_index, - grafanaDB: true - }; - } - - _.each(settings.datasources, function(datasource, key) { - datasource.name = key; - if (datasource.url) { parseBasicAuth(datasource); } - if (datasource.type === 'influxdb') { parseMultipleHosts(datasource); } - }); + // var parseBasicAuth = function(datasource) { + // var passwordEnd = datasource.url.indexOf('@'); + // if (passwordEnd > 0) { + // var userStart = datasource.url.indexOf('//') + 2; + // var userAndPassword = datasource.url.substring(userStart, passwordEnd); + // var bytes = crypto.charenc.Binary.stringToBytes(userAndPassword); + // datasource.basicAuth = crypto.util.bytesToBase64(bytes); + // + // var urlHead = datasource.url.substring(0, userStart); + // datasource.url = urlHead + datasource.url.substring(passwordEnd + 1); + // } + // + // return datasource; + // }; + // + // _.each(settings.datasources, function(datasource, key) { + // datasource.name = key; + // if (datasource.url) { parseBasicAuth(datasource); } + // if (datasource.type === 'influxdb') { parseMultipleHosts(datasource); } + // }); if (settings.plugins.panels) { _.extend(settings.panels, settings.plugins.panels); diff --git a/src/app/controllers/all.js b/src/app/controllers/all.js index bf5c8164397..f735963e886 100644 --- a/src/app/controllers/all.js +++ b/src/app/controllers/all.js @@ -3,7 +3,6 @@ define([ './pulldown', './search', './metricKeys', - './graphiteImport', './inspectCtrl', './jsonEditorCtrl', './loginCtrl', diff --git a/src/app/controllers/graphiteImport.js b/src/app/controllers/graphiteImport.js deleted file mode 100644 index 091f4b8fe5f..00000000000 --- a/src/app/controllers/graphiteImport.js +++ /dev/null @@ -1,108 +0,0 @@ -define([ - 'angular', - 'app', - 'lodash', - 'kbn' -], -function (angular, app, _, kbn) { - 'use strict'; - - var module = angular.module('grafana.controllers'); - - module.controller('GraphiteImportCtrl', function($scope, $rootScope, $timeout, datasourceSrv, $location) { - - $scope.init = function() { - $scope.datasources = datasourceSrv.getMetricSources(); - $scope.setDatasource(null); - }; - - $scope.setDatasource = function(datasource) { - $scope.datasource = datasourceSrv.get(datasource); - - if (!$scope.datasource) { - $scope.error = "Cannot find datasource " + datasource; - return; - } - }; - - $scope.listAll = function(query) { - delete $scope.error; - - $scope.datasource.listDashboards(query) - .then(function(results) { - $scope.dashboards = results; - }) - .then(null, function(err) { - $scope.error = err.message || 'Error while fetching list of dashboards'; - }); - }; - - $scope.import = function(dashName) { - delete $scope.error; - - $scope.datasource.loadDashboard(dashName) - .then(function(results) { - if (!results.data || !results.data.state) { - throw { message: 'no dashboard state received from graphite' }; - } - - graphiteToGrafanaTranslator(results.data.state, $scope.datasource.name); - }) - .then(null, function(err) { - $scope.error = err.message || 'Failed to import dashboard'; - }); - }; - - function graphiteToGrafanaTranslator(state, datasource) { - var graphsPerRow = 2; - var rowHeight = 300; - var rowTemplate; - var currentRow; - var panel; - - rowTemplate = { - title: '', - panels: [], - height: rowHeight - }; - - currentRow = angular.copy(rowTemplate); - - var newDashboard = angular.copy($scope.dashboard); - newDashboard.rows = []; - newDashboard.title = state.name; - newDashboard.rows.push(currentRow); - - _.each(state.graphs, function(graph, index) { - if (currentRow.panels.length === graphsPerRow) { - currentRow = angular.copy(rowTemplate); - newDashboard.rows.push(currentRow); - } - - panel = { - type: 'graph', - span: 12 / graphsPerRow, - title: graph[1].title, - targets: [], - datasource: datasource, - id: index + 1 - }; - - _.each(graph[1].target, function(target) { - panel.targets.push({ - target: target - }); - }); - - currentRow.panels.push(panel); - }); - - window.grafanaImportDashboard = newDashboard; - $location.path('/dashboard/import/' + kbn.slugifyForUrl(newDashboard.title)); - - $scope.dismiss(); - } - - }); - -}); diff --git a/src/app/controllers/search.js b/src/app/controllers/search.js index 6aa525b7f91..767b1e5d88b 100644 --- a/src/app/controllers/search.js +++ b/src/app/controllers/search.js @@ -2,9 +2,8 @@ define([ 'angular', 'lodash', 'config', - 'jquery' ], -function (angular, _, config, $) { +function (angular, _, config) { 'use strict'; var module = angular.module('grafana.controllers'); @@ -15,7 +14,7 @@ function (angular, _, config, $) { $scope.giveSearchFocus = 0; $scope.selectedIndex = -1; $scope.results = {dashboards: [], tags: [], metrics: []}; - $scope.query = { query: '' }; + $scope.query = { query: '', tag: '', starred: false }; $scope.db = datasourceSrv.getGrafanaDB(); $scope.currentSearchId = 0; @@ -29,7 +28,7 @@ function (angular, _, config, $) { $scope.keyDown = function (evt) { if (evt.keyCode === 27) { - $scope.appEvent('hide-dash-editor'); + $scope.dismiss(); } if (evt.keyCode === 40) { $scope.moveSelection(1); @@ -49,10 +48,7 @@ function (angular, _, config, $) { var selectedDash = $scope.results.dashboards[$scope.selectedIndex]; if (selectedDash) { $location.search({}); - $location.path("/dashboard/db/" + selectedDash.slug); - setTimeout(function() { - $('body').click(); // hack to force dropdown to close; - }); + $location.path(selectedDash.url); } } }; @@ -61,11 +57,6 @@ function (angular, _, config, $) { $scope.selectedIndex = Math.max(Math.min($scope.selectedIndex + direction, $scope.resultCount - 1), 0); }; - $scope.goToDashboard = function(slug) { - $location.search({}); - $location.path("/dashboard/db/" + slug); - }; - $scope.searchDashboards = function() { $scope.currentSearchId = $scope.currentSearchId + 1; var localSearchId = $scope.currentSearchId; @@ -74,16 +65,24 @@ function (angular, _, config, $) { .then(function(results) { if (localSearchId < $scope.currentSearchId) { return; } - if ($scope.query.query === "" && !$scope.query.starred) { - results.dashboards.unshift({ title: 'Home', url: config.appSubUrl + '/', isHome: true }); - } - - $scope.results.dashboards = results.dashboards; - $scope.results.tags = results.tags; $scope.resultCount = results.tagsOnly ? results.tags.length : results.dashboards.length; + $scope.results.tags = results.tags; + $scope.results.dashboards = _.map(results.dashboards, function(dash) { + dash.url = 'dashboard/db/' + dash.slug; + return dash; + }); + + if ($scope.queryHasNoFilters()) { + $scope.results.dashboards.unshift({ title: 'Home', url: config.appSubUrl + '/', isHome: true }); + } }); }; + $scope.queryHasNoFilters = function() { + var query = $scope.query; + return query.query === '' && query.starred === false && query.tag === ''; + }; + $scope.filterByTag = function(tag, evt) { $scope.query.tag = tag; $scope.query.tagcloud = false; diff --git a/src/app/controllers/sidemenuCtrl.js b/src/app/controllers/sidemenuCtrl.js index cb5027d3dd2..f5a9197caac 100644 --- a/src/app/controllers/sidemenuCtrl.js +++ b/src/app/controllers/sidemenuCtrl.js @@ -9,87 +9,118 @@ function (angular, _, $, config) { var module = angular.module('grafana.controllers'); - module.controller('SideMenuCtrl', function($scope, $location, contextSrv) { + module.controller('SideMenuCtrl', function($scope, $location, contextSrv, backendSrv) { $scope.getUrl = function(url) { return config.appSubUrl + url; }; - $scope.menu = []; - $scope.menu.push({ - text: "Dashboards", - icon: "fa fa-th-large", - href: $scope.getUrl("/"), - }); - - if (contextSrv.hasRole('Admin')) { - $scope.menu.push({ - text: "Data Sources", - icon: "fa fa-database", - href: $scope.getUrl("/account/datasources"), + $scope.setupMainNav = function() { + $scope.mainLinks.push({ + text: "Dashboards", + icon: "fa fa-fw fa-th-large", + href: $scope.getUrl("/"), }); - $scope.menu.push({ - text: "Account", href: $scope.getUrl("/account"), - requireRole: "Admin", - icon: "fa fa-shield", - }); - } - if (contextSrv.user.isGrafanaAdmin) { - $scope.menu.push({ - text: "Admin", href: $scope.getUrl("/admin/users"), - icon: "fa fa-cube", - requireSignedIn: true, - links: [ - { text: 'Settings', href: $scope.getUrl("/admin/settings")}, - { text: 'Users', href: $scope.getUrl("/admin/users"), icon: "fa fa-lock" }, - { text: 'Log', href: "", icon: "fa fa-lock" }, - ] - }); - } + if (contextSrv.hasRole('Admin')) { + $scope.mainLinks.push({ + text: "Data Sources", + icon: "fa fa-fw fa-database", + href: $scope.getUrl("/datasources"), + }); + } + }; - $scope.updateState = function() { - var currentPath = config.appSubUrl + $location.path(); - var search = $location.search(); + $scope.loadOrgs = function() { + $scope.orgMenu = []; - _.each($scope.menu, function(item) { - item.active = false; + if (contextSrv.hasRole('Admin')) { + $scope.orgMenu.push({ + text: "Organization settings", + href: $scope.getUrl("/org"), + }); + $scope.orgMenu.push({ + text: "Users", + href: $scope.getUrl("/org/users"), + }); + $scope.orgMenu.push({ + text: "API Keys", + href: $scope.getUrl("/org/apikeys"), + }); + } - if (item.href === currentPath) { - item.active = true; - } + if ($scope.orgMenu.length > 0) { + $scope.orgMenu.push({ cssClass: 'divider' }); + } - if (item.startsWith) { - if (currentPath.indexOf(item.startsWith) === 0) { - item.active = true; - item.href = currentPath; - } - } - - _.each(item.links, function(link) { - link.active = false; - - if (link.editview) { - var params = {}; - _.each(search, function(value, key) { - if (value !== null) { params[key] = value; } - }); - - params.editview = link.editview; - link.href = currentPath + '?' + $.param(params); + backendSrv.get('/api/user/orgs').then(function(orgs) { + _.each(orgs, function(org) { + if (org.isUsing) { + return; } - if (link.href === currentPath) { - item.active = true; - link.active = true; - } + $scope.orgMenu.push({ + text: "Switch to " + org.name, + icon: "fa fa-fw fa-random", + click: function() { + $scope.switchOrg(org.orgId); + } + }); + }); + + $scope.orgMenu.push({ + text: "New Organization", + icon: "fa fa-fw fa-plus", + href: $scope.getUrl('/org/new') }); }); + }; + $scope.switchOrg = function(orgId) { + backendSrv.post('/api/user/using/' + orgId).then(function() { + window.location.href = $scope.getUrl('/'); + }); + }; + + $scope.setupAdminNav = function() { + $scope.systemSection = true; + $scope.grafanaVersion = config.buildInfo.version; + + $scope.mainLinks.push({ + text: "System info", + icon: "fa fa-fw fa-info", + href: $scope.getUrl("/admin/settings"), + }); + + $scope.mainLinks.push({ + text: "Global Users", + icon: "fa fa-fw fa-user", + href: $scope.getUrl("/admin/users"), + }); + + $scope.mainLinks.push({ + text: "Global Orgs", + icon: "fa fa-fw fa-users", + href: $scope.getUrl("/admin/orgs"), + }); + }; + + $scope.updateMenu = function() { + $scope.systemSection = false; + $scope.mainLinks = []; + $scope.orgMenu = []; + + var currentPath = $location.path(); + if (currentPath.indexOf('/admin') === 0) { + $scope.setupAdminNav(); + } else { + $scope.setupMainNav(); + } }; $scope.init = function() { - $scope.updateState(); + $scope.updateMenu(); + $scope.$on('$routeChangeSuccess', $scope.updateMenu); }; }); diff --git a/src/app/directives/bodyClass.js b/src/app/directives/bodyClass.js index 0b1cac65614..20339b8bf92 100644 --- a/src/app/directives/bodyClass.js +++ b/src/app/directives/bodyClass.js @@ -1,9 +1,8 @@ define([ 'angular', - 'app', 'lodash' ], -function (angular) { +function (angular, _) { 'use strict'; angular @@ -35,9 +34,8 @@ function (angular) { } }); - $scope.$watch('playlist_active', function() { - elem.toggleClass('hide-controls', $scope.playlist_active === true); - elem.toggleClass('playlist-active', $scope.playlist_active === true); + $scope.$watch('playlistSrv', function(newValue) { + elem.toggleClass('playlist-active', _.isObject(newValue)); }); } }; diff --git a/src/app/directives/dashEditLink.js b/src/app/directives/dashEditLink.js index 0933d3f96f2..1b5173671b0 100644 --- a/src/app/directives/dashEditLink.js +++ b/src/app/directives/dashEditLink.js @@ -118,5 +118,4 @@ function (angular, $) { } }; }); - }); diff --git a/src/app/directives/dashUpload.js b/src/app/directives/dashUpload.js index 889438dffc9..d9f8e3e4ae9 100644 --- a/src/app/directives/dashUpload.js +++ b/src/app/directives/dashUpload.js @@ -24,6 +24,7 @@ function (angular, kbn) { return; } var title = kbn.slugifyForUrl(window.grafanaImportDashboard.title); + window.grafanaImportDashboard.id = null; $location.path('/dashboard/import/' + title); }); }; diff --git a/src/app/directives/ngModelOnBlur.js b/src/app/directives/ngModelOnBlur.js index 1aea09f9dc8..8057c071bdb 100644 --- a/src/app/directives/ngModelOnBlur.js +++ b/src/app/directives/ngModelOnBlur.js @@ -1,5 +1,8 @@ -define(['angular'], -function (angular) { +define([ + 'angular', + 'kbn' +], +function (angular, kbn) { 'use strict'; angular @@ -34,5 +37,18 @@ function (angular) { }); } }; + }) + .directive('validTimeSpan', function() { + return { + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + ctrl.$validators.integer = function(modelValue, viewValue) { + if (ctrl.$isEmpty(modelValue)) { + return true; + } + return kbn.isValidTimeSpan(viewValue); + }; + } + }; }); }); diff --git a/src/app/directives/templateParamSelector.js b/src/app/directives/templateParamSelector.js index 5ff6eaa0038..8a09e814f28 100644 --- a/src/app/directives/templateParamSelector.js +++ b/src/app/directives/templateParamSelector.js @@ -14,7 +14,7 @@ function (angular, app, _, $) { ' class="tight-form-clear-input input-medium"' + ' spellcheck="false" style="display:none">'; - var buttonTemplate = '{{variable.current.text}}'; + var buttonTemplate = '{{variable.current.text}} '; return { link: function($scope, elem) { diff --git a/src/app/directives/topnav.js b/src/app/directives/topnav.js index fb9d8a82ed1..c871dd085cb 100644 --- a/src/app/directives/topnav.js +++ b/src/app/directives/topnav.js @@ -20,7 +20,7 @@ function (angular) { template: '', link: function(scope, elem, attrs) { scope.icon = attrs.icon; - scope.showMenuBtn = !contextSrv.sidemenu; + scope.contextSrv = contextSrv; scope.toggle = function() { $rootScope.appEvent('toggle-sidemenu'); diff --git a/src/app/features/account/accountCtrl.js b/src/app/features/account/accountCtrl.js deleted file mode 100644 index 8d4130bda03..00000000000 --- a/src/app/features/account/accountCtrl.js +++ /dev/null @@ -1,29 +0,0 @@ -define([ - 'angular', -], -function (angular) { - 'use strict'; - - var module = angular.module('grafana.controllers'); - - module.controller('AccountCtrl', function($scope, $http, backendSrv) { - - $scope.init = function() { - $scope.getAccount(); - }; - - $scope.getAccount = function() { - backendSrv.get('/api/account').then(function(account) { - $scope.account = account; - }); - }; - - $scope.update = function() { - if (!$scope.accountForm.$valid) { return; } - backendSrv.put('/api/account', $scope.account).then($scope.getAccount); - }; - - $scope.init(); - - }); -}); diff --git a/src/app/features/account/datasourcesCtrl.js b/src/app/features/account/datasourcesCtrl.js deleted file mode 100644 index b8f9f9fda7a..00000000000 --- a/src/app/features/account/datasourcesCtrl.js +++ /dev/null @@ -1,88 +0,0 @@ -define([ - 'angular', -], -function (angular) { - 'use strict'; - - var module = angular.module('grafana.controllers'); - - module.controller('DataSourcesCtrl', function($scope, $http, backendSrv) { - - var defaults = { - name: '', - type: 'graphite', - url: '', - access: 'proxy' - }; - - $scope.types = [ - { name: 'Graphite', type: 'graphite' }, - { name: 'InfluxDB', type: 'influxdb' }, - { name: 'Elasticsearch', type: 'elasticsearch' }, - { name: 'OpenTSDB', type: 'opentsdb' }, - ]; - - $scope.init = function() { - $scope.reset(); - $scope.editor = {index: 0}; - $scope.datasources = []; - $scope.getDatasources(); - - $scope.$watch('editor.index', function(newVal) { - if (newVal !== 2) { - $scope.reset(); - } - }); - }; - - $scope.reset = function() { - $scope.current = angular.copy(defaults); - $scope.currentIsNew = true; - }; - - $scope.edit = function(ds) { - $scope.current = ds; - $scope.currentIsNew = false; - $scope.editor.index = 2; - }; - - $scope.cancel = function() { - $scope.reset(); - $scope.editor.index = 0; - }; - - $scope.getDatasources = function() { - backendSrv.get('/api/datasources').then(function(results) { - $scope.datasources = results; - }); - }; - - $scope.remove = function(ds) { - backendSrv.delete('/api/datasources/' + ds.id).then(function() { - $scope.getDatasources(); - }); - }; - - $scope.update = function() { - backendSrv.post('/api/datasources', $scope.current).then(function() { - $scope.editor.index = 0; - $scope.getDatasources(); - }); - }; - - $scope.add = function() { - if (!$scope.editForm.$valid) { - return; - } - - backendSrv.put('/api/datasources', $scope.current) - .then(function() { - $scope.editor.index = 0; - $scope.getDatasources(); - }); - }; - - $scope.init(); - - }); -}); diff --git a/src/app/features/account/partials/account.html b/src/app/features/account/partials/account.html deleted file mode 100644 index a25a6bb7bc3..00000000000 --- a/src/app/features/account/partials/account.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - -
-
- -

Account Info

- -
-
-
-
    -
  • - Account name -
  • -
  • - -
  • -
-
-
-
-
- -
- -

- API Keys -

- -
-
-
    -
  • - Add a key -
  • -
  • - -
  • -
  • - Role -
  • -
  • - -
  • -
  • - -
  • -
    -
-
-
- - - - - - - - -
{{t.name}}{{t.role}}{{t.key}} - - - -
-
- -
-
- - diff --git a/src/app/features/account/partials/apikeys.html b/src/app/features/account/partials/apikeys.html deleted file mode 100644 index bc9a3657a56..00000000000 --- a/src/app/features/account/partials/apikeys.html +++ /dev/null @@ -1,51 +0,0 @@ - - - -
-
-
-
-
-
    -
  • - Add a key -
  • -
  • - -
  • -
  • - Role -
  • -
  • - -
  • - - -
-
-
-
- -
-
- - - - - - - -
{{t.name}}{{t.role}}{{t.key}} - - - -
-
-
-
-
- - - diff --git a/src/app/features/account/partials/datasources.html b/src/app/features/account/partials/datasources.html deleted file mode 100644 index f3bac8b35c6..00000000000 --- a/src/app/features/account/partials/datasources.html +++ /dev/null @@ -1,117 +0,0 @@ - - - -
-
-
-
-
-
-
- -
-
-
-
-
- No datasources defined -
- - - - - - - - - - - - - - - -
NameUrl
-   - {{ds.name}} - - {{ds.url}} - - - default - - - - - Edit - - - - - -
-
-
- -
-
-
- - -
-
- - -
- -
- -
-
- - -
-
- - -
-
- -
-
-
InfluxDB Details
-
- - -
-
- - -
-
- - -
-
-
-
-
-
Elastic search details
-
- - -
-
-
-
- -
- - - -
- -
-
- diff --git a/src/app/features/admin/adminCtrl.js b/src/app/features/admin/adminCtrl.js deleted file mode 100644 index bda82618503..00000000000 --- a/src/app/features/admin/adminCtrl.js +++ /dev/null @@ -1,18 +0,0 @@ -define([ - 'angular', -], -function (angular) { - 'use strict'; - - var module = angular.module('grafana.controllers'); - - module.controller('AdminCtrl', function($scope) { - - $scope.init = function() { - $scope.editor = {index: 0}; - }; - - $scope.init(); - - }); -}); diff --git a/src/app/features/admin/adminEditUserCtrl.js b/src/app/features/admin/adminEditUserCtrl.js new file mode 100644 index 00000000000..19deac532ea --- /dev/null +++ b/src/app/features/admin/adminEditUserCtrl.js @@ -0,0 +1,63 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('AdminEditUserCtrl', function($scope, $routeParams, backendSrv, $location) { + $scope.user = {}; + $scope.permissions = {}; + + $scope.init = function() { + if ($routeParams.id) { + $scope.getUser($routeParams.id); + } + }; + + $scope.getUser = function(id) { + backendSrv.get('/api/admin/users/' + id).then(function(user) { + $scope.user = user; + $scope.user_id = id; + $scope.permissions.isGrafanaAdmin = user.isGrafanaAdmin; + }); + }; + + $scope.setPassword = function () { + if (!$scope.passwordForm.$valid) { return; } + + var payload = { password: $scope.password }; + backendSrv.put('/api/admin/users/' + $scope.user_id + '/password', payload).then(function() { + $location.path('/admin/users'); + }); + }; + + $scope.updatePermissions = function() { + var payload = $scope.permissions; + + backendSrv.put('/api/admin/users/' + $scope.user_id + '/permissions', payload).then(function() { + $location.path('/admin/users'); + }); + }; + + $scope.create = function() { + if (!$scope.userForm.$valid) { return; } + + backendSrv.post('/api/admin/users', $scope.user).then(function() { + $location.path('/admin/users'); + }); + }; + + $scope.update = function() { + if (!$scope.userForm.$valid) { return; } + + backendSrv.put('/api/admin/users/' + $scope.user_id + '/details', $scope.user).then(function() { + $location.path('/admin/users'); + }); + }; + + $scope.init(); + + }); +}); diff --git a/src/app/features/admin/adminSettingsCtrl.js b/src/app/features/admin/adminSettingsCtrl.js new file mode 100644 index 00000000000..d980281577d --- /dev/null +++ b/src/app/features/admin/adminSettingsCtrl.js @@ -0,0 +1,24 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('AdminSettingsCtrl', function($scope, backendSrv) { + + $scope.init = function() { + $scope.getUsers(); + }; + + $scope.getUsers = function() { + backendSrv.get('/api/admin/settings').then(function(settings) { + $scope.settings = settings; + }); + }; + + $scope.init(); + + }); +}); diff --git a/src/app/features/admin/adminUsersCtrl.js b/src/app/features/admin/adminUsersCtrl.js index ce5cc7a3b86..3c45d067a6e 100644 --- a/src/app/features/admin/adminUsersCtrl.js +++ b/src/app/features/admin/adminUsersCtrl.js @@ -9,7 +9,6 @@ function (angular) { module.controller('AdminUsersCtrl', function($scope, backendSrv) { $scope.init = function() { - $scope.accounts = []; $scope.getUsers(); }; @@ -24,7 +23,9 @@ function (angular) { title: 'Delete user', text: 'Are you sure you want to delete user: ' + user.login, onConfirm: function() { - backendSrv.delete('/api/admin/users/delete/' + user.id); + backendSrv.delete('/api/admin/users/' + user.id).then(function() { + $scope.getUsers(); + }); } }); }; diff --git a/src/app/features/admin/all.js b/src/app/features/admin/all.js new file mode 100644 index 00000000000..35f1f4f5db9 --- /dev/null +++ b/src/app/features/admin/all.js @@ -0,0 +1,5 @@ +define([ + './adminUsersCtrl', + './adminEditUserCtrl', + './adminSettingsCtrl', +], function () {}); diff --git a/src/app/features/admin/partials/admin.html b/src/app/features/admin/partials/admin.html deleted file mode 100644 index 1198f8b5777..00000000000 --- a/src/app/features/admin/partials/admin.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - -
-
diff --git a/src/app/features/admin/partials/edit_user.html b/src/app/features/admin/partials/edit_user.html new file mode 100644 index 00000000000..9b2a18fd010 --- /dev/null +++ b/src/app/features/admin/partials/edit_user.html @@ -0,0 +1,98 @@ + + + + +
+
+

+ User details +

+ +
+
+
+
    +
  • + Name +
  • +
  • + +
  • +
+
+
+
+
    +
  • + Email +
  • +
  • + +
  • +
+
+
+
+
    +
  • + Username +
  • +
  • + +
  • +
+
+
+
+ +
+ +
+ +

+ Change password +

+ +
+
+
+
    +
  • + New password +
  • +
  • + +
  • +
+
+
+
+ +
+ +
+ +

+ Permissions +

+ +
+
    +
  • + Grafana Admin  + + +
  • +
+
+
+
+ + +
+
diff --git a/src/app/features/admin/partials/new_user.html b/src/app/features/admin/partials/new_user.html new file mode 100644 index 00000000000..48f78fb76b6 --- /dev/null +++ b/src/app/features/admin/partials/new_user.html @@ -0,0 +1,66 @@ + + + + +
+
+

+ Create a new user +

+ +
+
+
+
    +
  • + Name +
  • +
  • + +
  • +
+
+
+
+
    +
  • + Email +
  • +
  • + +
  • +
+
+
+
+
    +
  • + Username +
  • +
  • + +
  • +
+
+
+
+
    +
  • + Password +
  • +
  • + +
  • +
+
+
+
+ +
+ +
+
+
diff --git a/src/app/features/admin/partials/orgs.html b/src/app/features/admin/partials/orgs.html new file mode 100644 index 00000000000..1bb8996a825 --- /dev/null +++ b/src/app/features/admin/partials/orgs.html @@ -0,0 +1,15 @@ + + + + +
+
+

+ Organizations +

+ + View not implemented yet... +
+
diff --git a/src/app/features/admin/partials/settings.html b/src/app/features/admin/partials/settings.html new file mode 100644 index 00000000000..d242a9a2776 --- /dev/null +++ b/src/app/features/admin/partials/settings.html @@ -0,0 +1,29 @@ + + + +
+
+

+ System information +

+ +
+ These system settings are defined in grafana.ini or grafana.custom.ini (or overriden in ENV variables). + To change these you currently need to restart grafana. +
+ + + + + + + + + + +
{{secName}}
{{keyName}}{{keyValue}}
+
+
+ + + diff --git a/src/app/features/admin/partials/users.html b/src/app/features/admin/partials/users.html index ebc31919457..14bcd922b5e 100644 --- a/src/app/features/admin/partials/users.html +++ b/src/app/features/admin/partials/users.html @@ -1,36 +1,38 @@ - +
-

Users

+

+ Users +

+ - - + + - diff --git a/src/app/features/all.js b/src/app/features/all.js index a262a57b5ba..6a3294f3487 100644 --- a/src/app/features/all.js +++ b/src/app/features/all.js @@ -4,17 +4,14 @@ define([ './templating/templateSrv', './graphite/datasource', './influxdb/datasource', + './influxdb_08/datasource', './opentsdb/datasource', './elasticsearch/datasource', './dashboard/all', './panel/all', './profile/profileCtrl', - './account/accountUsersCtrl', - './account/datasourcesCtrl', - './account/apiKeysCtrl', - './account/importCtrl', - './account/accountCtrl', - './admin/adminUsersCtrl', - './admin/adminCtrl', + './profile/changePasswordCtrl', + './org/all', + './admin/all', './grafanaDatasource/datasource', ], function () {}); diff --git a/src/app/features/annotations/editorCtrl.js b/src/app/features/annotations/editorCtrl.js index 9b6da497dce..39b072263fc 100644 --- a/src/app/features/annotations/editorCtrl.js +++ b/src/app/features/annotations/editorCtrl.js @@ -58,17 +58,22 @@ function (angular, _, $) { $scope.update = function() { $scope.reset(); $scope.editor.index = 0; + $scope.broadcastRefresh(); }; $scope.add = function() { $scope.annotations.push($scope.currentAnnotation); $scope.reset(); $scope.editor.index = 0; + $scope.updateSubmenuVisibility(); + $scope.broadcastRefresh(); }; $scope.removeAnnotation = function(annotation) { var index = _.indexOf($scope.annotations, annotation); $scope.annotations.splice(index, 1); + $scope.updateSubmenuVisibility(); + $scope.broadcastRefresh(); }; }); diff --git a/src/app/features/annotations/partials/editor.html b/src/app/features/annotations/partials/editor.html index fc7d6225d3f..db56d08f940 100644 --- a/src/app/features/annotations/partials/editor.html +++ b/src/app/features/annotations/partials/editor.html @@ -10,7 +10,8 @@
- diff --git a/src/app/features/dashboard/all.js b/src/app/features/dashboard/all.js index 8433ecc54ce..0bd631447a7 100644 --- a/src/app/features/dashboard/all.js +++ b/src/app/features/dashboard/all.js @@ -1,6 +1,7 @@ define([ './dashboardCtrl', './dashboardNavCtrl', + './cloneDashboardCtrl', './playlistCtrl', './rowCtrl', './sharePanelCtrl', @@ -11,4 +12,7 @@ define([ './playlistSrv', './timeSrv', './unsavedChangesSrv', + './directives/dashSearchView', + './graphiteImportCtrl', + './importCtrl', ], function () {}); diff --git a/src/app/features/dashboard/cloneDashboardCtrl.js b/src/app/features/dashboard/cloneDashboardCtrl.js new file mode 100644 index 00000000000..855129ecaf9 --- /dev/null +++ b/src/app/features/dashboard/cloneDashboardCtrl.js @@ -0,0 +1,33 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('CloneDashboardCtrl', function($scope, datasourceSrv, $location) { + + $scope.init = function() { + $scope.db = datasourceSrv.getGrafanaDB(); + $scope.clone.id = null; + $scope.clone.editable = true; + $scope.clone.title = $scope.clone.title + " Copy"; + }; + + $scope.saveClone = function() { + $scope.db.saveDashboard($scope.clone) + .then(function(result) { + + $scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + result.title]); + $location.url(result.url); + $scope.appEvent('dashboard-saved', $scope.clone); + $scope.dismiss(); + + }, function(err) { + $scope.appEvent('alert-error', ['Save failed', err]); + }); + }; + }); + +}); diff --git a/src/app/features/dashboard/dashboardCtrl.js b/src/app/features/dashboard/dashboardCtrl.js index 712998c6a45..8ec13b24f2d 100644 --- a/src/app/features/dashboard/dashboardCtrl.js +++ b/src/app/features/dashboard/dashboardCtrl.js @@ -51,14 +51,18 @@ function (angular, $, config) { timeSrv.init($scope.dashboard); templateValuesSrv.init($scope.dashboard, $scope.dashboardViewState); - $scope.checkFeatureToggles(); dashboardKeybindings.shortcuts($scope); + $scope.updateSubmenuVisibility(); $scope.setWindowTitleAndTheme(); $scope.appEvent("dashboard-loaded", $scope.dashboard); }; + $scope.updateSubmenuVisibility = function() { + $scope.submenuEnabled = $scope.dashboard.hasTemplateVarsOrAnnotations(); + }; + $scope.setWindowTitleAndTheme = function() { window.document.title = config.window_title_prefix + $scope.dashboard.title; $scope.contextSrv.lightTheme = $scope.dashboard.style === 'light'; @@ -68,6 +72,10 @@ function (angular, $, config) { $scope.contextSrv.lightTheme = $scope.dashboard.style === 'light'; }; + $scope.broadcastRefresh = function() { + $rootScope.$broadcast('refresh'); + }; + $scope.add_row = function(dash, row) { dash.rows.push(row); }; @@ -101,10 +109,6 @@ function (angular, $, config) { $scope.appEvent('show-dash-editor', { src: 'app/partials/edit_json.html', scope: editScope }); }; - $scope.checkFeatureToggles = function() { - $scope.submenuEnabled = $scope.dashboard.templating.enable || $scope.dashboard.annotations.enable || false; - }; - $scope.onDrop = function(panelId, row, dropTarget) { var info = $scope.dashboard.getPanelInfoById(panelId); if (dropTarget) { diff --git a/src/app/features/dashboard/dashboardNavCtrl.js b/src/app/features/dashboard/dashboardNavCtrl.js index 4955c2e5185..bbe22fc0b67 100644 --- a/src/app/features/dashboard/dashboardNavCtrl.js +++ b/src/app/features/dashboard/dashboardNavCtrl.js @@ -65,27 +65,15 @@ function (angular, _, moment, config, store) { window.sessionStorage["grafanaAdminPassword"] = pwd; }; - $scope.isAdmin = function() { - if (!config.admin || !config.admin.password) { return true; } - if ($scope.passwordCache() === config.admin.password) { return true; } - - var password = window.prompt("Admin password", ""); - $scope.passwordCache(password); - - if (password === config.admin.password) { return true; } - - alertSrv.set('Save failed', 'Password incorrect', 'error'); - - return false; + $scope.openSearch = function() { + $scope.appEvent('show-dash-search'); }; - $scope.openSearch = function() { - $scope.appEvent('show-dash-editor', { src: 'app/partials/search.html', cssClass: 'search-container' }); + $scope.dashboardTitleAction = function() { + $scope.appEvent('hide-dash-editor'); }; $scope.saveDashboard = function() { - if (!$scope.isAdmin()) { return false; } - var clone = angular.copy($scope.dashboard); $scope.db.saveDashboard(clone) .then(function(result) { @@ -96,7 +84,7 @@ function (angular, _, moment, config, store) { $location.path(result.url); } - $rootScope.$emit('dashboard-saved', $scope.dashboard); + $scope.appEvent('dashboard-saved', $scope.dashboard); }, function(err) { $scope.appEvent('alert-error', ['Save failed', err]); @@ -104,8 +92,6 @@ function (angular, _, moment, config, store) { }; $scope.deleteDashboard = function() { - if (!$scope.isAdmin()) { return false; } - $scope.appEvent('confirm-modal', { title: 'Delete dashboard', text: 'Do you want to delete dashboard ' + $scope.dashboard.title + '?', @@ -123,6 +109,16 @@ function (angular, _, moment, config, store) { }); }; + $scope.cloneDashboard = function() { + var newScope = $rootScope.$new(); + newScope.clone = angular.copy($scope.dashboard); + + $scope.appEvent('show-modal', { + src: './app/features/dashboard/partials/cloneDashboard.html', + scope: newScope, + }); + }; + $scope.exportDashboard = function() { var blob = new Blob([angular.toJson($scope.dashboard, true)], { type: "application/json;charset=utf-8" }); window.saveAs(blob, $scope.dashboard.title + '-' + new Date().getTime()); diff --git a/src/app/features/dashboard/dashboardSrv.js b/src/app/features/dashboard/dashboardSrv.js index 94448265cca..ef33c211d04 100644 --- a/src/app/features/dashboard/dashboardSrv.js +++ b/src/app/features/dashboard/dashboardSrv.js @@ -10,7 +10,7 @@ function (angular, $, kbn, _, moment) { var module = angular.module('grafana.services'); - module.factory('dashboardSrv', function($rootScope) { + module.factory('dashboardSrv', function() { function DashboardModel (data) { @@ -91,6 +91,10 @@ function (angular, $, kbn, _, moment) { row.panels.push(panel); }; + p.hasTemplateVarsOrAnnotations = function() { + return this.templating.list.length > 0 || this.annotations.list.length > 0; + }; + p.getPanelInfoById = function(panelId) { var result = {}; _.each(this.rows, function(row) { @@ -128,10 +132,6 @@ function (angular, $, kbn, _, moment) { moment.utc(date).format(format); }; - p.emit_refresh = function() { - $rootScope.$broadcast('refresh'); - }; - p._updateSchema = function(old) { var i, j, k; var oldVersion = this.version; @@ -215,15 +215,11 @@ function (angular, $, kbn, _, moment) { if (oldVersion < 6) { // move pulldowns to new schema - var filtering = _.findWhere(old.pulldowns, { type: 'filtering' }); var annotations = _.findWhere(old.pulldowns, { type: 'annotations' }); - if (filtering) { - this.templating.enable = filtering.enable; - } + if (annotations) { this.annotations = { - list: annotations.annotations, - enable: annotations.enable + list: annotations.annotations || [], }; } diff --git a/src/app/features/dashboard/directives/dashSearchView.js b/src/app/features/dashboard/directives/dashSearchView.js new file mode 100644 index 00000000000..ad73b84043c --- /dev/null +++ b/src/app/features/dashboard/directives/dashSearchView.js @@ -0,0 +1,62 @@ +define([ + 'angular', + 'jquery' +], +function (angular, $) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('dashSearchView', function($compile, $timeout) { + return { + restrict: 'A', + link: function(scope, elem) { + var editorScope; + + function hookUpHideWhenClickedOutside() { + $timeout(function() { + $(document).bind('click.hide-search', function(evt) { + // some items can be inside container + // but then removed + if ($(evt.target).parents().length === 0) { + return; + } + + if ($(evt.target).parents('.search-container').length === 0) { + if (editorScope) { + editorScope.dismiss(); + } + } + }); + }); + } + + function showSearch() { + if (editorScope) { + editorScope.dismiss(); + return; + } + + editorScope = scope.$new(); + editorScope.dismiss = function() { + editorScope.$destroy(); + elem.empty(); + elem.unbind(); + editorScope = null; + $(document).unbind('click.hide-search'); + }; + + var view = $('
'); + + elem.append(view); + $compile(elem.contents())(editorScope); + + hookUpHideWhenClickedOutside(); + } + + scope.onAppEvent('show-dash-search', showSearch); + } + }; + }); + +}); diff --git a/src/app/features/dashboard/graphiteImportCtrl.js b/src/app/features/dashboard/graphiteImportCtrl.js new file mode 100644 index 00000000000..5a9205bd817 --- /dev/null +++ b/src/app/features/dashboard/graphiteImportCtrl.js @@ -0,0 +1,94 @@ +define([ + 'angular', + 'app', + 'lodash', + 'kbn' +], +function (angular, app, _, kbn) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('GraphiteImportCtrl', function($scope, datasourceSrv, dashboardSrv, $location) { + + $scope.init = function() { + $scope.datasources = []; + _.each(datasourceSrv.getAll(), function(ds) { + if (ds.type === 'graphite') { + $scope.sourceName = ds.name; + $scope.datasources.push(ds.name); + } + }); + }; + + $scope.listAll = function() { + $scope.datasource = datasourceSrv.get($scope.sourceName); + + $scope.datasource.listDashboards('').then(function(results) { + $scope.dashboards = results; + }, function(err) { + var message = err.message || err.statusText || 'Error'; + $scope.appEvent('alert-error', ['Failed to load dashboard list from graphite', message]); + }); + }; + + $scope.import = function(dashName) { + $scope.datasource.loadDashboard(dashName).then(function(results) { + if (!results.data || !results.data.state) { + throw { message: 'no dashboard state received from graphite' }; + } + + graphiteToGrafanaTranslator(results.data.state, $scope.datasource.name); + }, function(err) { + var message = err.message || err.statusText || 'Error'; + $scope.appEvent('alert-error', ['Failed to load dashboard from graphite', message]); + }); + }; + + function graphiteToGrafanaTranslator(state, datasource) { + var graphsPerRow = 2; + var rowHeight = 300; + var rowTemplate; + var currentRow; + var panel; + + rowTemplate = { + title: '', + panels: [], + height: rowHeight + }; + + currentRow = angular.copy(rowTemplate); + + var newDashboard = dashboardSrv.create({}); + newDashboard.rows = []; + newDashboard.title = state.name; + newDashboard.rows.push(currentRow); + + _.each(state.graphs, function(graph, index) { + if (currentRow.panels.length === graphsPerRow) { + currentRow = angular.copy(rowTemplate); + newDashboard.rows.push(currentRow); + } + + panel = { + type: 'graph', + span: 12 / graphsPerRow, + title: graph[1].title, + targets: [], + datasource: datasource, + id: index + 1 + }; + + _.each(graph[1].target, function(target) { + panel.targets.push({ target: target }); + }); + + currentRow.panels.push(panel); + }); + + window.grafanaImportDashboard = newDashboard; + $location.path('/dashboard/import/' + kbn.slugifyForUrl(newDashboard.title)); + } + }); +}); diff --git a/src/app/features/account/importCtrl.js b/src/app/features/dashboard/importCtrl.js similarity index 84% rename from src/app/features/account/importCtrl.js rename to src/app/features/dashboard/importCtrl.js index a8548491c60..8b3612ea70e 100644 --- a/src/app/features/account/importCtrl.js +++ b/src/app/features/dashboard/importCtrl.js @@ -7,7 +7,7 @@ function (angular, _) { var module = angular.module('grafana.controllers'); - module.controller('ImportCtrl', function($scope, $http, backendSrv, datasourceSrv) { + module.controller('DashboardImportCtrl', function($scope, $http, backendSrv, datasourceSrv) { $scope.init = function() { $scope.datasources = []; @@ -43,6 +43,10 @@ function (angular, _) { $scope.importing = true; $scope.imported = []; $scope.next(); + }, function(err) { + var resp = err.message || err.statusText || 'Unknown error'; + var message = "Failed to load dashboards from selected data source, response from server was: " + resp; + $scope.appEvent('alert-error', ['Import failed', message]); }); }; diff --git a/src/app/features/dashboard/keybindings.js b/src/app/features/dashboard/keybindings.js index c0cddc5b493..d558b554ebf 100644 --- a/src/app/features/dashboard/keybindings.js +++ b/src/app/features/dashboard/keybindings.js @@ -12,13 +12,7 @@ function(angular, $) { this.shortcuts = function(scope) { scope.$on('$destroy', function() { - keyboardManager.unbind('ctrl+f'); - keyboardManager.unbind('ctrl+h'); - keyboardManager.unbind('ctrl+s'); - keyboardManager.unbind('ctrl+r'); - keyboardManager.unbind('ctrl+z'); - keyboardManager.unbind('ctrl+o'); - keyboardManager.unbind('esc'); + keyboardManager.unbindAll(); }); var helpModalScope = null; @@ -40,13 +34,13 @@ function(angular, $) { }, { inputDisabled: true }); keyboardManager.bind('ctrl+f', function() { - scope.appEvent('show-dash-editor', { src: 'app/partials/search.html', cssClass: 'search-container' }); + scope.appEvent('show-dash-search'); }, { inputDisabled: true }); keyboardManager.bind('ctrl+o', function() { var current = scope.dashboard.sharedCrosshair; scope.dashboard.sharedCrosshair = !current; - scope.dashboard.emit_refresh('refresh'); + scope.broadcastRefresh(); }, { inputDisabled: true }); keyboardManager.bind('ctrl+l', function() { @@ -63,7 +57,7 @@ function(angular, $) { }, { inputDisabled: true }); keyboardManager.bind('ctrl+r', function() { - scope.dashboard.emit_refresh(); + scope.broadcastRefresh(); }, { inputDisabled: true }); keyboardManager.bind('ctrl+z', function(evt) { diff --git a/src/app/features/dashboard/partials/cloneDashboard.html b/src/app/features/dashboard/partials/cloneDashboard.html new file mode 100644 index 00000000000..dcbea9d1d00 --- /dev/null +++ b/src/app/features/dashboard/partials/cloneDashboard.html @@ -0,0 +1,26 @@ + + diff --git a/src/app/features/dashboard/partials/graphiteImport.html b/src/app/features/dashboard/partials/graphiteImport.html new file mode 100644 index 00000000000..0082fa92047 --- /dev/null +++ b/src/app/features/dashboard/partials/graphiteImport.html @@ -0,0 +1,34 @@ +
+ +
+

Load dashboard from Graphite-Web

+ +
+
    +
  • + Data source +
  • +
  • + +
  • +
  • + +
  • +
    +
+
+ +
IdName Login EmailNameAdminGrafana Admin
{{user.id}}{{user.name}} {{user.login}} {{user.email}}{{user.name}} {{user.isAdmin}} - + Edit    - +
+ + + + +
{{dash.name}} +
+ +
+
diff --git a/src/app/features/account/partials/import.html b/src/app/features/dashboard/partials/import.html similarity index 67% rename from src/app/features/account/partials/import.html rename to src/app/features/dashboard/partials/import.html index 39ec78f0032..af02069657a 100644 --- a/src/app/features/account/partials/import.html +++ b/src/app/features/dashboard/partials/import.html @@ -1,16 +1,29 @@ - +
-

Import dashboards

+

+ Import file + Load dashboard JSON layout from file +

-
+
+
+
+
+
+
+
+
+
+ +

Migrate dashboards

+ +
  • Dashboard source @@ -49,6 +62,9 @@
+ +
+
diff --git a/src/app/features/dashboard/playlistCtrl.js b/src/app/features/dashboard/playlistCtrl.js index 9e6a60013e2..50440c5912b 100644 --- a/src/app/features/dashboard/playlistCtrl.js +++ b/src/app/features/dashboard/playlistCtrl.js @@ -8,29 +8,47 @@ function (angular, _, config) { var module = angular.module('grafana.controllers'); - module.controller('PlaylistCtrl', function($scope, playlistSrv) { + module.controller('PlaylistCtrl', function($scope, playlistSrv, datasourceSrv) { $scope.init = function() { + $scope.playlist = []; $scope.timespan = config.playlist_timespan; - $scope.loadFavorites(); + $scope.db = datasourceSrv.getGrafanaDB(); + $scope.search(); }; - $scope.loadFavorites = function() { - $scope.favDashboards = playlistSrv.getFavorites().dashboards; + $scope.search = function() { + var query = {starred: true, limit: 10}; - _.each($scope.favDashboards, function(dashboard) { - dashboard.include = true; + if ($scope.searchQuery) { + query.query = $scope.searchQuery; + query.starred = false; + } + + $scope.db.searchDashboards(query).then(function(results) { + $scope.searchHits = results.dashboards; + $scope.filterHits(); }); }; - $scope.removeAsFavorite = function(dashboard) { - playlistSrv.removeAsFavorite(dashboard); - $scope.loadFavorites(); + $scope.filterHits = function() { + $scope.filteredHits = _.reject($scope.searchHits, function(dash) { + return _.findWhere($scope.playlist, {slug: dash.slug}); + }); + }; + + $scope.addDashboard = function(dashboard) { + $scope.playlist.push(dashboard); + $scope.filterHits(); + }; + + $scope.removeDashboard = function(dashboard) { + $scope.playlist = _.without($scope.playlist, dashboard); + $scope.filterHits(); }; $scope.start = function() { - var included = _.where($scope.favDashboards, { include: true }); - playlistSrv.start(included, $scope.timespan); + playlistSrv.start($scope.playlist, $scope.timespan); }; }); diff --git a/src/app/features/dashboard/playlistSrv.js b/src/app/features/dashboard/playlistSrv.js index b486e5957fc..2c10391124b 100644 --- a/src/app/features/dashboard/playlistSrv.js +++ b/src/app/features/dashboard/playlistSrv.js @@ -4,84 +4,51 @@ define([ 'kbn', 'store' ], -function (angular, _, kbn, store) { +function (angular, _, kbn) { 'use strict'; var module = angular.module('grafana.services'); - module.service('playlistSrv', function($location, $rootScope) { - var timerInstance; - var favorites = { dashboards: [] }; + module.service('playlistSrv', function($location, $rootScope, $timeout) { + var self = this; - this.init = function() { - var existingJson = store.get("grafana-favorites"); - if (existingJson) { - favorites = angular.fromJson(existingJson); - } + this.next = function() { + $timeout.cancel(self.cancelPromise); + + angular.element(window).unbind('resize'); + var dash = self.dashboards[self.index % self.dashboards.length]; + var relativeUrl = dash.url.substring($location.absUrl().length - $location.url().length); + + $location.url(relativeUrl); + self.index++; + + self.cancelPromise = $timeout(self.next, self.interval); }; - this._save = function() { - store.set('grafana-favorites', angular.toJson(favorites)); - }; - - this._find = function(title) { - return _.findWhere(favorites.dashboards, { title: title }); - }; - - this._remove = function(existing) { - if (existing) { - favorites.dashboards = _.without(favorites.dashboards, existing); - } - }; - - this.isCurrentFavorite = function(dashboard) { - return this._find(dashboard.title) ? true : false; - }; - - this.markAsFavorite = function(dashboard) { - var existing = this._find(dashboard.title); - this._remove(existing); - - favorites.dashboards.push({ - url: $location.path(), - title: dashboard.title - }); - - this._save(); - }; - - this.removeAsFavorite = function(toRemove) { - var existing = this._find(toRemove.title); - this._remove(existing); - this._save(); - }; - - this.getFavorites = function() { - return favorites; + this.prev = function() { + self.index = Math.max(self.index - 2, 0); + self.next(); }; this.start = function(dashboards, timespan) { - var interval = kbn.interval_to_ms(timespan); - var index = 0; + this.stop(); - $rootScope.playlist_active = true; + self.interval = kbn.interval_to_ms(timespan); + self.dashboards = dashboards; + $rootScope.playlistSrv = this; - timerInstance = setInterval(function() { - $rootScope.$apply(function() { - angular.element(window).unbind('resize'); - $location.search({}); - $location.path(dashboards[index % dashboards.length].url); - index++; - }); - }, interval); + self.cancelPromise = $timeout(self.next, self.interval); }; this.stop = function() { - clearInterval(timerInstance); - $rootScope.playlist_active = false; - }; + self.index = 0; - this.init(); + if (self.cancelPromise) { + $timeout.cancel(self.cancelPromise); + } + + $rootScope.playlistSrv = null; + }; }); diff --git a/src/app/features/dashboard/submenuCtrl.js b/src/app/features/dashboard/submenuCtrl.js index bf71377dd14..456cc1b76c1 100644 --- a/src/app/features/dashboard/submenuCtrl.js +++ b/src/app/features/dashboard/submenuCtrl.js @@ -18,6 +18,7 @@ function (angular, _) { $scope.panel = $scope.pulldown; $scope.row = $scope.pulldown; $scope.variables = $scope.dashboard.templating.list; + $scope.annotations = $scope.dashboard.templating.list; }; $scope.disableAnnotation = function (annotation) { diff --git a/src/app/features/dashboard/unsavedChangesSrv.js b/src/app/features/dashboard/unsavedChangesSrv.js index f1782a35ea3..a2e419720f4 100644 --- a/src/app/features/dashboard/unsavedChangesSrv.js +++ b/src/app/features/dashboard/unsavedChangesSrv.js @@ -88,8 +88,11 @@ function(angular, _, config) { _.each(current.templating.list, function(value, index) { value.current = null; value.options = null; - original.templating.list[index].current = null; - original.templating.list[index].options = null; + + if (original.templating.list.length > index) { + original.templating.list[index].current = null; + original.templating.list[index].options = null; + } }); var currentTimepicker = _.findWhere(current.nav, { type: 'timepicker' }); diff --git a/src/app/features/dashboard/viewStateSrv.js b/src/app/features/dashboard/viewStateSrv.js index 289701c05fb..08f651f8d5b 100644 --- a/src/app/features/dashboard/viewStateSrv.js +++ b/src/app/features/dashboard/viewStateSrv.js @@ -115,7 +115,7 @@ function (angular, _, $) { $timeout(function() { if (self.oldTimeRange !== self.fullscreenPanel.range) { - self.$scope.dashboard.emit_refresh(); + self.$scope.broadcastRefresh(); } else { self.fullscreenPanel.$broadcast('render'); diff --git a/src/app/features/elasticsearch/datasource.js b/src/app/features/elasticsearch/datasource.js index 0ca8ec853f1..d12cc5d113d 100644 --- a/src/app/features/elasticsearch/datasource.js +++ b/src/app/features/elasticsearch/datasource.js @@ -68,7 +68,7 @@ function (angular, _, config, kbn, moment) { var titleField = annotation.titleField || 'desc'; var textField = annotation.textField || null; - range[annotation.timeField]= { + range[timeField]= { from: rangeUnparsed.from, to: rangeUnparsed.to, }; diff --git a/src/app/features/grafanaDatasource/datasource.js b/src/app/features/grafanaDatasource/datasource.js index 7afee86096f..b6babaa7a93 100644 --- a/src/app/features/grafanaDatasource/datasource.js +++ b/src/app/features/grafanaDatasource/datasource.js @@ -45,11 +45,6 @@ function (angular, _, kbn) { }; GrafanaDatasource.prototype.saveDashboard = function(dashboard) { - // remove id if title has changed - if (dashboard.title !== dashboard.originalTitle) { - dashboard.id = null; - } - return backendSrv.post('/api/dashboards/db/', { dashboard: dashboard }) .then(function(data) { return { title: dashboard.title, url: '/dashboard/db/' + data.slug }; diff --git a/src/app/features/influxdb/datasource.js b/src/app/features/influxdb/datasource.js index c867fb2a4a5..e35405bfaf9 100644 --- a/src/app/features/influxdb/datasource.js +++ b/src/app/features/influxdb/datasource.js @@ -16,15 +16,15 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { function InfluxDatasource(datasource) { this.type = 'influxdb'; - this.urls = datasource.urls; + this.urls = _.map(datasource.url.split(','), function(url) { + return url.trim(); + }); + this.username = datasource.username; this.password = datasource.password; this.name = datasource.name; + this.database = datasource.database; this.basicAuth = datasource.basicAuth; - this.grafanaDB = datasource.grafanaDB; - - this.saveTemp = _.isUndefined(datasource.save_temp) ? true : datasource.save_temp; - this.saveTempTTL = _.isUndefined(datasource.save_temp_ttl) ? '30d' : datasource.save_temp_ttl; this.supportAnnotations = true; this.supportMetrics = true; @@ -53,7 +53,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { var alias = target.alias ? templateSrv.replace(target.alias) : ''; - var handleResponse = _.partial(handleInfluxQueryResponse, alias, queryBuilder.groupByField); + var handleResponse = _.partial(handleInfluxQueryResponse, alias); return this._seriesQuery(query).then(handleResponse); }, this); @@ -96,7 +96,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { query = '/' + query + '/'; } - return this._seriesQuery('list series ' + query).then(function(data) { + return this._seriesQuery('SHOW MEASUREMENTS').then(function(data) { if (!data || data.length === 0) { return []; } @@ -143,24 +143,28 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { } InfluxDatasource.prototype._seriesQuery = function(query) { - return this._influxRequest('GET', '/series', { + return this._influxRequest('GET', '/query', { q: query, }); }; InfluxDatasource.prototype._influxRequest = function(method, url, data) { - var _this = this; + var self = this; var deferred = $q.defer(); retry(deferred, function() { - var currentUrl = _this.urls.shift(); - _this.urls.push(currentUrl); + var currentUrl = self.urls.shift(); + self.urls.push(currentUrl); var params = { - u: _this.username, - p: _this.password, + u: self.username, + p: self.password, }; + if (self.database) { + params.db = self.database; + } + if (method === 'GET') { _.extend(params, data); data = null; @@ -171,12 +175,13 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { url: currentUrl + url, params: params, data: data, + precision: "ms", inspect: { type: 'influxdb' }, }; options.headers = options.headers || {}; - if (_this.basicAuth) { - options.headers.Authorization = 'Basic ' + _this.basicAuth; + if (self.basicAuth) { + options.headers.Authorization = 'Basic ' + self.basicAuth; } return $http(options).success(function (data) { @@ -187,184 +192,8 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { return deferred.promise; }; - InfluxDatasource.prototype.saveDashboard = function(dashboard) { - var tags = dashboard.tags.join(','); - var title = dashboard.title; - var temp = dashboard.temp; - var id = kbn.slugifyForUrl(title); - if (temp) { delete dashboard.temp; } - - var data = [{ - name: 'grafana.dashboard_' + btoa(id), - columns: ['time', 'sequence_number', 'title', 'tags', 'dashboard', 'id'], - points: [[1000000000000, 1, title, tags, angular.toJson(dashboard), id]] - }]; - - if (temp) { - return this._saveDashboardTemp(data, title, id); - } - else { - var self = this; - return this._influxRequest('POST', '/series', data).then(function() { - self._removeUnslugifiedDashboard(id, title, false); - return { title: title, url: '/dashboard/db/' + id }; - }, function(err) { - throw 'Failed to save dashboard to InfluxDB: ' + err.data; - }); - } - }; - - InfluxDatasource.prototype._removeUnslugifiedDashboard = function(id, title, isTemp) { - if (id === title) { return; } - - var self = this; - self._getDashboardInternal(title, isTemp).then(function(dashboard) { - if (dashboard !== null) { - self.deleteDashboard(title); - } - }); - }; - - InfluxDatasource.prototype._saveDashboardTemp = function(data, title, id) { - data[0].name = 'grafana.temp_dashboard_' + btoa(id); - data[0].columns.push('expires'); - data[0].points[0].push(this._getTempDashboardExpiresDate()); - - return this._influxRequest('POST', '/series', data).then(function() { - var baseUrl = window.location.href.replace(window.location.hash,''); - var url = baseUrl + "#dashboard/temp/" + id; - return { title: title, url: url }; - }, function(err) { - throw 'Failed to save shared dashboard to InfluxDB: ' + err.data; - }); - }; - - InfluxDatasource.prototype._getTempDashboardExpiresDate = function() { - var ttlLength = this.saveTempTTL.substring(0, this.saveTempTTL.length - 1); - var ttlTerm = this.saveTempTTL.substring(this.saveTempTTL.length - 1, this.saveTempTTL.length).toLowerCase(); - var expires = Date.now(); - switch(ttlTerm) { - case "m": - expires += ttlLength * 60000; - break; - case "d": - expires += ttlLength * 86400000; - break; - case "w": - expires += ttlLength * 604800000; - break; - default: - throw "Unknown ttl duration format"; - } - return expires; - }; - - InfluxDatasource.prototype._getDashboardInternal = function(id, isTemp) { - var queryString = 'select dashboard from "grafana.dashboard_' + btoa(id) + '"'; - - if (isTemp) { - queryString = 'select dashboard from "grafana.temp_dashboard_' + btoa(id) + '"'; - } - - return this._seriesQuery(queryString).then(function(results) { - if (!results || !results.length) { - return null; - } - - var dashCol = _.indexOf(results[0].columns, 'dashboard'); - var dashJson = results[0].points[0][dashCol]; - - return angular.fromJson(dashJson); - }, function() { - return null; - }); - }; - - InfluxDatasource.prototype.getDashboard = function(id, isTemp) { - var self = this; - return this._getDashboardInternal(id, isTemp).then(function(dashboard) { - if (dashboard !== null) { - return dashboard; - } - - // backward compatible load for unslugified ids - var slug = kbn.slugifyForUrl(id); - if (slug !== id) { - return self.getDashboard(slug, isTemp); - } - - throw "Dashboard not found"; - }, function(err) { - throw "Could not load dashboard, " + err.data; - }); - }; - - InfluxDatasource.prototype.deleteDashboard = function(id) { - return this._seriesQuery('drop series "grafana.dashboard_' + btoa(id) + '"').then(function(results) { - if (!results) { - throw "Could not delete dashboard"; - } - return id; - }, function(err) { - throw "Could not delete dashboard, " + err.data; - }); - }; - - InfluxDatasource.prototype.searchDashboards = function(queryString) { - var influxQuery = 'select * from /grafana.dashboard_.*/ where '; - - var tagsOnly = queryString.indexOf('tags!:') === 0; - if (tagsOnly) { - var tagsQuery = queryString.substring(6, queryString.length); - influxQuery = influxQuery + 'tags =~ /.*' + tagsQuery + '.*/i'; - } - else { - var titleOnly = queryString.indexOf('title:') === 0; - if (titleOnly) { - var titleQuery = queryString.substring(6, queryString.length); - influxQuery = influxQuery + ' title =~ /.*' + titleQuery + '.*/i'; - } - else { - influxQuery = influxQuery + '(tags =~ /.*' + queryString + '.*/i or title =~ /.*' + queryString + '.*/i)'; - } - } - - return this._seriesQuery(influxQuery).then(function(results) { - var hits = { dashboards: [], tags: [], tagsOnly: false }; - - if (!results || !results.length) { - return hits; - } - - for (var i = 0; i < results.length; i++) { - var dashCol = _.indexOf(results[i].columns, 'title'); - var tagsCol = _.indexOf(results[i].columns, 'tags'); - var idCol = _.indexOf(results[i].columns, 'id'); - - var hit = { - id: results[i].points[0][dashCol], - title: results[i].points[0][dashCol], - tags: results[i].points[0][tagsCol].split(",") - }; - - if (idCol !== -1) { - hit.id = results[i].points[0][idCol]; - } - - hit.tags = hit.tags[0] ? hit.tags : []; - hits.dashboards.push(hit); - } - return hits; - }); - }; - - function handleInfluxQueryResponse(alias, groupByField, seriesList) { - var influxSeries = new InfluxSeries({ - seriesList: seriesList, - alias: alias, - groupByField: groupByField - }); - + function handleInfluxQueryResponse(alias, seriesList) { + var influxSeries = new InfluxSeries({ seriesList: seriesList, alias: alias }); return influxSeries.getTimeSeries(); } @@ -382,7 +211,7 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { function getInfluxTime(date) { if (_.isString(date)) { - return date.replace('now', 'now()'); + return date.replace('now', 'now()').replace('-', ' - '); } return to_utc_epoch_seconds(date); diff --git a/src/app/features/influxdb/influxSeries.js b/src/app/features/influxdb/influxSeries.js index bca6ca7a865..9e28884547b 100644 --- a/src/app/features/influxdb/influxSeries.js +++ b/src/app/features/influxdb/influxSeries.js @@ -7,7 +7,6 @@ function (_) { function InfluxSeries(options) { this.seriesList = options.seriesList; this.alias = options.alias; - this.groupByField = options.groupByField; this.annotation = options.annotation; } @@ -16,51 +15,30 @@ function (_) { p.getTimeSeries = function() { var output = []; var self = this; - var i; + + console.log(self.seriesList); + if (!self.seriesList || !self.seriesList.results || !self.seriesList.results[0]) { + return output; + } + + this.seriesList = self.seriesList.results[0].series; _.each(self.seriesList, function(series) { - var seriesName; - var timeCol = series.columns.indexOf('time'); - var valueCol = 1; - var groupByCol = -1; - - if (self.groupByField) { - groupByCol = series.columns.indexOf(self.groupByField); + var datapoints = []; + for (var i = 0; i < series.values.length; i++) { + datapoints[i] = [series.values[i][1], new Date(series.values[i][0]).getTime()]; } - // find value column - _.each(series.columns, function(column, index) { - if (column !== 'time' && column !== 'sequence_number' && column !== self.groupByField) { - valueCol = index; - } + var seriesName = series.name; + var tags = _.map(series.tags, function(value, key) { + return key + ': ' + value; }); - var groups = {}; - - if (self.groupByField) { - groups = _.groupBy(series.points, function (point) { - return point[groupByCol]; - }); - } - else { - groups[series.columns[valueCol]] = series.points; + if (tags.length > 0) { + seriesName = seriesName + ' {' + tags.join(', ') + '}'; } - _.each(groups, function(groupPoints, key) { - var datapoints = []; - for (i = 0; i < groupPoints.length; i++) { - var metricValue = isNaN(groupPoints[i][valueCol]) ? null : groupPoints[i][valueCol]; - datapoints[i] = [metricValue, groupPoints[i][timeCol]]; - } - - seriesName = series.name + '.' + key; - - if (self.alias) { - seriesName = self.createNameForSeries(series.name, key); - } - - output.push({ target: seriesName, datapoints: datapoints }); - }); + output.push({ target: seriesName, datapoints: datapoints }); }); return output; diff --git a/src/app/features/influxdb/partials/query.editor.html b/src/app/features/influxdb/partials/query.editor.html index 72a6f59ecac..d3d7d0ff95d 100644 --- a/src/app/features/influxdb/partials/query.editor.html +++ b/src/app/features/influxdb/partials/query.editor.html @@ -11,8 +11,6 @@ diff --git a/src/app/features/influxdb/queryBuilder.js b/src/app/features/influxdb/queryBuilder.js index 5ee7746af6b..a08d9b5401a 100644 --- a/src/app/features/influxdb/queryBuilder.js +++ b/src/app/features/influxdb/queryBuilder.js @@ -52,15 +52,7 @@ function () { p._modifyRawQuery = function () { var query = this.target.query.replace(";", ""); - - var queryElements = query.split(" "); - var lowerCaseQueryElements = query.toLowerCase().split(" "); - - if (lowerCaseQueryElements[1].indexOf(',') !== -1) { - this.groupByField = lowerCaseQueryElements[1].replace(',', ''); - } - - return queryElements.join(" "); + return query; }; return InfluxQueryBuilder; diff --git a/src/app/features/influxdb/queryCtrl.js b/src/app/features/influxdb/queryCtrl.js index 89bd572f4e5..608b5845d88 100644 --- a/src/app/features/influxdb/queryCtrl.js +++ b/src/app/features/influxdb/queryCtrl.js @@ -30,7 +30,7 @@ function (angular, _) { delete target.groupby_field_add; } - $scope.rawQuery = false; + $scope.rawQuery = true; $scope.functions = [ 'count', 'mean', 'sum', 'min', diff --git a/src/app/features/influxdb_08/datasource.js b/src/app/features/influxdb_08/datasource.js new file mode 100644 index 00000000000..bc5c1ad0791 --- /dev/null +++ b/src/app/features/influxdb_08/datasource.js @@ -0,0 +1,401 @@ +define([ + 'angular', + 'lodash', + 'kbn', + './influxSeries', + './queryBuilder', + './queryCtrl', + './funcEditor', +], +function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { + 'use strict'; + + var module = angular.module('grafana.services'); + + module.factory('InfluxDatasource_08', function($q, $http, templateSrv) { + + function InfluxDatasource(datasource) { + this.type = 'influxdb_08'; + this.urls = _.map(datasource.url.split(','), function(url) { + return url.trim(); + }); + this.username = datasource.username; + this.password = datasource.password; + this.name = datasource.name; + this.basicAuth = datasource.basicAuth; + this.grafanaDB = datasource.grafanaDB; + + this.saveTemp = _.isUndefined(datasource.save_temp) ? true : datasource.save_temp; + this.saveTempTTL = _.isUndefined(datasource.save_temp_ttl) ? '30d' : datasource.save_temp_ttl; + + this.supportAnnotations = true; + this.supportMetrics = true; + this.editorSrc = 'app/features/influxdb_08/partials/query.editor.html'; + this.annotationEditorSrc = 'app/features/influxdb_08/partials/annotations.editor.html'; + } + + InfluxDatasource.prototype.query = function(options) { + var timeFilter = getTimeFilter(options); + + var promises = _.map(options.targets, function(target) { + if (target.hide || !((target.series && target.column) || target.query)) { + return []; + } + + // build query + var queryBuilder = new InfluxQueryBuilder(target); + var query = queryBuilder.build(); + + // replace grafana variables + query = query.replace('$timeFilter', timeFilter); + query = query.replace(/\$interval/g, (target.interval || options.interval)); + + // replace templated variables + query = templateSrv.replace(query); + + var alias = target.alias ? templateSrv.replace(target.alias) : ''; + + var handleResponse = _.partial(handleInfluxQueryResponse, alias, queryBuilder.groupByField); + return this._seriesQuery(query).then(handleResponse); + + }, this); + + return $q.all(promises).then(function(results) { + return { data: _.flatten(results) }; + }); + }; + + InfluxDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) { + var timeFilter = getTimeFilter({ range: rangeUnparsed }); + var query = annotation.query.replace('$timeFilter', timeFilter); + query = templateSrv.replace(query); + + return this._seriesQuery(query).then(function(results) { + return new InfluxSeries({ seriesList: results, annotation: annotation }).getAnnotations(); + }); + }; + + InfluxDatasource.prototype.listColumns = function(seriesName) { + seriesName = templateSrv.replace(seriesName); + + if(!seriesName.match('^/.*/') && !seriesName.match(/^merge\(.*\)/)) { + seriesName = '"' + seriesName+ '"'; + } + + return this._seriesQuery('select * from ' + seriesName + ' limit 1').then(function(data) { + if (!data) { + return []; + } + return data[0].columns.map(function(item) { + return /^\w+$/.test(item) ? item : ('"' + item + '"'); + }); + }); + }; + + InfluxDatasource.prototype.listSeries = function(query) { + // wrap in regex + if (query && query.length > 0 && query[0] !== '/') { + query = '/' + query + '/'; + } + + return this._seriesQuery('list series ' + query).then(function(data) { + if (!data || data.length === 0) { + return []; + } + return _.map(data[0].points, function(point) { + return point[1]; + }); + }); + }; + + InfluxDatasource.prototype.metricFindQuery = function (query) { + var interpolated; + try { + interpolated = templateSrv.replace(query); + } + catch (err) { + return $q.reject(err); + } + + return this._seriesQuery(interpolated) + .then(function (results) { + if (!results || results.length === 0) { return []; } + + return _.map(results[0].points, function (metric) { + return { + text: metric[1], + expandable: false + }; + }); + }); + }; + + function retry(deferred, callback, delay) { + return callback().then(undefined, function(reason) { + if (reason.status !== 0 || reason.status >= 300) { + reason.message = 'InfluxDB Error:
' + reason.data; + deferred.reject(reason); + } + else { + setTimeout(function() { + return retry(deferred, callback, Math.min(delay * 2, 30000)); + }, delay); + } + }); + } + + InfluxDatasource.prototype._seriesQuery = function(query) { + return this._influxRequest('GET', '/series', { + q: query, + }); + }; + + InfluxDatasource.prototype._influxRequest = function(method, url, data) { + var _this = this; + var deferred = $q.defer(); + + retry(deferred, function() { + var currentUrl = _this.urls.shift(); + _this.urls.push(currentUrl); + + var params = { + u: _this.username, + p: _this.password, + }; + + if (method === 'GET') { + _.extend(params, data); + data = null; + } + + var options = { + method: method, + url: currentUrl + url, + params: params, + data: data, + inspect: { type: 'influxdb' }, + }; + + options.headers = options.headers || {}; + if (_this.basicAuth) { + options.headers.Authorization = 'Basic ' + _this.basicAuth; + } + + return $http(options).success(function (data) { + deferred.resolve(data); + }); + }, 10); + + return deferred.promise; + }; + + InfluxDatasource.prototype.saveDashboard = function(dashboard) { + var tags = dashboard.tags.join(','); + var title = dashboard.title; + var temp = dashboard.temp; + var id = kbn.slugifyForUrl(title); + if (temp) { delete dashboard.temp; } + + var data = [{ + name: 'grafana.dashboard_' + btoa(id), + columns: ['time', 'sequence_number', 'title', 'tags', 'dashboard', 'id'], + points: [[1000000000000, 1, title, tags, angular.toJson(dashboard), id]] + }]; + + if (temp) { + return this._saveDashboardTemp(data, title, id); + } + else { + var self = this; + return this._influxRequest('POST', '/series', data).then(function() { + self._removeUnslugifiedDashboard(id, title, false); + return { title: title, url: '/dashboard/db/' + id }; + }, function(err) { + throw 'Failed to save dashboard to InfluxDB: ' + err.data; + }); + } + }; + + InfluxDatasource.prototype._removeUnslugifiedDashboard = function(id, title, isTemp) { + if (id === title) { return; } + + var self = this; + self._getDashboardInternal(title, isTemp).then(function(dashboard) { + if (dashboard !== null) { + self.deleteDashboard(title); + } + }); + }; + + InfluxDatasource.prototype._saveDashboardTemp = function(data, title, id) { + data[0].name = 'grafana.temp_dashboard_' + btoa(id); + data[0].columns.push('expires'); + data[0].points[0].push(this._getTempDashboardExpiresDate()); + + return this._influxRequest('POST', '/series', data).then(function() { + var baseUrl = window.location.href.replace(window.location.hash,''); + var url = baseUrl + "#dashboard/temp/" + id; + return { title: title, url: url }; + }, function(err) { + throw 'Failed to save shared dashboard to InfluxDB: ' + err.data; + }); + }; + + InfluxDatasource.prototype._getTempDashboardExpiresDate = function() { + var ttlLength = this.saveTempTTL.substring(0, this.saveTempTTL.length - 1); + var ttlTerm = this.saveTempTTL.substring(this.saveTempTTL.length - 1, this.saveTempTTL.length).toLowerCase(); + var expires = Date.now(); + switch(ttlTerm) { + case "m": + expires += ttlLength * 60000; + break; + case "d": + expires += ttlLength * 86400000; + break; + case "w": + expires += ttlLength * 604800000; + break; + default: + throw "Unknown ttl duration format"; + } + return expires; + }; + + InfluxDatasource.prototype._getDashboardInternal = function(id, isTemp) { + var queryString = 'select dashboard from "grafana.dashboard_' + btoa(id) + '"'; + + if (isTemp) { + queryString = 'select dashboard from "grafana.temp_dashboard_' + btoa(id) + '"'; + } + + return this._seriesQuery(queryString).then(function(results) { + if (!results || !results.length) { + return null; + } + + var dashCol = _.indexOf(results[0].columns, 'dashboard'); + var dashJson = results[0].points[0][dashCol]; + + return angular.fromJson(dashJson); + }, function() { + return null; + }); + }; + + InfluxDatasource.prototype.getDashboard = function(id, isTemp) { + var self = this; + return this._getDashboardInternal(id, isTemp).then(function(dashboard) { + if (dashboard !== null) { + return dashboard; + } + + // backward compatible load for unslugified ids + var slug = kbn.slugifyForUrl(id); + if (slug !== id) { + return self.getDashboard(slug, isTemp); + } + + throw "Dashboard not found"; + }, function(err) { + throw "Could not load dashboard, " + err.data; + }); + }; + + InfluxDatasource.prototype.deleteDashboard = function(id) { + return this._seriesQuery('drop series "grafana.dashboard_' + btoa(id) + '"').then(function(results) { + if (!results) { + throw "Could not delete dashboard"; + } + return id; + }, function(err) { + throw "Could not delete dashboard, " + err.data; + }); + }; + + InfluxDatasource.prototype.searchDashboards = function(queryString) { + var influxQuery = 'select * from /grafana.dashboard_.*/ where '; + + var tagsOnly = queryString.indexOf('tags!:') === 0; + if (tagsOnly) { + var tagsQuery = queryString.substring(6, queryString.length); + influxQuery = influxQuery + 'tags =~ /.*' + tagsQuery + '.*/i'; + } + else { + var titleOnly = queryString.indexOf('title:') === 0; + if (titleOnly) { + var titleQuery = queryString.substring(6, queryString.length); + influxQuery = influxQuery + ' title =~ /.*' + titleQuery + '.*/i'; + } + else { + influxQuery = influxQuery + '(tags =~ /.*' + queryString + '.*/i or title =~ /.*' + queryString + '.*/i)'; + } + } + + return this._seriesQuery(influxQuery).then(function(results) { + var hits = { dashboards: [], tags: [], tagsOnly: false }; + + if (!results || !results.length) { + return hits; + } + + for (var i = 0; i < results.length; i++) { + var dashCol = _.indexOf(results[i].columns, 'title'); + var tagsCol = _.indexOf(results[i].columns, 'tags'); + var idCol = _.indexOf(results[i].columns, 'id'); + + var hit = { + id: results[i].points[0][dashCol], + title: results[i].points[0][dashCol], + tags: results[i].points[0][tagsCol].split(",") + }; + + if (idCol !== -1) { + hit.id = results[i].points[0][idCol]; + } + + hit.tags = hit.tags[0] ? hit.tags : []; + hits.dashboards.push(hit); + } + return hits; + }); + }; + + function handleInfluxQueryResponse(alias, groupByField, seriesList) { + var influxSeries = new InfluxSeries({ + seriesList: seriesList, + alias: alias, + groupByField: groupByField + }); + + return influxSeries.getTimeSeries(); + } + + function getTimeFilter(options) { + var from = getInfluxTime(options.range.from); + var until = getInfluxTime(options.range.to); + var fromIsAbsolute = from[from.length-1] === 's'; + + if (until === 'now()' && !fromIsAbsolute) { + return 'time > ' + from; + } + + return 'time > ' + from + ' and time < ' + until; + } + + function getInfluxTime(date) { + if (_.isString(date)) { + return date.replace('now', 'now()'); + } + + return to_utc_epoch_seconds(date); + } + + function to_utc_epoch_seconds(date) { + return (date.getTime() / 1000).toFixed(0) + 's'; + } + + return InfluxDatasource; + + }); + +}); diff --git a/src/app/features/influxdb_08/funcEditor.js b/src/app/features/influxdb_08/funcEditor.js new file mode 100644 index 00000000000..615dcd2e75d --- /dev/null +++ b/src/app/features/influxdb_08/funcEditor.js @@ -0,0 +1,136 @@ +define([ + 'angular', + 'lodash', + 'jquery', +], +function (angular, _, $) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('influxdbFuncEditor', function($compile) { + + var funcSpanTemplate = '{{target.function}}('; + + var paramTemplate = ''; + + return { + restrict: 'A', + link: function postLink($scope, elem) { + var $funcLink = $(funcSpanTemplate); + + $scope.functionMenu = _.map($scope.functions, function(func) { + return { + text: func, + click: "changeFunction('" + func + "');" + }; + }); + + function clickFuncParam() { + /*jshint validthis:true */ + + var $link = $(this); + var $input = $link.next(); + + $input.val($scope.target.column); + $input.css('width', ($link.width() + 16) + 'px'); + + $link.hide(); + $input.show(); + $input.focus(); + $input.select(); + + var typeahead = $input.data('typeahead'); + if (typeahead) { + $input.val(''); + typeahead.lookup(); + } + } + + function inputBlur() { + /*jshint validthis:true */ + + var $input = $(this); + var $link = $input.prev(); + + if ($input.val() !== '') { + $link.text($input.val()); + + $scope.target.column = $input.val(); + $scope.$apply($scope.get_data); + } + + $input.hide(); + $link.show(); + } + + function inputKeyPress(e) { + /*jshint validthis:true */ + + if(e.which === 13) { + inputBlur.call(this); + } + } + + function inputKeyDown() { + /*jshint validthis:true */ + this.style.width = (3 + this.value.length) * 8 + 'px'; + } + + function addTypeahead($input) { + $input.attr('data-provide', 'typeahead'); + + $input.typeahead({ + source: function () { + return $scope.listColumns.apply(null, arguments); + }, + minLength: 0, + items: 20, + updater: function (value) { + setTimeout(function() { + inputBlur.call($input[0]); + }, 0); + return value; + } + }); + + var typeahead = $input.data('typeahead'); + typeahead.lookup = function () { + var items; + this.query = this.$element.val() || ''; + items = this.source(this.query, $.proxy(this.process, this)); + return items ? this.process(items) : items; + }; + } + + function addElementsAndCompile() { + $funcLink.appendTo(elem); + + var $paramLink = $('' + $scope.target.column + ''); + var $input = $(paramTemplate); + + $paramLink.appendTo(elem); + $input.appendTo(elem); + + $input.blur(inputBlur); + $input.keyup(inputKeyDown); + $input.keypress(inputKeyPress); + $paramLink.click(clickFuncParam); + + addTypeahead($input); + + $(')').appendTo(elem); + + $compile(elem.contents())($scope); + } + + addElementsAndCompile(); + + } + }; + + }); + +}); diff --git a/src/app/features/influxdb_08/influxSeries.js b/src/app/features/influxdb_08/influxSeries.js new file mode 100644 index 00000000000..bca6ca7a865 --- /dev/null +++ b/src/app/features/influxdb_08/influxSeries.js @@ -0,0 +1,129 @@ +define([ + 'lodash', +], +function (_) { + 'use strict'; + + function InfluxSeries(options) { + this.seriesList = options.seriesList; + this.alias = options.alias; + this.groupByField = options.groupByField; + this.annotation = options.annotation; + } + + var p = InfluxSeries.prototype; + + p.getTimeSeries = function() { + var output = []; + var self = this; + var i; + + _.each(self.seriesList, function(series) { + var seriesName; + var timeCol = series.columns.indexOf('time'); + var valueCol = 1; + var groupByCol = -1; + + if (self.groupByField) { + groupByCol = series.columns.indexOf(self.groupByField); + } + + // find value column + _.each(series.columns, function(column, index) { + if (column !== 'time' && column !== 'sequence_number' && column !== self.groupByField) { + valueCol = index; + } + }); + + var groups = {}; + + if (self.groupByField) { + groups = _.groupBy(series.points, function (point) { + return point[groupByCol]; + }); + } + else { + groups[series.columns[valueCol]] = series.points; + } + + _.each(groups, function(groupPoints, key) { + var datapoints = []; + for (i = 0; i < groupPoints.length; i++) { + var metricValue = isNaN(groupPoints[i][valueCol]) ? null : groupPoints[i][valueCol]; + datapoints[i] = [metricValue, groupPoints[i][timeCol]]; + } + + seriesName = series.name + '.' + key; + + if (self.alias) { + seriesName = self.createNameForSeries(series.name, key); + } + + output.push({ target: seriesName, datapoints: datapoints }); + }); + }); + + return output; + }; + + p.getAnnotations = function () { + var list = []; + var self = this; + + _.each(this.seriesList, function (series) { + var titleCol = null; + var timeCol = null; + var tagsCol = null; + var textCol = null; + + _.each(series.columns, function(column, index) { + if (column === 'time') { timeCol = index; return; } + if (column === 'sequence_number') { return; } + if (!titleCol) { titleCol = index; } + if (column === self.annotation.titleColumn) { titleCol = index; return; } + if (column === self.annotation.tagsColumn) { tagsCol = index; return; } + if (column === self.annotation.textColumn) { textCol = index; return; } + }); + + _.each(series.points, function (point) { + var data = { + annotation: self.annotation, + time: point[timeCol], + title: point[titleCol], + tags: point[tagsCol], + text: point[textCol] + }; + + if (tagsCol) { + data.tags = point[tagsCol]; + } + + list.push(data); + }); + }); + + return list; + }; + + p.createNameForSeries = function(seriesName, groupByColValue) { + var regex = /\$(\w+)/g; + var segments = seriesName.split('.'); + + return this.alias.replace(regex, function(match, group) { + if (group === 's') { + return seriesName; + } + else if (group === 'g') { + return groupByColValue; + } + var index = parseInt(group); + if (_.isNumber(index) && index < segments.length) { + return segments[index]; + } + return match; + }); + + }; + + return InfluxSeries; +}); diff --git a/src/app/features/influxdb_08/partials/annotations.editor.html b/src/app/features/influxdb_08/partials/annotations.editor.html new file mode 100644 index 00000000000..fe867d68f36 --- /dev/null +++ b/src/app/features/influxdb_08/partials/annotations.editor.html @@ -0,0 +1,29 @@ +
+
+
InfluxDB Query Example: select text from events where $timeFilter
+
+ +
+
+
+ +
+
+
Column mappings If your influxdb query returns more than one column you need to specify the column names bellow. An annotation event is composed of a title, tags, and an additional text field.
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ diff --git a/src/app/features/influxdb_08/partials/query.editor.html b/src/app/features/influxdb_08/partials/query.editor.html new file mode 100644 index 00000000000..72a6f59ecac --- /dev/null +++ b/src/app/features/influxdb_08/partials/query.editor.html @@ -0,0 +1,256 @@ +
+
+
+ + + + + +
    +
  • + +
  • +
+ + + + +
+
+ +
+ +
    +
  • + +
  • +
  • + alias +
  • +
  • + +
  • +
  • + group by time +
  • +
  • + +
  • +
+ + + +
+
+
+
+ +
+
+
    +
  • + +
  • +
  • + group by time +
  • +
  • + +
  • +
  • + +
  • +
+
+
+ + +
+ +
+
+ +
+
Alias patterns
+
    +
  • $s = series name
  • +
  • $g = group by
  • +
  • $[0-9] part of series name for series names seperated by dots.
  • +
+
+ +
+
Stacking and fill
+
    +
  • When stacking is enabled it important that points align
  • +
  • If there are missing points for one series it can cause gaps or missing bars
  • +
  • You must use fill(0), and select a group by time low limit
  • +
  • Use the group by time option below your queries and specify for example >10s if your metrics are written every 10 seconds
  • +
  • This will insert zeros for series that are missing measurements and will make stacking work properly
  • +
+
+ +
+
Group by time
+
    +
  • Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana
  • +
  • Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph
  • +
  • If you use fill(0) or fill(null) set a low limit for the auto group by time interval
  • +
  • The low limit can only be set in the group by time option below your queries
  • +
  • You set a low limit by adding a greater sign before the interval
  • +
  • Example: >60s if you write metrics to InfluxDB every 60 seconds
  • +
+
+ + +
+
+ + diff --git a/src/app/features/influxdb_08/queryBuilder.js b/src/app/features/influxdb_08/queryBuilder.js new file mode 100644 index 00000000000..5ee7746af6b --- /dev/null +++ b/src/app/features/influxdb_08/queryBuilder.js @@ -0,0 +1,67 @@ +define([ +], +function () { + 'use strict'; + + function InfluxQueryBuilder(target) { + this.target = target; + } + + var p = InfluxQueryBuilder.prototype; + + p.build = function() { + return this.target.rawQuery ? this._modifyRawQuery() : this._buildQuery(); + }; + + p._buildQuery = function() { + var target = this.target; + var query = 'select '; + var seriesName = target.series; + + if(!seriesName.match('^/.*/') && !seriesName.match(/^merge\(.*\)/)) { + seriesName = '"' + seriesName+ '"'; + } + + if (target.groupby_field) { + query += target.groupby_field + ', '; + } + + query += target.function + '(' + target.column + ')'; + query += ' from ' + seriesName + ' where $timeFilter'; + + if (target.condition) { + query += ' and ' + target.condition; + } + + query += ' group by time($interval)'; + + if (target.groupby_field) { + query += ', ' + target.groupby_field; + this.groupByField = target.groupby_field; + } + + if (target.fill) { + query += ' fill(' + target.fill + ')'; + } + + query += " order asc"; + target.query = query; + + return query; + }; + + p._modifyRawQuery = function () { + var query = this.target.query.replace(";", ""); + + var queryElements = query.split(" "); + var lowerCaseQueryElements = query.toLowerCase().split(" "); + + if (lowerCaseQueryElements[1].indexOf(',') !== -1) { + this.groupByField = lowerCaseQueryElements[1].replace(',', ''); + } + + return queryElements.join(" "); + }; + + return InfluxQueryBuilder; +}); diff --git a/src/app/features/influxdb_08/queryCtrl.js b/src/app/features/influxdb_08/queryCtrl.js new file mode 100644 index 00000000000..89bd572f4e5 --- /dev/null +++ b/src/app/features/influxdb_08/queryCtrl.js @@ -0,0 +1,110 @@ +define([ + 'angular', + 'lodash' +], +function (angular, _) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + var seriesList = null; + + module.controller('InfluxQueryCtrl', function($scope, $timeout) { + + $scope.init = function() { + var target = $scope.target; + + target.function = target.function || 'mean'; + target.column = target.column || 'value'; + + // backward compatible correction of schema + if (target.condition_value) { + target.condition = target.condition_key + ' ' + target.condition_op + ' ' + target.condition_value; + delete target.condition_key; + delete target.condition_op; + delete target.condition_value; + } + + if (target.groupby_field_add === false) { + target.groupby_field = ''; + delete target.groupby_field_add; + } + + $scope.rawQuery = false; + + $scope.functions = [ + 'count', 'mean', 'sum', 'min', + 'max', 'mode', 'distinct', 'median', + 'derivative', 'stddev', 'first', 'last', + 'difference' + ]; + + $scope.operators = ['=', '=~', '>', '<', '!~', '<>']; + $scope.oldSeries = target.series; + $scope.$on('typeahead-updated', function() { + $timeout($scope.get_data); + }); + }; + + $scope.showQuery = function () { + $scope.target.rawQuery = true; + }; + + $scope.hideQuery = function () { + $scope.target.rawQuery = false; + }; + + // Cannot use typeahead and ng-change on blur at the same time + $scope.seriesBlur = function() { + if ($scope.oldSeries !== $scope.target.series) { + $scope.oldSeries = $scope.target.series; + $scope.columnList = null; + $scope.get_data(); + } + }; + + $scope.changeFunction = function(func) { + $scope.target.function = func; + $scope.get_data(); + }; + + // called outside of digest + $scope.listColumns = function(query, callback) { + if (!$scope.columnList) { + $scope.$apply(function() { + $scope.datasource.listColumns($scope.target.series).then(function(columns) { + $scope.columnList = columns; + callback(columns); + }); + }); + } + else { + return $scope.columnList; + } + }; + + $scope.listSeries = function(query, callback) { + if (query !== '') { + seriesList = []; + $scope.datasource.listSeries(query).then(function(series) { + seriesList = series; + callback(seriesList); + }); + } + else { + return seriesList; + } + }; + + $scope.moveMetricQuery = function(fromIndex, toIndex) { + _.move($scope.panel.targets, fromIndex, toIndex); + }; + + $scope.duplicate = function() { + var clone = angular.copy($scope.target); + $scope.panel.targets.push(clone); + }; + + }); + +}); diff --git a/src/app/features/opentsdb/datasource.js b/src/app/features/opentsdb/datasource.js index d5bdd011dcf..3ade07a7aac 100644 --- a/src/app/features/opentsdb/datasource.js +++ b/src/app/features/opentsdb/datasource.js @@ -24,7 +24,13 @@ function (angular, _, kbn) { OpenTSDBDatasource.prototype.query = function(options) { var start = convertToTSDBTime(options.range.from); var end = convertToTSDBTime(options.range.to); - var queries = _.compact(_.map(options.targets, convertTargetToQuery)); + var qs = []; + + _.each(options.targets, function(target) { + qs.push(convertTargetToQuery(target, options.interval)); + }); + + var queries = _.compact(qs); // No valid targets, return the empty result to save a round trip. if (_.isEmpty(queries)) { @@ -119,8 +125,8 @@ function (angular, _, kbn) { return metric; } - function convertTargetToQuery(target) { - if (!target.metric) { + function convertTargetToQuery(target, interval) { + if (!target.metric || target.hide) { return null; } @@ -148,8 +154,14 @@ function (angular, _, kbn) { } } - if (target.shouldDownsample) { - query.downsample = templateSrv.replace(target.downsampleInterval) + "-" + target.downsampleAggregator; + if (!target.disableDownsampling) { + interval = templateSrv.replace(target.downsampleInterval || interval); + + if (interval.match(/\.[0-9]+s/)) { + interval = parseFloat(interval)*1000 + "ms"; + } + + query.downsample = interval + "-" + target.downsampleAggregator; } query.tags = angular.copy(target.tags); diff --git a/src/app/features/opentsdb/partials/query.editor.html b/src/app/features/opentsdb/partials/query.editor.html index 794d148676c..b732d999719 100644 --- a/src/app/features/opentsdb/partials/query.editor.html +++ b/src/app/features/opentsdb/partials/query.editor.html @@ -1,7 +1,7 @@
diff --git a/src/app/features/org/all.js b/src/app/features/org/all.js new file mode 100644 index 00000000000..ba51b99369b --- /dev/null +++ b/src/app/features/org/all.js @@ -0,0 +1,8 @@ +define([ + './datasourcesCtrl', + './datasourceEditCtrl', + './orgUsersCtrl', + './newOrgCtrl', + './orgApiKeysCtrl', + './orgDetailsCtrl', +], function () {}); diff --git a/src/app/features/org/datasourceEditCtrl.js b/src/app/features/org/datasourceEditCtrl.js new file mode 100644 index 00000000000..0ecf0f9d3d8 --- /dev/null +++ b/src/app/features/org/datasourceEditCtrl.js @@ -0,0 +1,76 @@ +define([ + 'angular', + 'lodash', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('DataSourceEditCtrl', function($scope, $http, backendSrv, $routeParams, $location, datasourceSrv) { + + var defaults = { + name: '', + type: 'graphite', + url: '', + access: 'proxy' + }; + + $scope.types = [ + { name: 'Graphite', type: 'graphite' }, + { name: 'InfluxDB 0.9.x (Experimental support)', type: 'influxdb' }, + { name: 'InfluxDB 0.8.x', type: 'influxdb_08' }, + { name: 'Elasticsearch', type: 'elasticsearch' }, + { name: 'OpenTSDB', type: 'opentsdb' }, + ]; + + $scope.init = function() { + $scope.isNew = true; + $scope.datasources = []; + + if ($routeParams.id) { + $scope.isNew = false; + $scope.getDatasourceById($routeParams.id); + } else { + $scope.current = angular.copy(defaults); + } + }; + + $scope.getDatasourceById = function(id) { + backendSrv.get('/api/datasources/' + id).then(function(ds) { + $scope.current = ds; + }); + }; + + $scope.updateFrontendSettings = function() { + backendSrv.get('/api/frontend/settings').then(function(settings) { + datasourceSrv.init(settings.datasources); + }); + }; + + $scope.update = function() { + if (!$scope.editForm.$valid) { + return; + } + + backendSrv.post('/api/datasources', $scope.current).then(function() { + $scope.updateFrontendSettings(); + $location.path("datasources"); + }); + }; + + $scope.add = function() { + if (!$scope.editForm.$valid) { + return; + } + + backendSrv.put('/api/datasources', $scope.current).then(function() { + $scope.updateFrontendSettings(); + $location.path("datasources"); + }); + }; + + $scope.init(); + + }); +}); diff --git a/src/app/features/org/datasourcesCtrl.js b/src/app/features/org/datasourcesCtrl.js new file mode 100644 index 00000000000..da77df252e1 --- /dev/null +++ b/src/app/features/org/datasourcesCtrl.js @@ -0,0 +1,36 @@ +define([ + 'angular', + 'lodash', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('DataSourcesCtrl', function($scope, $http, backendSrv, datasourceSrv) { + + $scope.init = function() { + $scope.datasources = []; + $scope.getDatasources(); + }; + + $scope.getDatasources = function() { + backendSrv.get('/api/datasources').then(function(results) { + $scope.datasources = results; + }); + }; + + $scope.remove = function(ds) { + backendSrv.delete('/api/datasources/' + ds.id).then(function() { + $scope.getDatasources(); + + backendSrv.get('/api/frontend/settings').then(function(settings) { + datasourceSrv.init(settings.datasources); + }); + }); + }; + + $scope.init(); + + }); +}); diff --git a/src/app/features/org/newOrgCtrl.js b/src/app/features/org/newOrgCtrl.js new file mode 100644 index 00000000000..2dbee087687 --- /dev/null +++ b/src/app/features/org/newOrgCtrl.js @@ -0,0 +1,18 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('NewOrgCtrl', function($scope, $http, backendSrv) { + + $scope.newOrg = {name: ''}; + + $scope.createOrg = function() { + backendSrv.post('/api/org/', $scope.newOrg).then($scope.getUserOrgs); + }; + + }); +}); diff --git a/src/app/features/account/apiKeysCtrl.js b/src/app/features/org/orgApiKeysCtrl.js similarity index 90% rename from src/app/features/account/apiKeysCtrl.js rename to src/app/features/org/orgApiKeysCtrl.js index f4ec0d681e8..fa77441d7b5 100644 --- a/src/app/features/account/apiKeysCtrl.js +++ b/src/app/features/org/orgApiKeysCtrl.js @@ -6,7 +6,7 @@ function (angular) { var module = angular.module('grafana.controllers'); - module.controller('ApiKeysCtrl', function($scope, $http, backendSrv) { + module.controller('OrgApiKeysCtrl', function($scope, $http, backendSrv) { $scope.roleTypes = ['Viewer', 'Editor', 'Admin']; $scope.token = { role: 'Viewer' }; diff --git a/src/app/features/org/orgDetailsCtrl.js b/src/app/features/org/orgDetailsCtrl.js new file mode 100644 index 00000000000..4a3c3dcc7e5 --- /dev/null +++ b/src/app/features/org/orgDetailsCtrl.js @@ -0,0 +1,29 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('OrgDetailsCtrl', function($scope, $http, backendSrv) { + + $scope.init = function() { + $scope.getOrgInfo(); + }; + + $scope.getOrgInfo = function() { + backendSrv.get('/api/org').then(function(org) { + $scope.org = org; + }); + }; + + $scope.update = function() { + if (!$scope.orgForm.$valid) { return; } + backendSrv.put('/api/org', $scope.org).then($scope.getOrgInfo); + }; + + $scope.init(); + + }); +}); diff --git a/src/app/features/account/accountUsersCtrl.js b/src/app/features/org/orgUsersCtrl.js similarity index 63% rename from src/app/features/account/accountUsersCtrl.js rename to src/app/features/org/orgUsersCtrl.js index 65ad8bcf847..d679a8dab98 100644 --- a/src/app/features/account/accountUsersCtrl.js +++ b/src/app/features/org/orgUsersCtrl.js @@ -6,7 +6,7 @@ function (angular) { var module = angular.module('grafana.controllers'); - module.controller('AccountUsersCtrl', function($scope, $http, backendSrv) { + module.controller('OrgUsersCtrl', function($scope, $http, backendSrv) { $scope.user = { loginOrEmail: '', @@ -18,18 +18,18 @@ function (angular) { }; $scope.get = function() { - backendSrv.get('/api/account/users').then(function(users) { + backendSrv.get('/api/org/users').then(function(users) { $scope.users = users; }); }; $scope.removeUser = function(user) { - backendSrv.delete('/api/account/users/' + user.userId).then($scope.get); + backendSrv.delete('/api/org/users/' + user.userId).then($scope.get); }; $scope.addUser = function() { if (!$scope.form.$valid) { return; } - backendSrv.post('/api/account/users', $scope.user).then($scope.get); + backendSrv.post('/api/org/users', $scope.user).then($scope.get); }; $scope.init(); diff --git a/src/app/features/org/partials/datasourceEdit.html b/src/app/features/org/partials/datasourceEdit.html new file mode 100644 index 00000000000..0809d5958aa --- /dev/null +++ b/src/app/features/org/partials/datasourceEdit.html @@ -0,0 +1,74 @@ + + + + +
+
+

Add data source

+

Edit {{current.name}}

+ +
+
+
+ + +
+
+ + +
+ +
+ +
+
+ + +
+
+ + +
+
+ +
+
+
InfluxDB Details
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
Elastic search details
+
+ + +
+
+
+ +
+ + + + Cancel +
+
+
+
diff --git a/src/app/features/org/partials/datasources.html b/src/app/features/org/partials/datasources.html new file mode 100644 index 00000000000..a2e62673e70 --- /dev/null +++ b/src/app/features/org/partials/datasources.html @@ -0,0 +1,52 @@ + + + + +
+
+

Data sources

+ +
+ No datasources defined +
+ + + + + + + + + + + + + + + + +
NameUrl
+   + {{ds.name}} + + {{ds.url}} + + + default + + + + + Edit + + + + + +
+ +
+
diff --git a/src/app/features/org/partials/newOrg.html b/src/app/features/org/partials/newOrg.html new file mode 100644 index 00000000000..56d08da701e --- /dev/null +++ b/src/app/features/org/partials/newOrg.html @@ -0,0 +1,34 @@ + + + + +
+
+ +

Add Organization

+ +
+
+
+
    +
  • + Org. name +
  • +
  • + +
  • +
  • +
  • +
+
+
+
+
+ +
+ +
+
+ diff --git a/src/app/features/org/partials/orgApiKeys.html b/src/app/features/org/partials/orgApiKeys.html new file mode 100644 index 00000000000..96763856dec --- /dev/null +++ b/src/app/features/org/partials/orgApiKeys.html @@ -0,0 +1,52 @@ + + + + +
+
+ +

+ API Keys +

+ +
+
    +
  • + Add a key +
  • +
  • + +
  • +
  • + Role +
  • +
  • + +
  • +
  • + +
  • +
    +
+
+
+ + + + + + + + +
{{t.name}}{{t.role}}{{t.key}} + + + +
+
+ +
+ + diff --git a/src/app/features/org/partials/orgDetails.html b/src/app/features/org/partials/orgDetails.html new file mode 100644 index 00000000000..e3a21706913 --- /dev/null +++ b/src/app/features/org/partials/orgDetails.html @@ -0,0 +1,67 @@ + + + + +
+
+ +

Organization info

+ +
+
+
+
    +
  • + Org. name +
  • +
  • + +
  • +
+
+
+
+
    +
  • + Address 1 +
  • +
  • + +
  • +
+
+
+
+
    +
  • + Address 2 +
  • +
  • + +
  • +
+
+
+
+
    +
  • + City +
  • +
  • + +
  • +
+
+
+ +
+
+ +
+ +
+
+ + diff --git a/src/app/features/account/partials/users.html b/src/app/features/org/partials/orgUsers.html similarity index 85% rename from src/app/features/account/partials/users.html rename to src/app/features/org/partials/orgUsers.html index 36eda3fe5f4..2e1100f5838 100644 --- a/src/app/features/account/partials/users.html +++ b/src/app/features/org/partials/orgUsers.html @@ -1,8 +1,6 @@ - + diff --git a/src/app/features/panel/partials/panelTime.html b/src/app/features/panel/partials/panelTime.html index 7b8b507a2ae..0c407cf2ff1 100644 --- a/src/app/features/panel/partials/panelTime.html +++ b/src/app/features/panel/partials/panelTime.html @@ -12,8 +12,8 @@ Last
  • -
  • @@ -31,8 +31,8 @@ Amount
  • -
  • diff --git a/src/app/features/profile/changePasswordCtrl.js b/src/app/features/profile/changePasswordCtrl.js new file mode 100644 index 00000000000..b2bb90ea6cf --- /dev/null +++ b/src/app/features/profile/changePasswordCtrl.js @@ -0,0 +1,28 @@ +define([ + 'angular', + 'config', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('ChangePasswordCtrl', function($scope, backendSrv, $location) { + + $scope.command = {}; + + $scope.changePassword = function() { + if (!$scope.userForm.$valid) { return; } + + if ($scope.command.newPassword !== $scope.command.confirmNew) { + $scope.appEvent('alert-warning', ['New passwords do not match', '']); + return; + } + + backendSrv.put('/api/user/password', $scope.command).then(function() { + $location.path("profile"); + }); + }; + + }); +}); diff --git a/src/app/features/profile/partials/password.html b/src/app/features/profile/partials/password.html new file mode 100644 index 00000000000..f7b5a18e5d0 --- /dev/null +++ b/src/app/features/profile/partials/password.html @@ -0,0 +1,56 @@ + + + + +
    +
    + +

    Change password

    + +
    +
    +
    +
      +
    • + Old Password +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + New Password +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + Confirm New +
    • +
    • + +
    • +
    +
    +
    +
    + +
    + +
    + +
    +
    + diff --git a/src/app/features/profile/partials/profile.html b/src/app/features/profile/partials/profile.html index 2a839db6e10..7e0351d4af1 100644 --- a/src/app/features/profile/partials/profile.html +++ b/src/app/features/profile/partials/profile.html @@ -1,6 +1,8 @@ - + +
    @@ -27,7 +29,7 @@ Email
  • - +
  • @@ -49,45 +51,23 @@ -

    Your accounts

    +

    Your Organizations

    - - - + + +
    Name: {{ac.name}}Role: {{ac.role}}
    Name: {{org.name}}Role: {{org.role}} - + Current - + Select
    -

    Add account

    - -
    -
    -
    -
      -
    • - Account name -
    • -
    • - -
    • -
    • -
    • -
    -
    -
    -
    -
    - -
    -
    diff --git a/src/app/features/profile/profileCtrl.js b/src/app/features/profile/profileCtrl.js index b028e48be1d..e63d8a89a07 100644 --- a/src/app/features/profile/profileCtrl.js +++ b/src/app/features/profile/profileCtrl.js @@ -1,18 +1,17 @@ define([ 'angular', + 'config', ], -function (angular) { +function (angular, config) { 'use strict'; var module = angular.module('grafana.controllers'); - module.controller('ProfileCtrl', function($scope, $http, backendSrv) { - - $scope.newAccount = {name: ''}; + module.controller('ProfileCtrl', function($scope, backendSrv) { $scope.init = function() { $scope.getUser(); - $scope.getUserAccounts(); + $scope.getUserOrgs(); }; $scope.getUser = function() { @@ -21,14 +20,16 @@ function (angular) { }); }; - $scope.getUserAccounts = function() { - backendSrv.get('/api/user/accounts').then(function(accounts) { - $scope.accounts = accounts; + $scope.getUserOrgs = function() { + backendSrv.get('/api/user/orgs').then(function(orgs) { + $scope.orgs = orgs; }); }; - $scope.setUsingAccount = function(account) { - backendSrv.post('/api/user/using/' + account.accountId).then($scope.getUserAccounts); + $scope.setUsingOrg = function(org) { + backendSrv.post('/api/user/using/' + org.orgId).then(function() { + window.location.href = config.appSubUrl + '/profile'; + }); }; $scope.update = function() { @@ -36,10 +37,6 @@ function (angular) { backendSrv.put('/api/user/', $scope.user); }; - $scope.createAccount = function() { - backendSrv.post('/api/account/', $scope.newAccount).then($scope.getUserAccounts); - }; - $scope.init(); }); diff --git a/src/app/features/templating/editorCtrl.js b/src/app/features/templating/editorCtrl.js index 51b5d7c9e38..f17c383155d 100644 --- a/src/app/features/templating/editorCtrl.js +++ b/src/app/features/templating/editorCtrl.js @@ -36,6 +36,7 @@ function (angular, _) { if ($scope.isValid()) { $scope.variables.push($scope.current); $scope.update(); + $scope.updateSubmenuVisibility(); } }; @@ -104,6 +105,7 @@ function (angular, _) { $scope.removeVariable = function(variable) { var index = _.indexOf($scope.variables, variable); $scope.variables.splice(index, 1); + $scope.updateSubmenuVisibility(); }; }); diff --git a/src/app/features/templating/templateSrv.js b/src/app/features/templating/templateSrv.js index c44ff0a5669..8fc45097944 100644 --- a/src/app/features/templating/templateSrv.js +++ b/src/app/features/templating/templateSrv.js @@ -45,6 +45,9 @@ function (angular, _) { }; this.containsVariable = function(str, variableName) { + if (!str) { + return false; + } return str.indexOf('$' + variableName) !== -1 || str.indexOf('[[' + variableName + ']]') !== -1; }; diff --git a/src/app/panels/dashlist/module.html b/src/app/panels/dashlist/module.html index c02e993984a..b59822e09a2 100644 --- a/src/app/panels/dashlist/module.html +++ b/src/app/panels/dashlist/module.html @@ -1,7 +1,7 @@
    - + {{dash.title}} diff --git a/src/app/panels/graph/axisEditor.html b/src/app/panels/graph/axisEditor.html index 34f624e28e4..c23daa77711 100644 --- a/src/app/panels/graph/axisEditor.html +++ b/src/app/panels/graph/axisEditor.html @@ -9,7 +9,7 @@
  • Unit
  • -
  • Unit
  • -
    diff --git a/src/app/panels/timepicker/editor.html b/src/app/panels/timepicker/editor.html index 33a5fbfaf00..34712ad582d 100644 --- a/src/app/panels/timepicker/editor.html +++ b/src/app/panels/timepicker/editor.html @@ -18,7 +18,10 @@
  • + ng-model="panel.nowDelay" placeholder="0m" + valid-time-span + bs-tooltip="'Enter 1m to ignore the last minute (because it can contain incomplete metrics)'" + data-placement="right">
  • diff --git a/src/app/partials/dashboard.html b/src/app/partials/dashboard.html index b4c0c517968..b599f1e211d 100644 --- a/src/app/partials/dashboard.html +++ b/src/app/partials/dashboard.html @@ -8,8 +8,8 @@
    -
    -
    +
    +
    - + +
    diff --git a/src/app/partials/dasheditor.html b/src/app/partials/dasheditor.html index acb1bc578b2..2dc2b18e523 100644 --- a/src/app/partials/dasheditor.html +++ b/src/app/partials/dasheditor.html @@ -5,7 +5,7 @@
    -
    +
    @@ -33,6 +33,8 @@
    + +
    @@ -68,26 +70,7 @@
    -
    -
    -
    - - -
    - - - -
    - -
    -
    -
    - -
    - -
    - -
    +
    diff --git a/src/app/partials/import.html b/src/app/partials/import.html deleted file mode 100644 index e89aff41342..00000000000 --- a/src/app/partials/import.html +++ /dev/null @@ -1,35 +0,0 @@ -
    -
    Import dashboards from graphite web
    - -
    -
    -
    - - - -
    - -
    -
    - -
    - - - - - -
    {{dash.name}} - - import - -
    -
    - -
    - {{error}} -
    -
    diff --git a/src/app/partials/playlist.html b/src/app/partials/playlist.html index c0b9f0545fb..38fd552d43b 100644 --- a/src/app/partials/playlist.html +++ b/src/app/partials/playlist.html @@ -12,56 +12,83 @@
    - -
    -
    -
    - - - - - - - - - - - - - - -
    DashboardIncludeRemove as favorite
    - {{dashboard.title}} - - - - - -
    - No dashboards marked as favorites -
    -
    -
    -
    - - dashboards available in the playlist are only the ones marked as favorites (stored in local browser storage). - to mark a dashboard as favorite, use save icon in the menu and in the dropdown select mark as favorite -

    -
    +
    +
    +
    +
    +
      +
    • + Search +
    • +
    • + +
    • +
    +
    -
    - - -
    +
    +
    Playlist dashboards
    +
    +
    +
    +
    + + + + + + + + +
    + {{dashboard.title}} + + +
    + No dashboards found +
    +
    +
    + + + + + +
    + {{dashboard.title}} + + +
    +

    -
    +
    +
    +
      +
    • + Timespan between dashboard change +
    • +
    • + +
    • +
    • + +
    • +
    +
    +
    +
    + +
    +
    diff --git a/src/app/partials/search.html b/src/app/partials/search.html index a595c2309f0..8320c9d19b7 100644 --- a/src/app/partials/search.html +++ b/src/app/partials/search.html @@ -7,11 +7,11 @@
    - + starred | - + tags @@ -62,26 +62,15 @@
    -
    -
    -
    -
    Local File Load dashboard JSON layout from file
    -
    -
    -
    -
    -
    -
    -
    - +