Merge branch 'develop' into apikey_hashed

Conflicts:
	pkg/api/apikey.go
	pkg/services/sqlstore/migrations.go
This commit is contained in:
Torkel Ödegaard 2015-02-26 15:48:54 +01:00
commit 6a2a6afc1d
226 changed files with 6360 additions and 5676 deletions

3
.gitignore vendored
View File

@ -13,9 +13,12 @@ src/css/*.min.css
*.sublime-workspace
*.swp
.idea/
*.iml
/data/*
/bin/*
/grafana-pro
/grafana
grafana.custom.ini
fig.yml

View File

@ -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

View File

@ -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
```
@ -27,17 +33,12 @@ 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
```
For quicker builds:
```
godep restore (will pull down all golang lib dependecies in your current GOPATH)
go build -o ./bin/grafana .
go build .
```
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

4
benchmarks/ab/ab_test.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
ab -n 20000 -c 100 -H "Authorization: Bearer vEustw23NSOZ27y3zlj28ZL3B7BpBk5kqR85DOfT5AwiS3nCi33dnsk6nhvXhZdn" \
http://localhost:3000/api/dashboards/db/dash1

View File

@ -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

View File

@ -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"
}

View File

@ -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": "<h3 class=\"text-center\">Welcome to grafana demo, playground and interactive tutorial site.</h2>\n\n<div class=\"row-fluid\">\n\t<div class=\"span4\">\n\t\t<h4>Feature showcases</h2>\n\t\t<ul>\n\t\t\t<li>\n\t\t\t\t<a href=\"#/dashboard/file/graph-styles.json\">Graphs styles</a>\n\t\t\t</li>\n\t\t\t<li>\n\t\t\t\t<a href=\"#/dashboard/file/templated-graphs.json\">Templated graphs</a>\n\t\t\t</li>\n\t\t\t<li>\n\t\t\t\t<a href=\"#/dashboard/file/templated-graphs-nested.json\">Templated graphs nested</a>\n\t\t\t</li>\n\t\t\t<li>\n\t\t\t\t<a href=\"#/dashboard/file/annotations.json\">Annotations</a>\n\t\t\t</li>\n\t\t\t<li>\n\t\t\t\t<a href=\"#/dashboard/file/white-theme.json\">White theme</a>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n\t<div class=\"span4\">\n\t\t<h4>Graphite tutorials</h2>\n\t\t<ul>\n\t\t\t<li>\n\t\t\t\tGraphite introduction (TODO)\n\t\t\t</li>\n\t\t\t<li>\n\t\t\t\tBasic functions (TODO)\n\t\t\t</li>\n\t\t\t<li>\n\t\t\t\tAdvanced functions (TODO)\n\t\t\t</li>\n\t\t\t<li>\n\t\t\t\tTips and tricks (TODO)\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n\t<div class=\"span4\">\n\t\t<h4>InfluxDB examples</h2>\n\t\t<ul>\n\t\t\t<li>\n\t\t\t\tTODO\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</div>\n\n<script type=\"text/javascript\">(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');</script>",
"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"
}

File diff suppressed because one or more lines are too long

View File

@ -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"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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"
}

View File

@ -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"
}

File diff suppressed because one or more lines are too long

View File

@ -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"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

View File

@ -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"]

View File

@ -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

View File

@ -0,0 +1,6 @@
influxdb:
build: blocks/influxdb
ports:
- "2004:2004"
- "8083:8083"
- "8086:8086"

View File

@ -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"

View File

@ -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"]

View File

@ -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
```

15
docker/production/build.sh Executable file
View File

@ -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

View File

@ -0,0 +1,5 @@
#!/bin/bash
docker run -i -p 3001:3000 \
-e "GF_SERVER_ROOT_URL=http://grafana.server.name" \
grafana/grafana:develop

18
main.go
View File

@ -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()

View File

@ -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")
}

29
pkg/api/admin_settings.go Normal file
View File

@ -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)
}

View File

@ -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")
}

View File

@ -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

View File

@ -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")
}

View File

@ -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 {

View File

@ -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)
}
@ -39,7 +46,7 @@ func ProxyDataSourceRequest(c *middleware.Context) {
query := m.GetDataSourceByIdQuery{
Id: id,
AccountId: c.AccountId,
OrgId: c.OrgId,
}
err := bus.Dispatch(&query)

View File

@ -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}

View File

@ -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 {

View File

@ -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"`

22
pkg/api/dtos/user.go Normal file
View File

@ -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"`
}

View File

@ -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)
}

View File

@ -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,
}

50
pkg/api/org.go Normal file
View File

@ -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")
}

View File

@ -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")
}

View File

@ -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)

View File

@ -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{
cmd := m.SetUsingOrgCommand{
UserId: c.UserId,
AccountId: usingAccountId,
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")
}

13
pkg/cmd/common.go Normal file
View File

@ -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()
}

104
pkg/cmd/dashboard.go Normal file
View File

@ -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
}

230
pkg/cmd/datasource.go Normal file
View File

@ -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)
}

View File

@ -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
}

99
pkg/cmd/orgs.go Normal file
View File

@ -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)
}

View File

@ -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

View File

@ -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"`

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -9,7 +9,7 @@ var ErrInvalidApiKey = errors.New("Invalid API Key")
type ApiKey struct {
Id int64
AccountId int64
OrgId int64
Name string
Key string
Role RoleType
@ -22,7 +22,7 @@ type ApiKey struct {
type AddApiKeyCommand struct {
Name string `json:"name" binding:"Required"`
Role RoleType `json:"role" binding:"Required"`
AccountId int64 `json:"-"`
OrgId int64 `json:"-"`
Key string `json:"-"`
Result *ApiKey `json:"-"`
@ -33,19 +33,19 @@ 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:"-"`
OrgId int64 `json:"-"`
}
// ----------------------
// QUERIES
type GetApiKeysQuery struct {
AccountId int64
OrgId int64
Result []*ApiKey
}

View File

@ -16,7 +16,7 @@ var (
type Dashboard struct {
Id int64
Slug string
AccountId int64
OrgId int64
Version int
Created 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
OrgId int64
}
//
@ -96,7 +96,7 @@ type DeleteDashboardCommand struct {
type GetDashboardQuery struct {
Slug string
AccountId int64
OrgId int64
Result *Dashboard
}

View File

@ -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"
)
@ -23,7 +25,7 @@ type DsAccess string
type DataSource struct {
Id int64
AccountId int64
OrgId int64
Version int
Name string
@ -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
@ -76,20 +78,26 @@ type UpdateDataSourceCommand struct {
type DeleteDataSourceCommand struct {
Id int64
AccountId int64
OrgId int64
}
// ---------------------
// QUERIES
type GetDataSourcesQuery struct {
AccountId int64
OrgId int64
Result []*DataSource
}
type GetDataSourceByIdQuery struct {
Id int64
AccountId int64
OrgId int64
Result DataSource
}
type GetDataSourceByNameQuery struct {
Name string
OrgId int64
Result DataSource
}

65
pkg/models/org.go Normal file
View File

@ -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"`
}

67
pkg/models/org_user.go Normal file
View File

@ -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"`
}

View File

@ -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
OrgId int64
Result []*DashboardTagCloudItem
}

View File

@ -20,9 +20,11 @@ type User struct {
Salt string
Rands string
Company string
EmailVerified bool
Theme string
IsAdmin bool
AccountId int64
OrgId int64
Created time.Time
Updated time.Time
@ -50,9 +52,25 @@ type UpdateUserCommand struct {
UserId int64 `json:"-"`
}
type SetUsingAccountCommand struct {
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
AccountId 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
@ -100,6 +128,7 @@ type UserDTO struct {
Email string `json:"email"`
Name string `json:"name"`
Login string `json:"login"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
}
type UserSearchHitDTO struct {

View File

@ -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
})
}

View File

@ -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
})
}

View File

@ -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,7 +34,7 @@ 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,
OrgId: cmd.OrgId,
Name: cmd.Name,
Role: cmd.Role,
Key: cmd.Key,
@ -54,19 +54,19 @@ func UpdateApiKey(cmd *m.UpdateApiKeyCommand) error {
return inTransaction(func(sess *xorm.Session) error {
t := m.ApiKey{
Id: cmd.Id,
AccountId: cmd.AccountId,
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

View File

@ -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)
})
})
})
}

View File

@ -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
}

View File

@ -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,
@ -41,7 +41,7 @@ func TestDashboardDataAccess(t *testing.T) {
Convey("Should be able to get dashboard", func() {
query := m.GetDashboardQuery{
Slug: "test-dash-23",
AccountId: 1,
OrgId: 1,
}
err := GetDashboard(&query)
@ -54,7 +54,7 @@ func TestDashboardDataAccess(t *testing.T) {
Convey("Should be able to search for dashboard", func() {
query := m.SearchDashboardsQuery{
Title: "test",
AccountId: 1,
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)

View File

@ -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
}

View File

@ -42,7 +42,7 @@ func TestDataAccess(t *testing.T) {
Convey("Can add datasource", func() {
err := AddDataSource(&m.AddDataSourceCommand{
AccountId: 10,
OrgId: 10,
Type: m.DS_INFLUXDB,
Access: m.DS_ACCESS_DIRECT,
Url: "http://test",
@ -51,7 +51,7 @@ func TestDataAccess(t *testing.T) {
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,
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)

View File

@ -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())
}

View File

@ -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"))
}

View File

@ -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))
}

View File

@ -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"))
}

View File

@ -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"))
}

View File

@ -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]))
}

View File

@ -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)

View File

@ -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"))
}

View File

@ -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"))
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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

View File

@ -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))
}

View File

@ -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))
}

View File

@ -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"

View File

@ -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
})
}

View File

@ -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,
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)
})
})
})

View File

@ -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
})
}

View File

@ -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)

View File

@ -1,6 +1,7 @@
package sqlstore
import (
"fmt"
"strings"
"time"
@ -15,45 +16,50 @@ 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
}
@ -65,7 +71,7 @@ func CreateUser(cmd *m.CreateUserCommand) error {
Login: cmd.Login,
Company: cmd.Company,
IsAdmin: cmd.IsAdmin,
AccountId: accountId,
OrgId: orgId,
Created: time.Now(),
Updated: time.Now(),
}
@ -82,20 +88,20 @@ func CreateUser(cmd *m.CreateUserCommand) error {
return err
}
// create account user link
accountUser := m.AccountUser{
AccountId: accountId,
// 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
})
}

View File

@ -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
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"

View File

@ -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")
})
})
}

View File

@ -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' },
]
},
];

View File

@ -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);

View File

@ -3,7 +3,6 @@ define([
'./pulldown',
'./search',
'./metricKeys',
'./graphiteImport',
'./inspectCtrl',
'./jsonEditorCtrl',
'./loginCtrl',

View File

@ -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();
}
});
});

View File

@ -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,14 +65,22 @@ 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) {

View File

@ -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({
$scope.setupMainNav = function() {
$scope.mainLinks.push({
text: "Dashboards",
icon: "fa fa-th-large",
icon: "fa fa-fw fa-th-large",
href: $scope.getUrl("/"),
});
if (contextSrv.hasRole('Admin')) {
$scope.menu.push({
$scope.mainLinks.push({
text: "Data Sources",
icon: "fa fa-database",
href: $scope.getUrl("/account/datasources"),
icon: "fa fa-fw fa-database",
href: $scope.getUrl("/datasources"),
});
$scope.menu.push({
text: "Account", href: $scope.getUrl("/account"),
requireRole: "Admin",
icon: "fa fa-shield",
}
};
$scope.loadOrgs = function() {
$scope.orgMenu = [];
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 (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 ($scope.orgMenu.length > 0) {
$scope.orgMenu.push({ cssClass: 'divider' });
}
$scope.updateState = function() {
var currentPath = config.appSubUrl + $location.path();
var search = $location.search();
_.each($scope.menu, function(item) {
item.active = false;
if (item.href === currentPath) {
item.active = true;
backendSrv.get('/api/user/orgs').then(function(orgs) {
_.each(orgs, function(org) {
if (org.isUsing) {
return;
}
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);
}
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);
};
});

Some files were not shown because too many files have changed in this diff Show More