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\t\t\n\t\t\t\tGraphite introduction (TODO)\n\t\t\t \n\t\t\t\n\t\t\t\tBasic functions (TODO)\n\t\t\t \n\t\t\t\n\t\t\t\tAdvanced functions (TODO)\n\t\t\t \n\t\t\t\n\t\t\t\tTips and tricks (TODO)\n\t\t\t \n\t\t \n\t \n\t
\n\t\t
InfluxDB examples\n\t\t\n\t\t\t\n\t\t\t\tTODO\n\t\t\t \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:
'' +
'
' +
- '' +
@@ -41,7 +41,7 @@ function (angular) {
'
',
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
-
-
-
-
- API Keys
-
-
-
-
-
-
-
-
- {{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 @@
-
-
-
-
-
-
-
-
-
-
-
-
- {{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 @@
-
-
-
-
-
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
+
+
+
+
+
+ Change password
+
+
+
+
+
+ Permissions
+
+
+
+
+
Update
+
+
+
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
+
+
+
+
+
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
+
Id
+ Name
Login
Email
- Name
- Admin
+ Grafana Admin
{{user.id}}
+ {{user.name}}
{{user.login}}
{{user.email}}
- {{user.name}}
{{user.isAdmin}}
-
+
Edit
-
+
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 @@
-