mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into develop
This commit is contained in:
@@ -1,13 +1,6 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -15,5 +8,12 @@ charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -41,9 +41,17 @@ profile.cov
|
||||
.notouch
|
||||
/pkg/cmd/grafana-cli/grafana-cli
|
||||
/pkg/cmd/grafana-server/grafana-server
|
||||
/pkg/cmd/grafana-server/debug
|
||||
/examples/*/dist
|
||||
/packaging/**/*.rpm
|
||||
/packaging/**/*.deb
|
||||
|
||||
# Ignore OSX indexing
|
||||
.DS_Store
|
||||
|
||||
/vendor/**/*.py
|
||||
/vendor/**/*.xml
|
||||
/vendor/**/*.yml
|
||||
/vendor/**/*_test.go
|
||||
/vendor/**/.editorconfig
|
||||
/vendor/**/appengine*
|
||||
|
||||
@@ -19,12 +19,19 @@
|
||||
* **Prometheus**: Add support for instant queries [#5765](https://github.com/grafana/grafana/issues/5765), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Cloudwatch**: Add support for alerting using the cloudwatch datasource [#8050](https://github.com/grafana/grafana/pull/8050), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Pagerduty**: Include triggering series in pagerduty notification [#8479](https://github.com/grafana/grafana/issues/8479), thx [@rickymoorhouse](https://github.com/rickymoorhouse)
|
||||
* **Timezone**: Time ranges like Today & Yesterday now work correctly when timezone setting is set to UTC [#8916](https://github.com/grafana/grafana/issues/8916), thx [@ctide](https://github.com/ctide)
|
||||
* **Prometheus**: Align $__interval with the step parameters. [#9226](https://github.com/grafana/grafana/pull/9226), thx [@alin-amana](https://github.com/alin-amana)
|
||||
* **Prometheus**: Autocomplete for label name and label value [#9208](https://github.com/grafana/grafana/pull/9208), thx [@mtanda](https://github.com/mtanda)
|
||||
|
||||
## Minor
|
||||
* **SMTP**: Make it possible to set specific EHLO for smtp client. [#9319](https://github.com/grafana/grafana/issues/9319)
|
||||
* **Dataproxy**: Allow grafan to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250)
|
||||
* **HTTP**: set net.Dialer.DualStack to true for all http clients [#9367](https://github.com/grafana/grafana/pull/9367)
|
||||
* **Alerting**: Add diff and percent diff as series reducers [#9386](https://github.com/grafana/grafana/pull/9386), thx [@shanhuhai5739](https://github.com/shanhuhai5739)
|
||||
* **Slack**: Allow images to be uploaded to slack when Token is precent [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8)
|
||||
* **Opsgenie**: Use their latest API instead of old version [#9399](https://github.com/grafana/grafana/pull/9399), thx [@cglrkn](https://github.com/cglrkn)
|
||||
* **Table**: Add support for displaying the timestamp with milliseconds [#9429](https://github.com/grafana/grafana/pull/9429), thx [@s1061123](https://github.com/s1061123)
|
||||
* **Hipchat**: Add metrics, message and image to hipchat notifications [#9110](https://github.com/grafana/grafana/issues/9110), thx [@eloo](https://github.com/eloo)
|
||||
|
||||
## Tech
|
||||
* **Go**: Grafana is now built using golang 1.9
|
||||
|
||||
@@ -45,7 +45,7 @@ yarn install --pure-lockfile
|
||||
npm run build
|
||||
```
|
||||
|
||||
To rebuild frontend assets (typesript, sass etc) as you change them start the watcher via.
|
||||
To rebuild frontend assets (typescript, sass etc) as you change them start the watcher via.
|
||||
|
||||
```bash
|
||||
npm run watch
|
||||
|
||||
@@ -17,7 +17,7 @@ But it will give you an idea of our current vision and plan.
|
||||
### Long term
|
||||
|
||||
- Backend plugins to support more Auth options, Alerting data sources & notifications
|
||||
- Universial time series transformations for any data source (meta queries)
|
||||
- Universal time series transformations for any data source (meta queries)
|
||||
- Reporting
|
||||
- Web socket & live data streams
|
||||
- Migrate to Angular2 or react
|
||||
|
||||
@@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana
|
||||
environment:
|
||||
nodejs_version: "6"
|
||||
GOPATH: c:\gopath
|
||||
GOVERSION: 1.9
|
||||
GOVERSION: 1.9.1
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
|
||||
@@ -9,7 +9,7 @@ machine:
|
||||
GOPATH: "/home/ubuntu/.go_workspace"
|
||||
ORG_PATH: "github.com/grafana"
|
||||
REPO_PATH: "${ORG_PATH}/grafana"
|
||||
GODIST: "go1.9.linux-amd64.tar.gz"
|
||||
GODIST: "go1.9.1.linux-amd64.tar.gz"
|
||||
post:
|
||||
- mkdir -p ~/download
|
||||
- mkdir -p ~/docker
|
||||
|
||||
@@ -479,6 +479,7 @@ provider =
|
||||
bucket_url =
|
||||
bucket =
|
||||
region =
|
||||
path =
|
||||
access_key =
|
||||
secret_key =
|
||||
|
||||
|
||||
@@ -424,6 +424,7 @@
|
||||
[external_image_storage.s3]
|
||||
;bucket =
|
||||
;region =
|
||||
;path =
|
||||
;access_key =
|
||||
;secret_key =
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ then there are two flags that can be used to set homepath and the config file pa
|
||||
|
||||
If you have not lost the admin password then it is better to set in the Grafana UI. If you need to set the password in a script then the [Grafana API](http://docs.grafana.org/http_api/user/#change-password) can be used. Here is an example with curl using basic auth:
|
||||
|
||||
```
|
||||
```bash
|
||||
curl -X PUT -H "Content-Type: application/json" -d '{
|
||||
"oldPassword": "admin",
|
||||
"newPassword": "newpass",
|
||||
|
||||
@@ -48,12 +48,15 @@ external image destination if available or fallback to attaching the image in th
|
||||
|
||||
To set up slack you need to configure an incoming webhook url at slack. You can follow their guide for how
|
||||
to do that https://api.slack.com/incoming-webhooks If you want to include screenshots of the firing alerts
|
||||
in the slack messages you have to configure the [external image destination](#external-image-store) in Grafana.
|
||||
in the slack messages you have to configure either the [external image destination](#external-image-store) in Grafana,
|
||||
or a bot integration via Slack Apps. Follow Slack's guide to set up a bot integration and use the token provided
|
||||
https://api.slack.com/bot-users, which starts with "xoxb".
|
||||
|
||||
Setting | Description
|
||||
---------- | -----------
|
||||
Recipient | allows you to override the slack recipient.
|
||||
Mention | make it possible to include a mention in the slack notification sent by Grafana. Ex @here or @channel
|
||||
Token | If provided, Grafana will upload the generated image via Slack's file.upload API method, not the external image destination.
|
||||
|
||||
### PagerDuty
|
||||
|
||||
|
||||
@@ -50,11 +50,12 @@ Create a file at `~/.aws/credentials`. That is the `HOME` path for user running
|
||||
|
||||
Example content:
|
||||
|
||||
```bash
|
||||
[default]
|
||||
aws_access_key_id = asdsadasdasdasd
|
||||
aws_secret_access_key = dasdasdsadasdasdasdsa
|
||||
region = us-west-2
|
||||
|
||||
```
|
||||
|
||||
## Metric Query Editor
|
||||
|
||||
@@ -117,7 +118,9 @@ Filters syntax:
|
||||
|
||||
Example `ec2_instance_attribute()` query
|
||||
|
||||
```javascript
|
||||
ec2_instance_attribute(us-east-1, InstanceId, { "tag:Environment": [ "production" ] })
|
||||
```
|
||||
|
||||
### Selecting Attributes
|
||||
|
||||
@@ -156,7 +159,9 @@ Tags can be selected by prepending the tag name with `Tags.`
|
||||
|
||||
Example `ec2_instance_attribute()` query
|
||||
|
||||
```javascript
|
||||
ec2_instance_attribute(us-east-1, Tags.Name, { "tag:Team": [ "sysops" ] })
|
||||
```
|
||||
|
||||
## Cost
|
||||
|
||||
|
||||
@@ -38,8 +38,10 @@ Proxy access means that the Grafana backend will proxy all requests from the bro
|
||||
If you select direct access you must update your Elasticsearch configuration to allow other domains to access
|
||||
Elasticsearch from the browser. You do this by specifying these to options in your **elasticsearch.yml** config file.
|
||||
|
||||
```bash
|
||||
http.cors.enabled: true
|
||||
http.cors.allow-origin: "*"
|
||||
```
|
||||
|
||||
### Index settings
|
||||
|
||||
|
||||
@@ -56,44 +56,46 @@ options.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v43/graph_axes_grid_options.png" max-width= "900px" >}}
|
||||
|
||||
The Axes tab controls the display of axes, grids and legend. The ``Left Y`` and ``Right Y`` can be customized using:
|
||||
The Axes tab controls the display of axes, grids and legend. The **Left Y** and **Right Y** can be customized using:
|
||||
|
||||
- ``Unit`` - The display unit for the Y value
|
||||
- ``Grid Max`` - The maximum Y value. (default auto)
|
||||
- ``Grid Min`` - The minimum Y value. (default auto)
|
||||
- ``Label`` - The Y axis label (default "")
|
||||
- **Unit** - The display unit for the Y value
|
||||
- **Scale** -
|
||||
- **Y-Min** - The minimum Y value. (default auto)
|
||||
- **Y-Max** - The maximum Y value. (default auto)
|
||||
- **Label** - The Y axis label (default "")
|
||||
|
||||
Axes can also be hidden by unchecking the appropriate box from `Show Axis`.
|
||||
Axes can also be hidden by unchecking the appropriate box from **Show**.
|
||||
|
||||
### X-Axis Mode
|
||||
|
||||
There are three options:
|
||||
|
||||
- The default option is `Time` and means the x-axis represents time and that the data is grouped by time (for example, by hour or by minute).
|
||||
- The default option is **Time** and means the x-axis represents time and that the data is grouped by time (for example, by hour or by minute).
|
||||
|
||||
- The `Series` option means that the data is grouped by series and not by time. The y-axis still represents the value.
|
||||
- The **Series** option means that the data is grouped by series and not by time. The y-axis still represents the value.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v45/graph-x-axis-mode-series.png" max-width="700px">}}
|
||||
|
||||
- The `Histogram` option converts the graph into a histogram. A Histogram is a kind of bar chart that groups numbers into ranges, often called buckets or bins. Taller bars show that more data falls in that range. Histograms and buckets are described in more detail [here](http://docs.grafana.org/features/panels/heatmap/#histograms-and-buckets).
|
||||
- The **Histogram** option converts the graph into a histogram. A Histogram is a kind of bar chart that groups numbers into ranges, often called buckets or bins. Taller bars show that more data falls in that range. Histograms and buckets are described in more detail [here](http://docs.grafana.org/features/panels/heatmap/#histograms-and-buckets).
|
||||
|
||||
<img src="/img/docs/v43/heatmap_histogram.png" class="no-shadow">
|
||||
|
||||
### Legend
|
||||
|
||||
The legend hand be hidden by checking the ``Show`` checkbox. If it's shown, it can be
|
||||
displayed as a table of values by checking the ``Table`` checkbox. Series with no
|
||||
values can be hidden from the legend using the ``Hide empty`` checkbox.
|
||||
The legend hand be hidden by checking the **Show** checkbox. If it's shown, it can be
|
||||
displayed as a table of values by checking the **Table** checkbox. Series with no
|
||||
values can be hidden from the legend using the **Hide empty** checkbox.
|
||||
|
||||
### Legend Values
|
||||
|
||||
Additional values can be shown along-side the legend names:
|
||||
- ``Total`` - Sum of all values returned from metric query
|
||||
- ``Current`` - Last value returned from the metric query
|
||||
- ``Min`` - Minimum of all values returned from metric query
|
||||
- ``Max`` - Maximum of all values returned from the metric query
|
||||
- ``Avg`` - Average of all values returned from metric query
|
||||
- ``Decimals`` - Controls how many decimals are displayed for legend values (and graph hover tooltips)
|
||||
|
||||
- **Total** - Sum of all values returned from metric query
|
||||
- **Current** - Last value returned from the metric query
|
||||
- **Min** - Minimum of all values returned from metric query
|
||||
- **Max** - Maximum of all values returned from the metric query
|
||||
- **Avg** - Average of all values returned from metric query
|
||||
- **Decimals** - Controls how many decimals are displayed for legend values (and graph hover tooltips)
|
||||
|
||||
The legend values are calculated client side by Grafana and depend on what type of
|
||||
aggregation or point consolidation your metric query is using. All the above legend values cannot
|
||||
@@ -115,23 +117,23 @@ the graph crosses a particular threshold.
|
||||
|
||||
### Chart Options
|
||||
|
||||
- ``Bar`` - Display values as a bar chart
|
||||
- ``Lines`` - Display values as a line graph
|
||||
- ``Points`` - Display points for values
|
||||
- **Bar** - Display values as a bar chart
|
||||
- **Lines** - Display values as a line graph
|
||||
- **Points** - Display points for values
|
||||
|
||||
### Line Options
|
||||
|
||||
- ``Line Fill`` - Amount of color fill for a series. 0 is none.
|
||||
- ``Line Width`` - The width of the line for a series.
|
||||
- ``Null point mode`` - How null values are displayed
|
||||
- ``Staircase line`` - Draws adjacent points as staircase
|
||||
- **Line Fill** - Amount of color fill for a series. 0 is none.
|
||||
- **Line Width** - The width of the line for a series.
|
||||
- **Null point mode** - How null values are displayed
|
||||
- **Staircase line** - Draws adjacent points as staircase
|
||||
|
||||
### Multiple Series
|
||||
|
||||
If there are multiple series, they can be displayed as a group.
|
||||
|
||||
- ``Stack`` - Each series is stacked on top of another
|
||||
- ``Percent`` - Each series is drawn as a percentage of the total of all series
|
||||
- **Stack** - Each series is stacked on top of another
|
||||
- **Percent** - Each series is drawn as a percentage of the total of all series
|
||||
|
||||
If you have stack enabled, you can select what the mouse hover feature should show.
|
||||
|
||||
@@ -140,12 +142,12 @@ If you have stack enabled, you can select what the mouse hover feature should sh
|
||||
|
||||
### Rendering
|
||||
|
||||
- ``Flot`` - Render the graphs in the browser using Flot (default)
|
||||
- ``Graphite PNG`` - Render the graph on the server using graphite's render API.
|
||||
- **Flot** - Render the graphs in the browser using Flot (default)
|
||||
- **Graphite PNG** - Render the graph on the server using graphite's render API.
|
||||
|
||||
### Tooltip
|
||||
|
||||
- ``All series`` - Show all series on the same tooltip and a x crosshairs to help follow all series
|
||||
- **All series** - Show all series on the same tooltip and a x crosshairs to help follow all series
|
||||
|
||||
### Series Specific Overrides
|
||||
|
||||
@@ -158,12 +160,6 @@ There is an option under Series overrides to draw lines as dashes. Set Dashes to
|
||||
|
||||
## Time Range
|
||||
|
||||
<<<<<<< HEAD
|
||||
The time range tab allows you to override the dashboard time range and specify a panel specific time. Either through a relative from now time option or through a timeshift.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v45/graph-time-range.png" max-width= "900px" >}}
|
||||
||||||| merged common ancestors
|
||||

|
||||
=======
|
||||
<img src="/img/docs/v45/graph-time-range.png" class="no-shadow">
|
||||
>>>>>>> 0a65100eaf64cd57b38110001bf614630821610c
|
||||
|
||||
@@ -22,20 +22,20 @@ The singlestat panel has a normal query editor to allow you define your exact me
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v45/singlestat-value-options.png" class="docs-image--no-shadow" max-width="900px" >}}
|
||||
|
||||
1. `Stats`: The Stats field let you set the function (min, max, average, current, total, first, delta, range) that your entire query is reduced into a single value with. This reduces the entire query into a single summary value that is displayed.
|
||||
* `min` - The smallest value in the series
|
||||
* `max` - The largest value in the series
|
||||
* `avg` - The average of all the non-null values in the series
|
||||
* `current` - The last value in the series. If the series ends on null the previous value will be used.
|
||||
* `total` - The sum of all the non-null values in the series
|
||||
* `first` - The first value in the series
|
||||
* `delta` - The total incremental increase (of a counter) in the series. An attempt is made to account for counter resets, but this will only be accurate for single instance metrics. Used to show total counter increase in time series.
|
||||
* `diff` - The difference betwen 'current' (last value) and 'first'.
|
||||
* `range` - The difference between 'min' and 'max'. Useful the show the range of change for a gauge.
|
||||
2. `Prefix/Postfix`: The Prefix/Postfix fields let you define a custom label to appear *before/after* the value. The `$__name` variable can be used here to use the series name or alias from the metric query.
|
||||
3. `Units`: Units are appended to the the Singlestat within the panel, and will respect the color and threshold settings for the value.
|
||||
4. `Decimals`: The Decimal field allows you to override the automatic decimal precision, and set it explicitly.
|
||||
5. `Font Size`: You can use this section to select the font size of the different texts in the Singlestat Panel, i.e. prefix, value and postfix.
|
||||
1. **Stats**: The Stats field let you set the function (min, max, average, current, total, first, delta, range) that your entire query is reduced into a single value with. This reduces the entire query into a single summary value that is displayed.
|
||||
* **min** - The smallest value in the series
|
||||
* **max** - The largest value in the series
|
||||
* **avg** - The average of all the non-null values in the series
|
||||
* **current** - The last value in the series. If the series ends on null the previous value will be used.
|
||||
* **total** - The sum of all the non-null values in the series
|
||||
* **first** - The first value in the series
|
||||
* **delta** - The total incremental increase (of a counter) in the series. An attempt is made to account for counter resets, but this will only be accurate for single instance metrics. Used to show total counter increase in time series.
|
||||
* **diff** - The difference betwen 'current' (last value) and 'first'.
|
||||
* **range** - The difference between 'min' and 'max'. Useful the show the range of change for a gauge.
|
||||
2. **Prefix/Postfix**: The Prefix/Postfix fields let you define a custom label to appear *before/after* the value. The `$__name` variable can be used here to use the series name or alias from the metric query.
|
||||
3. **Units**: Units are appended to the the Singlestat within the panel, and will respect the color and threshold settings for the value.
|
||||
4. **Decimals**: The Decimal field allows you to override the automatic decimal precision, and set it explicitly.
|
||||
5. **Font Size**: You can use this section to select the font size of the different texts in the Singlestat Panel, i.e. prefix, value and postfix.
|
||||
|
||||
### Coloring
|
||||
|
||||
@@ -43,11 +43,11 @@ The coloring options of the Singlestat Panel config allow you to dynamically cha
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v45/singlestat-color-options.png" max-width="500px" class="docs-image--right docs-image--no-shadow">}}
|
||||
|
||||
1. `Background`: This checkbox applies the configured thresholds and colors to the entirety of the Singlestat Panel background.
|
||||
2. `Thresholds`: Change the background and value colors dynamically within the panel, depending on the Singlestat value. The threshold field accepts **2 comma-separated** values which represent 3 ranges that correspond to the three colors directly to the right. For example: if the thresholds are 70, 90 then the first color represents < 70, the second color represents between 70 and 90 and the third color represents > 90.
|
||||
3. `Colors`: Select a color and opacity
|
||||
4. `Value`: This checkbox applies the configured thresholds and colors to the summary stat.
|
||||
5. `Invert order`: This link toggles the threshold color order.</br>For example: Green, Orange, Red (<img class="no-shadow" src="/img/docs(v1/gyr.png">) will become Red, Orange, Green (<img class="no-shadow" src="/img/docs/v1/ryg.png">).
|
||||
1. **Background**: This checkbox applies the configured thresholds and colors to the entirety of the Singlestat Panel background.
|
||||
2. **Thresholds**: Change the background and value colors dynamically within the panel, depending on the Singlestat value. The threshold field accepts **2 comma-separated** values which represent 3 ranges that correspond to the three colors directly to the right. For example: if the thresholds are 70, 90 then the first color represents < 70, the second color represents between 70 and 90 and the third color represents > 90.
|
||||
3. **Colors**: Select a color and opacity
|
||||
4. **Value**: This checkbox applies the configured thresholds and colors to the summary stat.
|
||||
5. **Invert order**: This link toggles the threshold color order.</br>For example: Green, Orange, Red (<img class="no-shadow" src="/img/docs(v1/gyr.png">) will become Red, Orange, Green (<img class="no-shadow" src="/img/docs/v1/ryg.png">).
|
||||
|
||||
### Spark Lines
|
||||
|
||||
@@ -55,62 +55,35 @@ Sparklines are a great way of seeing the historical data related to the summary
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v45/singlestat-spark-options.png" max-width="500px" class="docs-image--right docs-image--no-shadow">}}
|
||||
|
||||
1. `Show`: The show checkbox will toggle whether the spark line is shown in the Panel. When unselected, only the Singlestat value will appear.
|
||||
2. `Full Height`: Check if you want the sparklines to take up the full panel height, or uncheck if they should be below the main Singlestat value.
|
||||
3. `Line Color`: This color selection applies to the color of the sparkline itself.
|
||||
4. `Fill Color`: This color selection applies to the area below the sparkline.
|
||||
1. **Show**: The show checkbox will toggle whether the spark line is shown in the Panel. When unselected, only the Singlestat value will appear.
|
||||
2. **Full Height**: Check if you want the sparklines to take up the full panel height, or uncheck if they should be below the main Singlestat value.
|
||||
3. **Line Color**: This color selection applies to the color of the sparkline itself.
|
||||
4. **Fill Color**: This color selection applies to the area below the sparkline.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
> ***Pro-tip:*** Reduce the opacity on fill colors for nice looking panels.
|
||||
|
||||
<<<<<<< HEAD
|
||||
### Gauge
|
||||
|
||||
Gauges gives a clear picture of how high a value is in it's context. It's a great way to see if a value is close to the thresholds. The gauge uses the colors set in the color options.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v45/singlestat-gauge-options.png" max-width="500px" class="docs-image--right docs-image--no-shadow">}}
|
||||
|
||||
1. `Show`: The show checkbox will toggle wether the gauge is shown in the panel. When unselected, only the Singlestat value will appear.
|
||||
2. `Min/Max`: This sets the start and end point for the gauge.
|
||||
3. `Threshold Labels`: Check if you want to show the threshold labels. Thresholds are set in the color options.
|
||||
4. `Threshold Markers`: Check if you want to have a second meter showing the thresholds.
|
||||
1. **Show**: The show checkbox will toggle wether the gauge is shown in the panel. When unselected, only the Singlestat value will appear.
|
||||
2. **Min/Max**: This sets the start and end point for the gauge.
|
||||
3. **Threshold Labels**: Check if you want to show the threshold labels. Thresholds are set in the color options.
|
||||
4. **Threshold Markers**: Check if you want to have a second meter showing the thresholds.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
||||||| merged common ancestors
|
||||
=======
|
||||
### Gauge
|
||||
|
||||
Gauges gives a clear picture of how high a value is in it's context. It's a great way to see if a value is close to the thresholds. The gauge uses the colors set in the color options.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v45/singlestat-gauge-options.png" max-width="500px" class="docs-image--right docs-image--no-shadow">}}
|
||||
|
||||
1. `Show`: The show checkbox will toggle wether the gauge is shown in the panel. When unselected, only the Singlestat value will appear.
|
||||
2. `Min/Max`: This sets the start and end point for the gauge.
|
||||
3. `Threshold Labels`: Check if you want to show the threshold labels. Thresholds are set in the color options.
|
||||
4. `Threshold Markers`: Check if you want to have a second meter showing the thresholds.
|
||||
|
||||
>>>>>>> 0a65100eaf64cd57b38110001bf614630821610c
|
||||
### Value to text mapping
|
||||
|
||||
<<<<<<< HEAD
|
||||
{{< docs-imagebox img="/img/docs/v45/singlestat-value-mapping.png" class="docs-image--right docs-image--no-shadow">}}
|
||||
|
||||
Value to text mapping allows you to translate the value of the summary stat into explicit text. The text will respect all styling, thresholds and customization defined for the value. This can be useful to translate the number of the main Singlestat value into a context-specific human-readable word or message.
|
||||
||||||| merged common ancestors
|
||||
Value to text mapping allows you to translate the value of the summary stat into explicit text. The text will respect all styling, thresholds and customization defined for the value. This can be useful to translate the number of the main Singlestat value into a context-specific human-readable word or message.
|
||||
=======
|
||||
{{< docs-imagebox img="/img/docs/v45/singlestat-value-mapping.png" class="docs-image--right docs-image--no-shadow">}}
|
||||
>>>>>>> 0a65100eaf64cd57b38110001bf614630821610c
|
||||
|
||||
<<<<<<< HEAD
|
||||
<div class="clearfix"></div>
|
||||
||||||| merged common ancestors
|
||||
<img class="no-shadow" src="/img/docs/v1/Singlestat-ValueMapping.png">
|
||||
=======
|
||||
Value to text mapping allows you to translate the value of the summary stat into explicit text. The text will respect all styling, thresholds and customization defined for the value. This can be useful to translate the number of the main Singlestat value into a context-specific human-readable word or message.
|
||||
>>>>>>> 0a65100eaf64cd57b38110001bf614630821610c
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ The table panel has many ways to manipulate your data for optimal presentation.
|
||||
{{< docs-imagebox img="/img/docs/v45/table_options.png" class="docs-image--no-shadow" max-width= "500px" >}}
|
||||
|
||||
|
||||
1. `Data`: Control how your query is transformed into a table.
|
||||
2. `Paging`: Table display options.
|
||||
1. **Data**: Control how your query is transformed into a table.
|
||||
2. **Paging**: Table display options.
|
||||
|
||||
|
||||
## Data to Table
|
||||
@@ -43,20 +43,20 @@ you want in the table. Only applicable for some transforms.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v45/table_ts_to_rows.png" >}}
|
||||
|
||||
In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column. Where `Metric` is the name of the time series.
|
||||
In the most simple mode you can turn time series to rows. This means you get a **Time**, **Metric** and a **Value** column. Where **Metric** is the name of the time series.
|
||||
|
||||
### Time series to columns
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v45/table_ts_to_columns.png" >}}
|
||||
|
||||
|
||||
This transform allows you to take multiple time series and group them by time. Which will result in the primary column being `Time` and a column for each time series.
|
||||
This transform allows you to take multiple time series and group them by time. Which will result in the primary column being **Time** and a column for each time series.
|
||||
|
||||
### Time series aggregations
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v45/table_ts_to_aggregations.png" >}}
|
||||
|
||||
This table transformation will lay out your table into rows by metric, allowing columns of `Avg`, `Min`, `Max`, `Total`, `Current` and `Count`. More than one column can be added.
|
||||
This table transformation will lay out your table into rows by metric, allowing columns of **Avg**, **Min**, **Max**, **Total**, **Current** and **Count**. More than one column can be added.
|
||||
|
||||
### Annotations
|
||||
|
||||
@@ -70,7 +70,7 @@ mode then any queries you have in the metrics tab will be ignored.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v45/table_json_data.png" max-width="500px" >}}
|
||||
|
||||
If you have an Elasticsearch **Raw Document** query or an Elasticsearch query without a `date histogram` use this
|
||||
If you have an Elasticsearch **Raw Document** query or an Elasticsearch query without a **date histogram** use this
|
||||
transform mode and pick the columns using the **Columns** section.
|
||||
|
||||
|
||||
@@ -80,9 +80,9 @@ transform mode and pick the columns using the **Columns** section.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v45/table_paging.png" class="docs-image--no-shadow docs-image--right" max-width="350px" >}}
|
||||
|
||||
1. `Pagination (Page Size)`: The table display fields allow you to control The `Pagination` (page size) is the threshold at which the table rows will be broken into pages. For example, if your table had 95 records with a pagination value of 10, your table would be split across 9 pages.
|
||||
2. `Scroll`: The `scroll bar` checkbox toggles the ability to scroll within the panel, when unchecked, the panel height will grow to display all rows.
|
||||
3. `Font Size`: The `font size` field allows you to increase or decrease the size for the panel, relative to the default font size.
|
||||
1. **Rows Per Page**: The table display fields allow you to control how many rows per page there should be. For example, if your table had 95 records with a rows per page value of 10, your table would be split across 10 pages.
|
||||
2. **Scroll**: The scroll bar checkbox toggles the ability to scroll within the panel, when unchecked, the panel height will grow to display all rows.
|
||||
3. **Font Size**: The font size field allows you to increase or decrease the size for the panel, relative to the default font size.
|
||||
|
||||
|
||||
## Column Styles
|
||||
@@ -91,9 +91,9 @@ The column styles allow you control how dates and numbers are formatted.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v45/table_column_styles.png" class="docs-image--no-shadow" >}}
|
||||
|
||||
1. `Name or regex`: The Name or Regex field controls what columns the rule should be applied to. The regex or name filter will be matched against the column name not against column values.
|
||||
2. `Column Header`: Title for the column, when using a Regex the title can include replacement strings like `$1`.
|
||||
3. `Add column style rule`: Add new column rule.
|
||||
4. `Thresholds` and `Coloring`: Specify color mode and thresholds limits.
|
||||
5. `Type`: The three supported types of types are `Number`, `String` and `Date`. `Unit` and `Decimals`: Specify unit and decimal precision for numbers.`Format`: Specify date format for dates.
|
||||
1. **Name or regex**: The Name or Regex field controls what columns the rule should be applied to. The regex or name filter will be matched against the column name not against column values.
|
||||
2. **Column Header**: Title for the column, when using a Regex the title can include replacement strings like `$1`.
|
||||
3. **Add column style rule**: Add new column rule.
|
||||
4. **Thresholds and Coloring**: Specify color mode and thresholds limits.
|
||||
5. **Type**: The three supported types of types are **Number**, **String** and **Date**. **Unit** and **Decimals**: Specify unit and decimal precision for numbers. **Format**: Specify date format for dates.
|
||||
|
||||
|
||||
@@ -18,6 +18,6 @@ The text panel lets you make information and description panels etc. for your da
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v45/text-options.png" max-width="600px" class="docs-image--no-shadow">}}
|
||||
|
||||
1. `Mode`: Here you can choose between markdown, HTML or text.
|
||||
2. `Content`: Here you write your content.
|
||||
1. **Mode**: Here you can choose between markdown, HTML or text.
|
||||
2. **Content**: Here you write your content.
|
||||
|
||||
|
||||
@@ -23,12 +23,15 @@ Only works with Basic Authentication (username and password). See [introduction]
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```bash
|
||||
GET /api/admin/settings
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```bash
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -166,7 +169,8 @@ Only works with Basic Authentication (username and password). See [introduction]
|
||||
"key_file":"",
|
||||
"password":"************",
|
||||
"skip_verify":"false",
|
||||
"user":""},
|
||||
"user":""
|
||||
},
|
||||
"users":{
|
||||
"allow_org_create":"true",
|
||||
"allow_sign_up":"false",
|
||||
@@ -174,7 +178,7 @@ Only works with Basic Authentication (username and password). See [introduction]
|
||||
"auto_assign_org_role":"Viewer"
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
## Grafana Stats
|
||||
|
||||
`GET /api/admin/stats`
|
||||
@@ -183,12 +187,15 @@ Only works with Basic Authentication (username and password). See [introduction]
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```bash
|
||||
GET /api/admin/stats
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -203,6 +210,7 @@ Only works with Basic Authentication (username and password). See [introduction]
|
||||
"starred_db_count":2,
|
||||
"grafana_admin_count":2
|
||||
}
|
||||
```
|
||||
|
||||
## Global Users
|
||||
|
||||
@@ -211,6 +219,7 @@ Only works with Basic Authentication (username and password). See [introduction]
|
||||
Create new user. Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
|
||||
|
||||
**Example Request**:
|
||||
```json
|
||||
|
||||
POST /api/admin/users HTTP/1.1
|
||||
Accept: application/json
|
||||
@@ -222,13 +231,16 @@ Create new user. Only works with Basic Authentication (username and password). S
|
||||
"login":"user",
|
||||
"password":"userpassword"
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"id":5,"message":"User created"}
|
||||
```
|
||||
|
||||
## Password for User
|
||||
|
||||
@@ -239,18 +251,22 @@ Change password for a specific user.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```json
|
||||
PUT /api/admin/users/2/password HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{"password":"userpassword"}
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message": "User password updated"}
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
@@ -260,18 +276,22 @@ Only works with Basic Authentication (username and password). See [introduction]
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```json
|
||||
PUT /api/admin/users/2/permissions HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{"isGrafanaAdmin": true}
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{message: "User permissions updated"}
|
||||
```
|
||||
|
||||
## Delete global User
|
||||
|
||||
@@ -281,16 +301,20 @@ Only works with Basic Authentication (username and password). See [introduction]
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```json
|
||||
DELETE /api/admin/users/2 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{message: "User deleted"}
|
||||
```
|
||||
|
||||
## Pause all alerts
|
||||
|
||||
@@ -300,6 +324,7 @@ Only works with Basic Authentication (username and password). See [introduction]
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```json
|
||||
POST /api/admin/pause-all-alerts HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
@@ -307,6 +332,7 @@ Only works with Basic Authentication (username and password). See [introduction]
|
||||
{
|
||||
"paused": true
|
||||
}
|
||||
```
|
||||
|
||||
JSON Body schema:
|
||||
|
||||
@@ -314,7 +340,9 @@ JSON Body schema:
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{state: "new state", message: "alerts pause/un paused", "alertsAffected": 100}
|
||||
```
|
||||
@@ -23,11 +23,12 @@ This API can also be used to create, update and delete alert notifications.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/alerts HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
```
|
||||
Querystring Parameters:
|
||||
|
||||
These parameters are used as querystring parameters. For example:
|
||||
@@ -41,6 +42,7 @@ This API can also be used to create, update and delete alert notifications.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
[
|
||||
@@ -63,6 +65,7 @@ This API can also be used to create, update and delete alert notifications.
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
```
|
||||
|
||||
@@ -70,13 +73,16 @@ This API can also be used to create, update and delete alert notifications.
|
||||
|
||||
`POST /api/alerts/:id/pause`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
POST /api/alerts/1/pause HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
```
|
||||
|
||||
The :id query parameter is the id of the alert to be paused or unpaused.
|
||||
|
||||
JSON Body Schema:
|
||||
@@ -90,6 +96,7 @@ This API can also be used to create, update and delete alert notifications.
|
||||
Content-Type: application/json
|
||||
{
|
||||
```
|
||||
|
||||
## Get alert notifications
|
||||
|
||||
`GET /api/alert-notifications`
|
||||
@@ -97,6 +104,7 @@ This API can also be used to create, update and delete alert notifications.
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/alert-notifications HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
@@ -105,6 +113,7 @@ This API can also be used to create, update and delete alert notifications.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -114,6 +123,7 @@ JSON Body Schema:
|
||||
|
||||
`POST /api/alert-notifications`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
POST /api/alert-notifications HTTP/1.1
|
||||
@@ -121,6 +131,7 @@ JSON Body Schema:
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
@@ -128,15 +139,17 @@ JSON Body Schema:
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
```
|
||||
|
||||
## Update alert notification
|
||||
|
||||
`PUT /api/alert-notifications/1`
|
||||
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
PUT /api/alert-notifications/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
@@ -148,6 +161,7 @@ JSON Body Schema:
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
```
|
||||
|
||||
## Delete alert notification
|
||||
|
||||
@@ -155,6 +169,7 @@ JSON Body Schema:
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/alert-notifications/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
@@ -168,10 +183,11 @@ JSON Body Schema:
|
||||
Content-Type: application/json
|
||||
{
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
@@ -183,6 +199,7 @@ JSON Body Schema:
|
||||
"created": "2017-01-01 12:34",
|
||||
"updated": "2017-01-01 12:34"
|
||||
}
|
||||
```
|
||||
|
||||
## Update alert notification
|
||||
|
||||
@@ -190,6 +207,7 @@ JSON Body Schema:
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
PUT /api/alert-notifications/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
@@ -204,10 +222,11 @@ JSON Body Schema:
|
||||
"addresses: "carl@grafana.com;dev@grafana.com"
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
@@ -219,6 +238,7 @@ JSON Body Schema:
|
||||
"created": "2017-01-01 12:34",
|
||||
"updated": "2017-01-01 12:34"
|
||||
}
|
||||
```
|
||||
|
||||
## Delete alert notification
|
||||
|
||||
@@ -226,15 +246,19 @@ JSON Body Schema:
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/alert-notifications/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"message": "Notification deleted"
|
||||
}
|
||||
```
|
||||
189
docs/sources/http_api/annotations.md
Normal file
189
docs/sources/http_api/annotations.md
Normal file
@@ -0,0 +1,189 @@
|
||||
+++
|
||||
title = "Annotations HTTP API "
|
||||
description = "Grafana Annotations HTTP API"
|
||||
keywords = ["grafana", "http", "documentation", "api", "annotation", "annotations", "comment"]
|
||||
aliases = ["/http_api/annotations/"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Annotations"
|
||||
identifier = "annotationshttp"
|
||||
parent = "http_api"
|
||||
+++
|
||||
|
||||
# Annotations resources / actions
|
||||
|
||||
This is the API documentation for the new Grafana Annotations feature released in Grafana 4.6. Annotations are saved in the Grafana database (sqlite, mysql or postgres). Annotations can be global annotations that can be shown on any dashboard by configuring an annotation data source - they are filtered by tags. Or they can be tied to a panel on a dashboard and are then only shown on that panel.
|
||||
|
||||
## Find Annotations
|
||||
|
||||
`GET /api/annotations?from=1506676478816&to=1507281278816&tags=tag1&tags=tag2&limit=100`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/annotations?from=1506676478816&to=1507281278816&tags=tag1&tags=tag2&limit=100 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
```
|
||||
|
||||
|
||||
Query Parameters:
|
||||
|
||||
- `from`: epoch datetime in milliseconds. Optional.
|
||||
- `to`: epoch datetime in milliseconds. Optional.
|
||||
- `limit`: number. Optional - default is 10. Max limit for results returned.
|
||||
- `alertId`: number. Optional. Find annotations for a specified alert.
|
||||
- `dashboardId`: number. Optional. Find annotations that are scoped to a specific dashboard
|
||||
- `panelId`: number. Optional. Find annotations that are scoped to a specific panel
|
||||
- `tags`: string. Optional. Use this to filter global annotations. Global annotations are annotations from an annotation data source that are not connected specifically to a dashboard or panel. To do an "AND" filtering with multiple tags, specify the tags parameter multiple times e.g. `tags=tag1&tags=tag2`.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
[
|
||||
```
|
||||
|
||||
## Create Annotation
|
||||
|
||||
Creates an annotation in the Grafana database. The `dashboardId` and `panelId` fields are optional. If they are not specified then a global annotation is created and can be queried in any dashboard that adds the Grafana annotations data source.
|
||||
|
||||
`POST /api/annotations`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```json
|
||||
POST /api/annotations HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"dashboardId":468,
|
||||
"panelId":1,
|
||||
"time":1507037197339,
|
||||
"isRegion":true,
|
||||
"timeEnd":1507180805056,
|
||||
"tags":["tag1","tag2"],
|
||||
"text":"Annotation Description"
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Annotation added"}
|
||||
```
|
||||
|
||||
## Update Annotation
|
||||
|
||||
`PUT /api/annotations/:id`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```json
|
||||
PUT /api/annotations/1141 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"time":1507037197339,
|
||||
"isRegion":true,
|
||||
"timeEnd":1507180805056,
|
||||
"text":"Annotation Description",
|
||||
"tags":["tag3","tag4","tag5"]
|
||||
}
|
||||
```
|
||||
|
||||
## Delete Annotation By Id
|
||||
|
||||
`DELETE /api/annotation/:id`
|
||||
|
||||
Deletes the annotation that matches the specified id.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/annotation/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Delete Annotation By RegionId
|
||||
|
||||
`DELETE /api/annotation/region/:id`
|
||||
|
||||
Deletes the annotation that matches the specified region id. A region is an annotation that covers a timerange and has a start and end time. In the Grafana database, this is a stored as two annotations connected by a region id.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/annotation/region/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
Deletes the annotation that matches the specified id.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/annotation/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Annotation deleted"}
|
||||
```
|
||||
|
||||
## Delete Annotation By RegionId
|
||||
|
||||
`DELETE /api/annotation/region/:id`
|
||||
|
||||
Deletes the annotation that matches the specified region id. A region is an annotation that covers a timerange and has a start and end time. In the Grafana database, this is a stored as two annotations connected by a region id.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/annotation/region/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Annotation region deleted"}
|
||||
```
|
||||
@@ -21,7 +21,7 @@ If basic auth is enabled (it is enabled by default) you can authenticate your HT
|
||||
standard basic auth. Basic auth will also authenticate LDAP users.
|
||||
|
||||
curl example:
|
||||
```
|
||||
```bash
|
||||
?curl http://admin:admin@localhost:3000/api/org
|
||||
{"id":1,"name":"Main Org."}
|
||||
```
|
||||
@@ -36,9 +36,11 @@ You use the token in all requests in the `Authorization` header, like this:
|
||||
|
||||
**Example**:
|
||||
|
||||
```http
|
||||
GET http://your.grafana.com/api/dashboards/db/mydash HTTP/1.1
|
||||
Accept: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
The `Authorization` header value should be `Bearer <your api key>`.
|
||||
|
||||
@@ -50,13 +52,16 @@ The `Authorization` header value should be `Bearer <your api key>`.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/auth/keys HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -72,6 +77,7 @@ The `Authorization` header value should be `Bearer <your api key>`.
|
||||
POST /api/auth/keys HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
```
|
||||
|
||||
@@ -79,6 +85,7 @@ The `Authorization` header value should be `Bearer <your api key>`.
|
||||
|
||||
- **name** – The key name
|
||||
- **role** – Sets the access level/Grafana Role for the key. Can be one of the following values: `Viewer`, `Editor`, `Read Only Editor` or `Admin`.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
@@ -88,6 +95,7 @@ The `Authorization` header value should be `Bearer <your api key>`.
|
||||
```
|
||||
|
||||
## Delete API Key
|
||||
|
||||
`DELETE /api/auth/keys/:id`
|
||||
|
||||
**Example Request**:
|
||||
@@ -96,10 +104,12 @@ JSON Body schema:
|
||||
DELETE /api/auth/keys/3 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
@@ -107,14 +117,17 @@ JSON Body schema:
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/auth/keys/3 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
```
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"API key deleted"}
|
||||
```
|
||||
@@ -158,13 +158,16 @@ Will return the home dashboard.
|
||||
|
||||
`GET /api/search/`
|
||||
|
||||
Query parameters:
|
||||
|
||||
- **query** – Search Query
|
||||
- **tag** – Tag to use
|
||||
- **starred** – Flag indicating if only starred Dashboards should be returned
|
||||
- **tagcloud** - Flag indicating if a tagcloud should be returned
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/search?query=MyDashboard&starred=true&tag=prod HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
@@ -205,6 +208,7 @@ Will return the home dashboard.
|
||||
"version":5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tags for Dashboard
|
||||
|
||||
@@ -215,13 +219,16 @@ Get all tags of dashboards
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/dashboards/tags HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -235,6 +242,7 @@ Get all tags of dashboards
|
||||
"count":4
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Search Dashboards
|
||||
|
||||
@@ -249,13 +257,16 @@ Query parameters:
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/search?query=MyDashboard&starred=true&tag=prod HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -269,3 +280,4 @@ Query parameters:
|
||||
"isStarred":false
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -18,13 +18,16 @@ parent = "http_api"
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/datasources HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -46,6 +49,7 @@ parent = "http_api"
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
@@ -53,13 +57,16 @@ parent = "http_api"
|
||||
## Get a single data source by Name
|
||||
|
||||
`GET /api/datasources/name/:name`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/datasources/name/test_datasource HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
@@ -79,6 +86,7 @@ parent = "http_api"
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
@@ -86,13 +94,16 @@ parent = "http_api"
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Create data source
|
||||
|
||||
`POST /api/datasources`
|
||||
|
||||
**Example Graphite Request**:
|
||||
|
||||
```http
|
||||
POST /api/datasources HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
@@ -112,6 +123,7 @@ parent = "http_api"
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
@@ -119,19 +131,23 @@ parent = "http_api"
|
||||
## Update an existing data source
|
||||
|
||||
`PUT /api/datasources/:datasourceId`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
PUT /api/datasources/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Delete an existing data source by id
|
||||
@@ -139,6 +155,7 @@ parent = "http_api"
|
||||
`DELETE /api/datasources/:datasourceId`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/datasources/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
@@ -151,9 +168,11 @@ parent = "http_api"
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
## Delete an existing data source by name
|
||||
|
||||
`DELETE /api/datasources/name/:datasourceName`
|
||||
|
||||
**Example Request**:
|
||||
@@ -177,10 +196,12 @@ parent = "http_api"
|
||||
|
||||
`GET /api/datasources/proxy/:datasourceId/*`
|
||||
|
||||
Proxies all calls to the actual datasource.
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"id":1,"message":"Datasource added", "name": "test_datasource"}
|
||||
```
|
||||
|
||||
## Update an existing data source
|
||||
|
||||
@@ -188,6 +209,7 @@ parent = "http_api"
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
PUT /api/datasources/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
@@ -209,13 +231,16 @@ parent = "http_api"
|
||||
"isDefault":false,
|
||||
"jsonData":null
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Datasource updated", "id": 1, "name": "test_datasource"}
|
||||
```
|
||||
|
||||
## Delete an existing data source by id
|
||||
|
||||
@@ -223,17 +248,21 @@ parent = "http_api"
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/datasources/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Data source deleted"}
|
||||
```
|
||||
|
||||
## Delete an existing data source by name
|
||||
|
||||
@@ -241,17 +270,21 @@ parent = "http_api"
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/datasources/name/test_datasource HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Data source deleted"}
|
||||
```
|
||||
|
||||
## Data source proxy calls
|
||||
|
||||
|
||||
@@ -18,13 +18,16 @@ parent = "http_api"
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/org HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -32,6 +35,7 @@ parent = "http_api"
|
||||
|
||||
## Get Organisation by Id
|
||||
|
||||
`GET /api/orgs/:orgId`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
@@ -39,13 +43,16 @@ parent = "http_api"
|
||||
GET /api/orgs/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
## Get Organisation by Name
|
||||
|
||||
`GET /api/orgs/name/:orgName`
|
||||
@@ -61,20 +68,23 @@ parent = "http_api"
|
||||
|
||||
**Example Response**:
|
||||
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Create Organisation
|
||||
|
||||
`POST /api/orgs`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
POST /api/orgs HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
```
|
||||
|
||||
@@ -90,6 +100,7 @@ parent = "http_api"
|
||||
## Update current Organisation
|
||||
|
||||
`PUT /api/org`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
@@ -97,6 +108,7 @@ parent = "http_api"
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
@@ -105,10 +117,11 @@ parent = "http_api"
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Get all users within the actual organisation
|
||||
|
||||
`GET /api/org/users`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
@@ -116,7 +129,7 @@ parent = "http_api"
|
||||
GET /api/org/users HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
@@ -125,6 +138,7 @@ parent = "http_api"
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Add a new user to the actual organisation
|
||||
|
||||
@@ -133,15 +147,16 @@ parent = "http_api"
|
||||
Adds a global user to the actual organisation.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
|
||||
```http
|
||||
POST /api/org/users HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
```
|
||||
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
@@ -149,13 +164,16 @@ parent = "http_api"
|
||||
|
||||
```
|
||||
|
||||
## Updates the given user
|
||||
|
||||
`PATCH /api/org/users/:userId`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
PATCH /api/org/users/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
```
|
||||
@@ -168,6 +186,7 @@ parent = "http_api"
|
||||
|
||||
```
|
||||
|
||||
## Delete user in actual organisation
|
||||
|
||||
`DELETE /api/org/users/:userId`
|
||||
|
||||
@@ -177,6 +196,7 @@ Adds a global user to the actual organisation.
|
||||
DELETE /api/org/users/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
@@ -186,14 +206,16 @@ Adds a global user to the actual organisation.
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
|
||||
# Organisations
|
||||
|
||||
## Search all Organisations
|
||||
|
||||
`GET /api/orgs`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/orgs HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
@@ -201,6 +223,7 @@ Adds a global user to the actual organisation.
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
@@ -209,15 +232,16 @@ Adds a global user to the actual organisation.
|
||||
|
||||
## Update Organisation
|
||||
|
||||
|
||||
`PUT /api/orgs/:orgId`
|
||||
|
||||
Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented yet.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
PUT /api/orgs/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
```
|
||||
@@ -225,18 +249,21 @@ Adds a global user to the actual organisation.
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Get Users in Organisation
|
||||
|
||||
`GET /api/orgs/:orgId/users`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/orgs/1/users HTTP/1.1
|
||||
Accept: application/json
|
||||
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
@@ -246,13 +273,16 @@ Adds a global user to the actual organisation.
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
[
|
||||
```
|
||||
|
||||
## Add User in Organisation
|
||||
|
||||
`POST /api/orgs/:orgId/users`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
POST /api/orgs/1/users HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
@@ -262,6 +292,7 @@ Adds a global user to the actual organisation.
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
@@ -271,6 +302,7 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y
|
||||
`PATCH /api/orgs/:orgId/users/:userId`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
PATCH /api/orgs/1/users/2 HTTP/1.1
|
||||
Accept: application/json
|
||||
@@ -279,14 +311,16 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y
|
||||
|
||||
```
|
||||
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Delete User in Organisation
|
||||
|
||||
`DELETE /api/orgs/:orgId/users/:userId`
|
||||
|
||||
**Example Request**:
|
||||
@@ -294,13 +328,16 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y
|
||||
```http
|
||||
DELETE /api/orgs/1/users/2 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
Content-Type: application/json
|
||||
[
|
||||
@@ -312,6 +349,7 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y
|
||||
"role":"Admin"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Add User in Organisation
|
||||
|
||||
@@ -319,6 +357,7 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
POST /api/orgs/1/users HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
@@ -328,13 +367,16 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y
|
||||
"loginOrEmail":"user",
|
||||
"role":"Viewer"
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"User added to organization"}
|
||||
```
|
||||
|
||||
## Update Users in Organisation
|
||||
|
||||
@@ -342,6 +384,7 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
PATCH /api/orgs/1/users/2 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
@@ -350,13 +393,16 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y
|
||||
{
|
||||
"role":"Admin"
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Organization user updated"}
|
||||
```
|
||||
|
||||
## Delete User in Organisation
|
||||
|
||||
@@ -364,14 +410,18 @@ Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented y
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/orgs/1/users/2 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"User removed from organization"}
|
||||
```
|
||||
@@ -18,13 +18,16 @@ parent = "http_api"
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/frontend/settings HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -55,6 +58,7 @@ parent = "http_api"
|
||||
},
|
||||
"defaultDatasource": "Grafana"
|
||||
}
|
||||
```
|
||||
|
||||
# Login API
|
||||
|
||||
@@ -64,14 +68,18 @@ parent = "http_api"
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/login/ping HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message": "Logged in"}
|
||||
```
|
||||
@@ -26,17 +26,21 @@ system default value.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/user/preferences HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Update Current User Prefs
|
||||
|
||||
`PUT /api/user/preferences`
|
||||
@@ -44,6 +48,7 @@ system default value.
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
PUT /api/user/preferences HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
@@ -54,13 +59,16 @@ system default value.
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
```
|
||||
|
||||
## Get Current Org Prefs
|
||||
|
||||
`GET /api/org/preferences`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/org/preferences HTTP/1.1
|
||||
Accept: application/json
|
||||
@@ -68,17 +76,21 @@ system default value.
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Update Current Org Prefs
|
||||
|
||||
`PUT /api/org/preferences`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
PUT /api/org/preferences HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
@@ -86,6 +98,7 @@ system default value.
|
||||
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
@@ -96,10 +109,13 @@ system default value.
|
||||
"homeDashboardId":0,
|
||||
"timezone":"utc"
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
{"message":"Preferences updated"}
|
||||
```
|
||||
@@ -17,6 +17,7 @@ parent = "http_api"
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
POST /api/snapshots HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
@@ -51,6 +52,7 @@ parent = "http_api"
|
||||
},
|
||||
"expires": 3600
|
||||
}
|
||||
```
|
||||
|
||||
JSON Body schema:
|
||||
|
||||
@@ -63,6 +65,7 @@ JSON Body schema:
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
@@ -71,6 +74,7 @@ JSON Body schema:
|
||||
"key":"YYYYYYY",
|
||||
"url":"myurl/dashboard/snapshot/YYYYYYY"
|
||||
}
|
||||
```
|
||||
|
||||
Keys:
|
||||
|
||||
@@ -83,13 +87,16 @@ Keys:
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/snapshots/YYYYYYY HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -131,6 +138,7 @@ Keys:
|
||||
"version":5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Delete Snapshot by Id
|
||||
|
||||
@@ -138,14 +146,18 @@ Keys:
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/snapshots/YYYYYYY HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Snapshot deleted. It might take an hour before it's cleared from a CDN cache."}
|
||||
```
|
||||
@@ -17,15 +17,18 @@ parent = "http_api"
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/users HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
```
|
||||
|
||||
Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -45,6 +48,7 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter
|
||||
```
|
||||
|
||||
Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. The `totalCount` field in the response can be used for pagination of the user list E.g. if `totalCount` is equal to 100 users and the `perpage` parameter is set to 10 then there are 10 pages of users. The `query` parameter is optional and it will return results where the query value is contained in one of the `name`, `login` or `email` fields. Query values with spaces need to be url encoded e.g. `query=Jane%20Doe`.
|
||||
|
||||
Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Response**:
|
||||
@@ -52,10 +56,12 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
```
|
||||
|
||||
## Get single user by Id
|
||||
|
||||
`GET /api/users/:id`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
@@ -63,6 +69,7 @@ Requires basic authentication and that the authenticated user is a Grafana Admin
|
||||
GET /api/users/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
```
|
||||
Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
@@ -86,6 +93,7 @@ Requires basic authentication and that the authenticated user is a Grafana Admin
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Request using the username as option**:
|
||||
|
||||
```http
|
||||
@@ -93,15 +101,17 @@ Requires basic authentication and that the authenticated user is a Grafana Admin
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
```
|
||||
|
||||
Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## User Update
|
||||
|
||||
@@ -113,6 +123,7 @@ Requires basic authentication and that the authenticated user is a Grafana Admin
|
||||
PUT /api/users/2 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
|
||||
```
|
||||
|
||||
@@ -120,22 +131,27 @@ Requires basic authentication and that the authenticated user is a Grafana Admin
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Get Organisations for user
|
||||
|
||||
`GET /api/users/:id/orgs`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/users/1/orgs HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
```
|
||||
|
||||
Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
@@ -147,7 +163,7 @@ Requires basic authentication and that the authenticated user is a Grafana Admin
|
||||
## User
|
||||
|
||||
## Actual User
|
||||
|
||||
|
||||
`GET /api/user`
|
||||
|
||||
**Example Request**:
|
||||
@@ -155,6 +171,7 @@ Requires basic authentication and that the authenticated user is a Grafana Admin
|
||||
```http
|
||||
GET /api/user HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
@@ -166,16 +183,18 @@ Requires basic authentication and that the authenticated user is a Grafana Admin
|
||||
|
||||
```
|
||||
|
||||
## Change Password
|
||||
|
||||
`PUT /api/user/password`
|
||||
|
||||
Changes the password for the user
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
PUT /api/user/password HTTP/1.1
|
||||
Accept: application/json
|
||||
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
```
|
||||
@@ -183,15 +202,18 @@ Requires basic authentication and that the authenticated user is a Grafana Admin
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Switch user context for a specified user
|
||||
|
||||
`POST /api/users/:userId/using/:organizationId`
|
||||
|
||||
Switch user context to the given organization. Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
POST /api/users/7/using/2 HTTP/1.1
|
||||
@@ -202,6 +224,7 @@ Requires basic authentication and that the authenticated user is a Grafana Admin
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
@@ -211,13 +234,16 @@ Requires basic authentication and that the authenticated user is a Grafana Admin
|
||||
|
||||
Switch user context to the given organization.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
POST /api/user/using/2 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
@@ -229,6 +255,7 @@ Requires basic authentication and that the authenticated user is a Grafana Admin
|
||||
|
||||
`GET /api/user/orgs`
|
||||
|
||||
Return a list of all organisations of the current user.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
@@ -238,6 +265,7 @@ Changes the password for the user
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
@@ -248,13 +276,16 @@ Changes the password for the user
|
||||
|
||||
## Star a dashboard
|
||||
|
||||
`POST /api/user/stars/dashboard/:dashboardId`
|
||||
|
||||
Stars the given Dashboard for the actual user.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
POST /api/user/stars/dashboard/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
@@ -264,15 +295,19 @@ Switch user context to the given organization. Requires basic authentication and
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Unstar a dashboard
|
||||
|
||||
`DELETE /api/user/stars/dashboard/:dashboardId`
|
||||
|
||||
Deletes the starring of the given Dashboard for the actual user.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/user/stars/dashboard/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
@@ -282,17 +317,21 @@ Switch user context to the given organization.
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Active organization changed"}
|
||||
```
|
||||
|
||||
## Organisations of the actual User
|
||||
|
||||
@@ -302,13 +341,16 @@ Return a list of all organisations of the current user.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/user/orgs HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -319,6 +361,7 @@ Return a list of all organisations of the current user.
|
||||
"role":"Admin"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Star a dashboard
|
||||
|
||||
@@ -328,17 +371,21 @@ Stars the given Dashboard for the actual user.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
POST /api/user/stars/dashboard/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Dashboard starred!"}
|
||||
```
|
||||
|
||||
## Unstar a dashboard
|
||||
|
||||
@@ -348,14 +395,18 @@ Deletes the starring of the given Dashboard for the actual user.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/user/stars/dashboard/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Dashboard unstarred"}
|
||||
```
|
||||
@@ -15,7 +15,7 @@ weight = 1
|
||||
It should be straight forward to get Grafana up and running behind a reverse proxy. But here are some things that you might run into.
|
||||
|
||||
Links and redirects will not be rendered correctly unless you set the server.domain setting.
|
||||
```
|
||||
```bash
|
||||
[server]
|
||||
domain = foo.bar
|
||||
```
|
||||
@@ -28,14 +28,14 @@ Here are some example configurations for running Grafana behind a reverse proxy.
|
||||
|
||||
### Grafana configuration (ex http://foo.bar.com)
|
||||
|
||||
```
|
||||
```bash
|
||||
[server]
|
||||
domain = foo.bar
|
||||
```
|
||||
|
||||
### Nginx configuration
|
||||
|
||||
```
|
||||
```bash
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/www;
|
||||
@@ -50,14 +50,14 @@ server {
|
||||
### Examples with **sub path** (ex http://foo.bar.com/grafana)
|
||||
|
||||
#### Grafana configuration with sub path
|
||||
```
|
||||
```bash
|
||||
[server]
|
||||
domain = foo.bar
|
||||
root_url = %(protocol)s://%(domain)s:/grafana
|
||||
```
|
||||
|
||||
#### Nginx configuration with sub path
|
||||
```
|
||||
```bash
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/www;
|
||||
|
||||
@@ -37,11 +37,14 @@ A common problem is forgetting to uncomment a line in the `custom.ini` (or `graf
|
||||
All options in the configuration file (listed below) can be overridden
|
||||
using environment variables using the syntax:
|
||||
|
||||
```bash
|
||||
GF_<SectionName>_<KeyName>
|
||||
```
|
||||
|
||||
Where the section name is the text within the brackets. Everything
|
||||
should be upper case, `.` should be replaced by `_`. For example, given these configuration settings:
|
||||
|
||||
```bash
|
||||
# default section
|
||||
instance_name = ${HOSTNAME}
|
||||
|
||||
@@ -50,13 +53,15 @@ should be upper case, `.` should be replaced by `_`. For example, given these co
|
||||
|
||||
[auth.google]
|
||||
client_secret = 0ldS3cretKey
|
||||
|
||||
```
|
||||
|
||||
Then you can override them using:
|
||||
|
||||
```bash
|
||||
export GF_DEFAULT_INSTANCE_NAME=my-instance
|
||||
export GF_SECURITY_ADMIN_USER=true
|
||||
export GF_AUTH_GOOGLE_CLIENT_SECRET=newS3cretKey
|
||||
```
|
||||
|
||||
<hr />
|
||||
|
||||
@@ -93,11 +98,15 @@ The IP address to bind to. If empty will bind to all interfaces
|
||||
The port to bind to, defaults to `3000`. To use port 80 you need to
|
||||
either give the Grafana binary permission for example:
|
||||
|
||||
```bash
|
||||
$ sudo setcap 'cap_net_bind_service=+ep' /usr/sbin/grafana-server
|
||||
```
|
||||
|
||||
Or redirect port 80 to the Grafana port using:
|
||||
|
||||
```bash
|
||||
$ sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3000
|
||||
```
|
||||
|
||||
Another way is put a webserver like Nginx or Apache in front of Grafana and have them proxy requests to Grafana.
|
||||
|
||||
@@ -312,7 +321,9 @@ You need to create a GitHub OAuth application (you find this under the GitHub
|
||||
settings page). When you create the application you will need to specify
|
||||
a callback URL. Specify this as callback:
|
||||
|
||||
```bash
|
||||
http://<my_grafana_server_name_or_ip>:<grafana_server_port>/login/github
|
||||
```
|
||||
|
||||
This callback URL must match the full HTTP address that you use in your
|
||||
browser to access Grafana, but with the prefix path of `/login/github`.
|
||||
@@ -320,6 +331,7 @@ When the GitHub OAuth application is created you will get a Client ID and a
|
||||
Client Secret. Specify these in the Grafana configuration file. For
|
||||
example:
|
||||
|
||||
```bash
|
||||
[auth.github]
|
||||
enabled = true
|
||||
allow_sign_up = true
|
||||
@@ -331,6 +343,7 @@ example:
|
||||
api_url = https://api.github.com/user
|
||||
team_ids =
|
||||
allowed_organizations =
|
||||
```
|
||||
|
||||
Restart the Grafana back-end. You should now see a GitHub login button
|
||||
on the login page. You can now login or sign up with your GitHub
|
||||
@@ -348,6 +361,7 @@ GitHub. If the authenticated user isn't a member of at least one of the
|
||||
teams they will not be able to register or authenticate with your
|
||||
Grafana instance. For example:
|
||||
|
||||
```bash
|
||||
[auth.github]
|
||||
enabled = true
|
||||
client_id = YOUR_GITHUB_APP_CLIENT_ID
|
||||
@@ -357,6 +371,7 @@ Grafana instance. For example:
|
||||
auth_url = https://github.com/login/oauth/authorize
|
||||
token_url = https://github.com/login/oauth/access_token
|
||||
allow_sign_up = true
|
||||
```
|
||||
|
||||
### allowed_organizations
|
||||
|
||||
@@ -365,6 +380,7 @@ organizations on GitHub. If the authenticated user isn't a member of at least
|
||||
one of the organizations they will not be able to register or authenticate with
|
||||
your Grafana instance. For example
|
||||
|
||||
```bash
|
||||
[auth.github]
|
||||
enabled = true
|
||||
client_id = YOUR_GITHUB_APP_CLIENT_ID
|
||||
@@ -375,6 +391,7 @@ your Grafana instance. For example
|
||||
allow_sign_up = true
|
||||
# space-delimited organization names
|
||||
allowed_organizations = github google
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -385,13 +402,16 @@ Developer Console](https://console.developers.google.com/project). When
|
||||
you create the project you will need to specify a callback URL. Specify
|
||||
this as callback:
|
||||
|
||||
```bash
|
||||
http://<my_grafana_server_name_or_ip>:<grafana_server_port>/login/google
|
||||
```
|
||||
|
||||
This callback URL must match the full HTTP address that you use in your
|
||||
browser to access Grafana, but with the prefix path of `/login/google`.
|
||||
When the Google project is created you will get a Client ID and a Client
|
||||
Secret. Specify these in the Grafana configuration file. For example:
|
||||
|
||||
```bash
|
||||
[auth.google]
|
||||
enabled = true
|
||||
client_id = YOUR_GOOGLE_APP_CLIENT_ID
|
||||
@@ -401,6 +421,7 @@ Secret. Specify these in the Grafana configuration file. For example:
|
||||
token_url = https://accounts.google.com/o/oauth2/token
|
||||
allowed_domains = mycompany.com mycompany.org
|
||||
allow_sign_up = true
|
||||
```
|
||||
|
||||
Restart the Grafana back-end. You should now see a Google login button
|
||||
on the login page. You can now login or sign up with your Google
|
||||
@@ -418,6 +439,7 @@ This option could be used if have your own oauth service.
|
||||
This callback URL must match the full HTTP address that you use in your
|
||||
browser to access Grafana, but with the prefix path of `/login/generic_oauth`.
|
||||
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
enabled = true
|
||||
client_id = YOUR_APP_CLIENT_ID
|
||||
@@ -428,9 +450,44 @@ browser to access Grafana, but with the prefix path of `/login/generic_oauth`.
|
||||
api_url =
|
||||
allowed_domains = mycompany.com mycompany.org
|
||||
allow_sign_up = true
|
||||
```
|
||||
|
||||
Set api_url to the resource that returns [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo) compatible information.
|
||||
|
||||
### Set up oauth2 with Okta
|
||||
|
||||
First set up Grafana as an OpenId client "webapplication" in Okta. Then set the Base URIs to `https://<grafana domain>/` and set the Login redirect URIs to `https://<grafana domain>/login/generic_oauth`.
|
||||
|
||||
Finaly set up the generic oauth module like this:
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
name = Okta
|
||||
enabled = true
|
||||
scopes = openid profile email
|
||||
client_id = <okta application Client ID>
|
||||
client_secret = <okta application Client Secret>
|
||||
auth_url = https://<okta domain>/oauth2/v1/authorize
|
||||
token_url = https://<okta domain>/oauth2/v1/token
|
||||
api_url = https://<okta domain>/oauth2/v1/userinfo
|
||||
```
|
||||
|
||||
### Set up oauth2 with Bitbucket
|
||||
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
name = BitBucket
|
||||
enabled = true
|
||||
allow_sign_up = true
|
||||
client_id = <client id>
|
||||
client_secret = <secret>
|
||||
scopes = account email
|
||||
auth_url = https://bitbucket.org/site/oauth2/authorize
|
||||
token_url = https://bitbucket.org/site/oauth2/access_token
|
||||
api_url = https://api.bitbucket.org/2.0/user
|
||||
team_ids =
|
||||
allowed_organizations =
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
## [auth.basic]
|
||||
@@ -503,21 +560,25 @@ session table manually.
|
||||
|
||||
Mysql Example:
|
||||
|
||||
```bash
|
||||
CREATE TABLE `session` (
|
||||
`key` CHAR(16) NOT NULL,
|
||||
`data` BLOB,
|
||||
`expiry` INT(11) UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (`key`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
|
||||
```
|
||||
|
||||
Postgres Example:
|
||||
|
||||
```bash
|
||||
CREATE TABLE session (
|
||||
key CHAR(16) NOT NULL,
|
||||
data BYTEA,
|
||||
expiry INTEGER NOT NULL,
|
||||
PRIMARY KEY (key)
|
||||
);
|
||||
```
|
||||
|
||||
Postgres valid `sslmode` are `disable`, `require` (default), `verify-ca`, and `verify-full`.
|
||||
|
||||
@@ -651,11 +712,16 @@ These options control how images should be made public so they can be shared on
|
||||
You can choose between (s3, webdav, gcs). If left empty Grafana will ignore the upload action.
|
||||
|
||||
## [external_image_storage.s3]
|
||||
|
||||
### bucket
|
||||
Bucket name for S3. e.g. grafana.snapshot
|
||||
|
||||
### region
|
||||
Region name for S3. e.g. 'us-east-1', 'cn-north-1', etc
|
||||
|
||||
### path
|
||||
Optional extra path inside bucket, useful to apply expiration policies
|
||||
|
||||
### bucket_url
|
||||
(for backward compatibility, only works when no bucket or region are configured)
|
||||
Bucket URL for S3. AWS region can be specified within URL or defaults to 'us-east-1', e.g.
|
||||
|
||||
@@ -45,13 +45,17 @@ sudo dpkg -i grafana_4.5.2-beta1_amd64.deb
|
||||
|
||||
Add the following line to your `/etc/apt/sources.list` file.
|
||||
|
||||
```bash
|
||||
deb https://packagecloud.io/grafana/stable/debian/ jessie main
|
||||
```
|
||||
|
||||
Use the above line even if you are on Ubuntu or another Debian version.
|
||||
There is also a testing repository if you want beta or release
|
||||
candidates.
|
||||
|
||||
```bash
|
||||
deb https://packagecloud.io/grafana/testing/debian/ jessie main
|
||||
```
|
||||
|
||||
Then add the [Package Cloud](https://packagecloud.io/grafana) key. This
|
||||
allows you to install signed packages.
|
||||
|
||||
@@ -14,7 +14,9 @@ weight = 4
|
||||
|
||||
Grafana is very easy to install and run using the offical docker container.
|
||||
|
||||
```bash
|
||||
$ docker run -d -p 3000:3000 grafana/grafana
|
||||
```
|
||||
|
||||
All Grafana configuration settings can be defined using environment
|
||||
variables, this is especially useful when using the above container.
|
||||
@@ -26,10 +28,12 @@ folder `/var/lib/grafana` and configuration files is in `/etc/grafana/`
|
||||
folder. You can map these volumes to host folders when you start the
|
||||
container:
|
||||
|
||||
```bash
|
||||
$ docker run -d -p 3000:3000 \
|
||||
-v /var/lib/grafana:/var/lib/grafana \
|
||||
-e "GF_SECURITY_ADMIN_PASSWORD=secret" \
|
||||
grafana/grafana
|
||||
```
|
||||
|
||||
In the above example I map the data folder and sets a configuration option via
|
||||
an `ENV` instruction.
|
||||
|
||||
@@ -92,7 +92,7 @@ org_role = "Viewer"
|
||||
By default the configuration expects you to specify a bind DN and bind password. This should be a read only user that can perform LDAP searches.
|
||||
When the user DN is found a second bind is performed with the user provided username & password (in the normal Grafana login form).
|
||||
|
||||
```
|
||||
```bash
|
||||
bind_dn = "cn=admin,dc=grafana,dc=org"
|
||||
bind_password = "grafana"
|
||||
```
|
||||
@@ -102,7 +102,7 @@ bind_password = "grafana"
|
||||
If you can provide a single bind expression that matches all possible users, you can skip the second bind and bind against the user DN directly.
|
||||
This allows you to not specify a bind_password in the configuration file.
|
||||
|
||||
```
|
||||
```bash
|
||||
bind_dn = "cn=%s,o=users,dc=grafana,dc=org"
|
||||
```
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Installation can be done using [homebrew](http://brew.sh/)
|
||||
|
||||
Install latest stable:
|
||||
|
||||
```
|
||||
```bash
|
||||
brew update
|
||||
brew install grafana
|
||||
```
|
||||
@@ -24,7 +24,7 @@ To start grafana look at the command printed after the homebrew install complete
|
||||
|
||||
To upgrade use the reinstall command
|
||||
|
||||
```
|
||||
```bash
|
||||
brew update
|
||||
brew reinstall grafana
|
||||
```
|
||||
@@ -34,13 +34,13 @@ brew reinstall grafana
|
||||
You can also install the latest unstable grafana from git:
|
||||
|
||||
|
||||
```
|
||||
```bash
|
||||
brew install --HEAD grafana/grafana/grafana
|
||||
```
|
||||
|
||||
To upgrade grafana if you've installed from HEAD:
|
||||
|
||||
```
|
||||
```bash
|
||||
brew reinstall --HEAD grafana/grafana/grafana
|
||||
```
|
||||
|
||||
@@ -48,13 +48,13 @@ brew reinstall --HEAD grafana/grafana/grafana
|
||||
|
||||
To start Grafana using homebrew services first make sure homebrew/services is installed.
|
||||
|
||||
```
|
||||
```bash
|
||||
brew tap homebrew/services
|
||||
```
|
||||
|
||||
Then start Grafana using:
|
||||
|
||||
```
|
||||
```bash
|
||||
brew services start grafana
|
||||
```
|
||||
|
||||
|
||||
@@ -26,41 +26,54 @@ installation.
|
||||
|
||||
You can install Grafana using Yum directly.
|
||||
|
||||
```bash
|
||||
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.5.2-1.x86_64.rpm
|
||||
```
|
||||
|
||||
Or install manually using `rpm`.
|
||||
|
||||
#### On CentOS / Fedora / Redhat:
|
||||
|
||||
```bash
|
||||
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.5.2-1.x86_64.rpm
|
||||
$ sudo yum install initscripts fontconfig
|
||||
$ sudo rpm -Uvh grafana-4.5.2-1.x86_64.rpm
|
||||
```
|
||||
|
||||
#### On OpenSuse:
|
||||
|
||||
```bash
|
||||
$ sudo rpm -i --nodeps grafana-4.5.2-1.x86_64.rpm
|
||||
```
|
||||
|
||||
## Install via YUM Repository
|
||||
|
||||
Add the following to a new file at `/etc/yum.repos.d/grafana.repo`
|
||||
|
||||
```bash
|
||||
[grafana]
|
||||
name=grafana
|
||||
baseurl=https://packagecloud.io/grafana/stable/el/6/$basearch
|
||||
repo_gpgcheck=1
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
gpgkey=https://packagecloud.io/gpg.key https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana
|
||||
gpgkey=https://packagecloud.io/gpg.key
|
||||
https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana
|
||||
sslverify=1
|
||||
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
|
||||
```
|
||||
|
||||
There is also a testing repository if you want beta or release candidates.
|
||||
|
||||
```bash
|
||||
baseurl=https://packagecloud.io/grafana/testing/el/6/$basearch
|
||||
```
|
||||
|
||||
Then install Grafana via the `yum` command.
|
||||
|
||||
```bash
|
||||
$ sudo yum install grafana
|
||||
```
|
||||
|
||||
### RPM GPG Key
|
||||
|
||||
@@ -81,7 +94,9 @@ key](https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana).
|
||||
|
||||
You can start Grafana by running:
|
||||
|
||||
```bash
|
||||
$ sudo service grafana-server start
|
||||
```
|
||||
|
||||
This will start the `grafana-server` process as the `grafana` user,
|
||||
which is created during package installation. The default HTTP port is
|
||||
@@ -89,17 +104,23 @@ which is created during package installation. The default HTTP port is
|
||||
|
||||
To configure the Grafana server to start at boot time:
|
||||
|
||||
```bash
|
||||
$ sudo /sbin/chkconfig --add grafana-server
|
||||
```
|
||||
|
||||
## Start the server (via systemd)
|
||||
|
||||
```bash
|
||||
$ systemctl daemon-reload
|
||||
$ systemctl start grafana-server
|
||||
$ systemctl status grafana-server
|
||||
```
|
||||
|
||||
### Enable the systemd service to start at boot
|
||||
|
||||
```bash
|
||||
sudo systemctl enable grafana-server.service
|
||||
```
|
||||
|
||||
## Environment file
|
||||
|
||||
@@ -138,7 +159,7 @@ for example in alert notifications.
|
||||
|
||||
If the image is missing text make sure you have font packages installed.
|
||||
|
||||
```
|
||||
```bash
|
||||
yum install fontconfig
|
||||
yum install freetype*
|
||||
yum install urw-fonts
|
||||
|
||||
@@ -29,7 +29,7 @@ installed grafana to custom location using a binary tar/zip it is usally in `<gr
|
||||
|
||||
#### mysql
|
||||
|
||||
```
|
||||
```bash
|
||||
backup:
|
||||
> mysqldump -u root -p[root_password] [grafana] > grafana_backup.sql
|
||||
|
||||
@@ -39,7 +39,7 @@ restore:
|
||||
|
||||
#### postgres
|
||||
|
||||
```
|
||||
```bash
|
||||
backup:
|
||||
> pg_dump grafana > grafana_backup
|
||||
|
||||
@@ -54,7 +54,7 @@ and execute the same `dpkg -i` command but with the new package. It will upgrade
|
||||
|
||||
If you used our APT repository:
|
||||
|
||||
```
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install grafana
|
||||
```
|
||||
@@ -73,14 +73,14 @@ and execute the same `yum install` or `rpm -i` command but with the new package.
|
||||
|
||||
If you used our YUM repository:
|
||||
|
||||
```
|
||||
```bash
|
||||
sudo yum update grafana
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
This just an example, details depend on how you configured your grafana container.
|
||||
```
|
||||
```bash
|
||||
docker pull grafana
|
||||
docker stop my-grafana-container
|
||||
docker rm my-grafana-container
|
||||
|
||||
@@ -23,7 +23,7 @@ The most important fields are the first three, especially the id. The convention
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
```bash
|
||||
raintank-worldping-app
|
||||
grafana-simple-json-datasource
|
||||
grafana-piechart-panel
|
||||
@@ -66,7 +66,7 @@ The README.md file is rendered both on Grafana.net and in the plugins section in
|
||||
|
||||
Here is a typical directory structure for a plugin.
|
||||
|
||||
```
|
||||
```bash
|
||||
johnnyb-awesome-datasource
|
||||
|-- dist
|
||||
|-- spec
|
||||
|
||||
@@ -45,7 +45,7 @@ The javascript object that communicates with the database and transforms data to
|
||||
|
||||
The Datasource should contain the following functions:
|
||||
|
||||
```
|
||||
```javascript
|
||||
query(options) //used by panels to get data
|
||||
testDatasource() //used by datasource configuration page to make sure the connection is working
|
||||
annotationQuery(options) // used by dashboards to get annotations
|
||||
|
||||
@@ -30,37 +30,37 @@ On Linux systems the grafana-cli will assume that the grafana plugin directory i
|
||||
### Grafana-cli Commands
|
||||
|
||||
List available plugins
|
||||
```
|
||||
```bash
|
||||
grafana-cli plugins list-remote
|
||||
```
|
||||
|
||||
Install the latest version of a plugin
|
||||
```
|
||||
```bash
|
||||
grafana-cli plugins install <plugin-id>
|
||||
```
|
||||
|
||||
Install a specific version of a plugin
|
||||
```
|
||||
```bash
|
||||
grafana-cli plugins install <plugin-id> <version>
|
||||
```
|
||||
|
||||
List installed plugins
|
||||
```
|
||||
```bash
|
||||
grafana-cli plugins ls
|
||||
```
|
||||
|
||||
Update all installed plugins
|
||||
```
|
||||
```bash
|
||||
grafana-cli plugins update-all
|
||||
```
|
||||
|
||||
Update one plugin
|
||||
```
|
||||
```bash
|
||||
grafana-cli plugins update <plugin-id>
|
||||
```
|
||||
|
||||
Remove one plugin
|
||||
```
|
||||
```bash
|
||||
grafana-cli plugins remove <plugin-id>
|
||||
```
|
||||
|
||||
@@ -73,7 +73,7 @@ The Download URL from Grafana.com API is in this form:
|
||||
`https://grafana.com/api/plugins/<plugin id>/versions/<version number>/download`
|
||||
|
||||
You can specify a local URL by using the `--pluginUrl` option.
|
||||
```
|
||||
```bash
|
||||
grafana-cli --pluginUrl https://nexus.company.com/grafana/plugins/<plugin-id>-<plugin-version>.zip plugins install <plugin-id>
|
||||
```
|
||||
|
||||
@@ -84,7 +84,7 @@ To manually install a Plugin via the Grafana.com API:
|
||||
{{< imgbox img="/img/docs/installation-tab.png" caption="Installation Tab" >}}
|
||||
|
||||
2. Use the Grafana API to find the plugin using this url `https://grafana.com/api/plugins/<plugin id from step 1>`. For example: https://grafana.com/api/plugins/jdbranham-diagram-panel should return:
|
||||
```
|
||||
```bash
|
||||
{
|
||||
"id": 145,
|
||||
"typeId": 3,
|
||||
@@ -97,7 +97,7 @@ To manually install a Plugin via the Grafana.com API:
|
||||
```
|
||||
|
||||
3. Find the download link:
|
||||
```
|
||||
```bash
|
||||
{
|
||||
"rel": "download",
|
||||
"href": "/plugins/jdbranham-diagram-panel/versions/1.4.0/download"
|
||||
|
||||
@@ -13,27 +13,27 @@ dev environment. Grafana ships with its own required backend server; also comple
|
||||
|
||||
## Dependencies
|
||||
|
||||
- [Go 1.9](https://golang.org/dl/)
|
||||
- [Go 1.9.1](https://golang.org/dl/)
|
||||
- [NodeJS LTS](https://nodejs.org/download/)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
|
||||
## Get Code
|
||||
Create a directory for the project and set your path accordingly (or use the [default Go workspace directory](https://golang.org/doc/code.html#GOPATH)). Then download and install Grafana into your $GOPATH directory:
|
||||
|
||||
```
|
||||
```bash
|
||||
export GOPATH=`pwd`
|
||||
go get github.com/grafana/grafana
|
||||
```
|
||||
|
||||
On Windows use setx instead of export and then restart your command prompt:
|
||||
```
|
||||
```bash
|
||||
setx GOPATH %cd%
|
||||
```
|
||||
|
||||
You may see an error such as: `package github.com/grafana/grafana: no buildable Go source files`. This is just a warning, and you can proceed with the directions.
|
||||
|
||||
## Building the backend
|
||||
```
|
||||
```bash
|
||||
cd $GOPATH/src/github.com/grafana/grafana
|
||||
go run build.go setup
|
||||
go run build.go build # (or 'go build ./pkg/cmd/grafana-server')
|
||||
@@ -45,7 +45,7 @@ to install GCC. We recommend [TDM-GCC](http://tdm-gcc.tdragon.net/download).
|
||||
|
||||
[node-gyp](https://github.com/nodejs/node-gyp#installation) is the Node.js native addon build tool and it requires extra dependencies to be installed on Windows. In a command prompt which is run as administrator, run:
|
||||
|
||||
```
|
||||
```bash
|
||||
npm --add-python-to-path='true' --debug install --global windows-build-tools
|
||||
```
|
||||
|
||||
@@ -53,7 +53,7 @@ npm --add-python-to-path='true' --debug install --global windows-build-tools
|
||||
|
||||
For this you need nodejs (v.6+).
|
||||
|
||||
```
|
||||
```bash
|
||||
npm install -g yarn
|
||||
yarn install --pure-lockfile
|
||||
npm run build
|
||||
@@ -62,7 +62,7 @@ npm run build
|
||||
## Running Grafana Locally
|
||||
You can run a local instance of Grafana by running:
|
||||
|
||||
```
|
||||
```bash
|
||||
./bin/grafana-server
|
||||
```
|
||||
If you built the binary with `go run build.go build`, run `./bin/grafana-server`
|
||||
@@ -76,7 +76,7 @@ Open grafana in your browser (default [http://localhost:3000](http://localhost:3
|
||||
To add features, customize your config, etc, you'll need to rebuild the backend when you change the source code. We use a tool named `bra` that
|
||||
does this.
|
||||
|
||||
```
|
||||
```bash
|
||||
go get github.com/Unknwon/bra
|
||||
|
||||
bra run
|
||||
@@ -88,7 +88,7 @@ You'll also need to run `npm run watch` to watch for changes to the front-end (t
|
||||
|
||||
This step builds linux packages and requires that fpm is installed. Install fpm via `gem install fpm`.
|
||||
|
||||
```
|
||||
```bash
|
||||
go run build.go build package
|
||||
```
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized
|
||||
|
||||
> Note: In the following JSON, id is shown as null which is the default value assigned to it until a dashboard is saved. Once a dashboard is saved, an integer value is assigned to the `id` field.
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"id": null,
|
||||
"title": "New dashboard",
|
||||
|
||||
@@ -43,7 +43,7 @@ You also get a link to service side rendered PNG of the panel. Useful if you wan
|
||||
|
||||
Example of a link to a server-side rendered PNG:
|
||||
|
||||
```
|
||||
```bash
|
||||
http://play.grafana.org/render/dashboard-solo/db/grafana-play-home?orgId=1&panelId=4&from=1499272191563&to=1499279391563&width=1000&height=500&tz=UTC%2B02%3A00&timeout=5000
|
||||
```
|
||||
|
||||
|
||||
@@ -22,24 +22,24 @@ Some parts of the API are only available through basic authentication and these
|
||||
The task is to create a new organization and then add a Token that can be used by other users. In the examples below which use basic auth, the user is `admin` and the password is `admin`.
|
||||
|
||||
1. [Create the org](http://docs.grafana.org/http_api/org/#create-organisation). Here is an example using curl:
|
||||
```
|
||||
```bash
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"name":"apiorg"}' http://admin:admin@localhost:3000/api/orgs
|
||||
```
|
||||
|
||||
This should return a response: `{"message":"Organization created","orgId":6}`. Use the orgId for the next steps.
|
||||
|
||||
2. Optional step. If the org was created previously and/or step 3 fails then first [add your Admin user to the org](http://docs.grafana.org/http_api/org/#add-user-in-organisation):
|
||||
```
|
||||
```bash
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"loginOrEmail":"admin", "role": "Admin"}' http://admin:admin@localhost:3000/api/orgs/<org id of new org>/users
|
||||
```
|
||||
|
||||
3. [Switch the org context for the Admin user to the new org](http://docs.grafana.org/http_api/user/#switch-user-context):
|
||||
```
|
||||
```bash
|
||||
curl -X POST http://admin:admin@localhost:3000/api/user/using/<id of new org>
|
||||
```
|
||||
|
||||
4. [Create the API token](http://docs.grafana.org/http_api/auth/#create-api-key):
|
||||
```
|
||||
```bash
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"name":"apikeycurl", "role": "Admin"}' http://admin:admin@localhost:3000/api/auth/keys
|
||||
```
|
||||
|
||||
@@ -53,7 +53,7 @@ Using the Token that was created in the previous step, you can create a dashboar
|
||||
|
||||
1. [Add a dashboard](http://docs.grafana.org/http_api/dashboard/#create-update-dashboard) using the key (or bearer token as it is also called):
|
||||
|
||||
```
|
||||
```bash
|
||||
curl -X POST --insecure -H "Authorization: Bearer eyJrIjoiR0ZXZmt1UFc0OEpIOGN5RWdUalBJTllUTk83VlhtVGwiLCJuIjoiYXBpa2V5Y3VybCIsImlkIjo2fQ==" -H "Content-Type: application/json" -d '{
|
||||
"dashboard": {
|
||||
"id": null,
|
||||
|
||||
243
docs/sources/tutorials/authproxy.md
Normal file
243
docs/sources/tutorials/authproxy.md
Normal file
@@ -0,0 +1,243 @@
|
||||
+++
|
||||
title = "Grafana Authproxy"
|
||||
type = "docs"
|
||||
keywords = ["grafana", "tutorials", "authproxy"]
|
||||
[menu.docs]
|
||||
parent = "tutorials"
|
||||
weight = 10
|
||||
+++
|
||||
|
||||
# Grafana Authproxy
|
||||
|
||||
AuthProxy allows you to offload the authentication of users to a web server (there are many reasons why you’d want to run a web server in front of a production version of Grafana, especially if it’s exposed to the Internet).
|
||||
|
||||
Popular web servers have a very extensive list of pluggable authentication modules, and any of them can be used with the AuthProxy feature.
|
||||
|
||||
The Grafana AuthProxy feature is very simple in design, but it is this simplicity that makes it so powerful.
|
||||
|
||||
## Interacting with Grafana’s AuthProxy via curl
|
||||
|
||||
The AuthProxy feature can be configured through the Grafana configuration file with the following options:
|
||||
|
||||
```js
|
||||
[auth.proxy]
|
||||
enabled = true
|
||||
header_name = X-WEBAUTH-USER
|
||||
header_property = username
|
||||
auto_sign_up = true
|
||||
```
|
||||
|
||||
* **enabled**: this is to toggle the feature on or off
|
||||
* **header_name**: this is the HTTP header name that passes the username or email address of the authenticated user to Grafana. Grafana will trust what ever username is contained in this header and automatically log the user in.
|
||||
* **header_property**: this tells Grafana whether the value in the header_name is a username or an email address. (In Grafana you can log in using your account username or account email)
|
||||
* **auto_sign_up**: If set to true, Grafana will automatically create user accounts in the Grafana DB if one does not exist. If set to false, users who do not exist in the GrafanaDB won’t be able to log in, even though their username and password are valid.
|
||||
|
||||
With a fresh install of Grafana, using the above configuration for the authProxy feature, we can send a simple API call to list all users. The only user that will be present is the default “Admin” user that is added the first time Grafana starts up. As you can see all we need to do to authenticate the request is to provide the “X-WEBAUTH-USER” header.
|
||||
|
||||
```bash
|
||||
curl -H "X-WEBAUTH-USER: admin" http://localhost:3000/api/users
|
||||
[
|
||||
{
|
||||
"id":1,
|
||||
"name":"",
|
||||
"login":"admin",
|
||||
"email":"admin@localhost",
|
||||
"isAdmin":true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
We can then send a second request to the `/api/user` method which will return the details of the logged in user. We will use this request to show how Grafana automatically adds the new user we specify to the system. Here we create a new user called “anthony”.
|
||||
|
||||
```bash
|
||||
curl -H "X-WEBAUTH-USER: anthony" http://localhost:3000/api/user
|
||||
{
|
||||
"email":"anthony",
|
||||
"name":"",
|
||||
"login":"anthony",
|
||||
"theme":"",
|
||||
"orgId":1,
|
||||
"isGrafanaAdmin":false
|
||||
}
|
||||
```
|
||||
|
||||
## Making Apache’s auth work together with Grafana’s AuthProxy
|
||||
|
||||
I’ll demonstrate how to use Apache for authenticating users. In this example we use BasicAuth with Apache’s text file based authentication handler, i.e. htpasswd files. However, any available Apache authentication capabilities could be used.
|
||||
|
||||
### Apache BasicAuth
|
||||
|
||||
In this example we use Apache as a reverseProxy in front of Grafana. Apache handles the Authentication of users before forwarding requests to the Grafana backend service.
|
||||
|
||||
#### Apache configuration
|
||||
|
||||
```bash
|
||||
<VirtualHost *:80>
|
||||
ServerAdmin webmaster@authproxy
|
||||
ServerName authproxy
|
||||
ErrorLog "logs/authproxy-error_log"
|
||||
CustomLog "logs/authproxy-access_log" common
|
||||
|
||||
<Proxy *>
|
||||
AuthType Basic
|
||||
AuthName GrafanaAuthProxy
|
||||
AuthBasicProvider file
|
||||
AuthUserFile /etc/apache2/grafana_htpasswd
|
||||
Require valid-user
|
||||
|
||||
RewriteEngine On
|
||||
RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER},NS]
|
||||
RequestHeader set X-WEBAUTH-USER "%{PROXY_USER}e"
|
||||
</Proxy>
|
||||
|
||||
RequestHeader unset Authorization
|
||||
|
||||
ProxyRequests Off
|
||||
ProxyPass / http://localhost:3000/
|
||||
ProxyPassReverse / http://localhost:3000/
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
* The first 4 lines of the virtualhost configuration are standard, so we won’t go into detail on what they do.
|
||||
|
||||
* We use a **\<proxy>** configuration block for applying our authentication rules to every proxied request. These rules include requiring basic authentication where user:password credentials are stored in the **/etc/apache2/grafana_htpasswd** file. This file can be created with the `htpasswd` command.
|
||||
|
||||
* The next part of the configuration is the tricky part. We use Apache’s rewrite engine to create our **X-WEBAUTH-USER header**, populated with the authenticated user.
|
||||
|
||||
* **RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER}, NS]**: This line is a little bit of magic. What it does, is for every request use the rewriteEngines look-ahead (LA-U) feature to determine what the REMOTE_USER variable would be set to after processing the request. Then assign the result to the variable PROXY_USER. This is neccessary as the REMOTE_USER variable is not available to the RequestHeader function.
|
||||
|
||||
* **RequestHeader set X-WEBAUTH-USER “%{PROXY_USER}e”**: With the authenticated username now stored in the PROXY_USER variable, we create a new HTTP request header that will be sent to our backend Grafana containing the username.
|
||||
|
||||
* The **RequestHeader unset Authorization** removes the Authorization header from the HTTP request before it is forwarded to Grafana. This ensures that Grafana does not try to authenticate the user using these credentials (BasicAuth is a supported authentication handler in Grafana).
|
||||
|
||||
* The last 3 lines are then just standard reverse proxy configuration to direct all authenticated requests to our Grafana server running on port 3000.
|
||||
|
||||
#### Grafana configuration
|
||||
|
||||
```bash
|
||||
############# Users ################
|
||||
[users]
|
||||
# disable user signup / registration
|
||||
allow_sign_up = false
|
||||
|
||||
# Set to true to automatically assign new users to the default organization (id 1)
|
||||
auto_assign_org = true
|
||||
|
||||
# Default role new users will be automatically assigned (if auto_assign_org above is set to true)
|
||||
auto_assign_org_role = Editor
|
||||
|
||||
|
||||
############ Auth Proxy ########
|
||||
[auth.proxy]
|
||||
enabled = true
|
||||
|
||||
# the Header name that contains the authenticated user.
|
||||
header_name = X-WEBAUTH-USER
|
||||
|
||||
# does the user authenticate against the proxy using a 'username' or an 'email'
|
||||
header_property = username
|
||||
|
||||
# automatically add the user to the system if they don't already exist.
|
||||
auto_sign_up = true
|
||||
```
|
||||
|
||||
#### Full walk through using Docker.
|
||||
|
||||
##### Grafana Container
|
||||
|
||||
For this example, we use the offical Grafana docker image available at [Docker Hub](https://hub.docker.com/r/grafana/grafana/)
|
||||
|
||||
* Create a file `grafana.ini` with the following contents
|
||||
|
||||
```bash
|
||||
[users]
|
||||
allow_sign_up = false
|
||||
auto_assign_org = true
|
||||
auto_assign_org_role = Editor
|
||||
|
||||
[auth.proxy]
|
||||
enabled = true
|
||||
header_name = X-WEBAUTH-USER
|
||||
header_property = username
|
||||
auto_sign_up = true
|
||||
```
|
||||
|
||||
* Launch the Grafana container, using our custom grafana.ini to replace `/etc/grafana/grafana.ini`. We dont expose any ports for this container as it will only be connected to by our Apache container.
|
||||
|
||||
```bash
|
||||
docker run -i -v $(pwd)/grafana.ini:/etc/grafana/grafana.ini --name grafana grafana/grafana
|
||||
```
|
||||
|
||||
### Apache Container
|
||||
|
||||
For this example we use the offical Apache docker image available at [Docker Hub](https://hub.docker.com/_/httpd/)
|
||||
|
||||
* Create a file `httpd.conf` with the following contents
|
||||
|
||||
```bash
|
||||
ServerRoot "/usr/local/apache2"
|
||||
Listen 80
|
||||
LoadModule authn_file_module modules/mod_authn_file.so
|
||||
LoadModule authn_core_module modules/mod_authn_core.so
|
||||
LoadModule authz_host_module modules/mod_authz_host.so
|
||||
LoadModule authz_user_module modules/mod_authz_user.so
|
||||
LoadModule authz_core_module modules/mod_authz_core.so
|
||||
LoadModule auth_basic_module modules/mod_auth_basic.so
|
||||
LoadModule log_config_module modules/mod_log_config.so
|
||||
LoadModule env_module modules/mod_env.so
|
||||
LoadModule headers_module modules/mod_headers.so
|
||||
LoadModule unixd_module modules/mod_unixd.so
|
||||
LoadModule rewrite_module modules/mod_rewrite.so
|
||||
LoadModule proxy_module modules/mod_proxy.so
|
||||
LoadModule proxy_http_module modules/mod_proxy_http.so
|
||||
<IfModule unixd_module>
|
||||
User daemon
|
||||
Group daemon
|
||||
</IfModule>
|
||||
ServerAdmin you@example.com
|
||||
<Directory />
|
||||
AllowOverride none
|
||||
Require all denied
|
||||
</Directory>
|
||||
DocumentRoot "/usr/local/apache2/htdocs"
|
||||
ErrorLog /proc/self/fd/2
|
||||
LogLevel error
|
||||
<IfModule log_config_module>
|
||||
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
|
||||
LogFormat "%h %l %u %t \"%r\" %>s %b" common
|
||||
<IfModule logio_module>
|
||||
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
|
||||
</IfModule>
|
||||
CustomLog /proc/self/fd/1 common
|
||||
</IfModule>
|
||||
<Proxy *>
|
||||
AuthType Basic
|
||||
AuthName GrafanaAuthProxy
|
||||
AuthBasicProvider file
|
||||
AuthUserFile /tmp/htpasswd
|
||||
Require valid-user
|
||||
RewriteEngine On
|
||||
RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER},NS]
|
||||
RequestHeader set X-WEBAUTH-USER "%{PROXY_USER}e"
|
||||
</Proxy>
|
||||
RequestHeader unset Authorization
|
||||
ProxyRequests Off
|
||||
ProxyPass / http://grafana:3000/
|
||||
ProxyPassReverse / http://grafana:3000/
|
||||
```
|
||||
|
||||
* Create a htpasswd file. We create a new user **anthony** with the password **password**
|
||||
|
||||
```bash
|
||||
htpasswd -bc htpasswd anthony password
|
||||
```
|
||||
|
||||
* Launch the httpd container using our custom httpd.conf and our htpasswd file. The container will listen on port 80, and we create a link to the **grafana** container so that this container can resolve the hostname **grafana** to the grafana container’s ip address.
|
||||
|
||||
```bash
|
||||
docker run -i -p 80:80 --link grafana:grafana -v $(pwd)/httpd.conf:/usr/local/apache2/conf/httpd.conf -v $(pwd)/htpasswd:/tmp/htpasswd httpd:2.4
|
||||
```
|
||||
|
||||
### Use grafana.
|
||||
|
||||
With our Grafana and Apache containers running, you can now connect to http://localhost/ and log in using the username/password we created in the htpasswd file.
|
||||
@@ -39,9 +39,9 @@ read the official [Getting Started With Hubot](https://hubot.github.com/docs/) g
|
||||
## Install Hubot-Grafana script
|
||||
|
||||
In your Hubot project repo install the Grafana plugin using `npm`:
|
||||
|
||||
```bash
|
||||
npm install hubot-grafana --save
|
||||
|
||||
```
|
||||
Edit the file external-scripts.json, and add hubot-grafana to the list of plugins.
|
||||
|
||||
```json
|
||||
@@ -56,6 +56,7 @@ Edit the file external-scripts.json, and add hubot-grafana to the list of plugin
|
||||
|
||||
The `hubot-grafana` plugin requires a number of environment variables to be set in order to work properly.
|
||||
|
||||
```bash
|
||||
export HUBOT_GRAFANA_HOST=http://play.grafana.org
|
||||
export HUBOT_GRAFANA_API_KEY=abcd01234deadbeef01234
|
||||
export HUBOT_GRAFANA_S3_BUCKET=mybucket
|
||||
@@ -63,6 +64,7 @@ The `hubot-grafana` plugin requires a number of environment variables to be set
|
||||
export HUBOT_GRAFANA_S3_SECRET_ACCESS_KEY=aBcD01234dEaDbEef01234
|
||||
export HUBOT_GRAFANA_S3_PREFIX=graphs
|
||||
export HUBOT_GRAFANA_S3_REGION=us-standard
|
||||
```
|
||||
|
||||
### Grafana server side rendering
|
||||
|
||||
@@ -112,7 +114,9 @@ can create hubot command aliases with the hubot script `hubot-alias`.
|
||||
|
||||
Install it:
|
||||
|
||||
```bash
|
||||
npm i --save hubot-alias
|
||||
```
|
||||
|
||||
Now add `hubot-alias` to the list of plugins in `external-scripts.json` and restart hubot.
|
||||
|
||||
|
||||
29
package.json
29
package.json
@@ -15,7 +15,9 @@
|
||||
"@types/node": "^8.0.31",
|
||||
"@types/react": "^16.0.5",
|
||||
"@types/react-dom": "^15.5.4",
|
||||
"angular-mocks": "^1.6.6",
|
||||
"autoprefixer": "^6.4.0",
|
||||
"awesome-typescript-loader": "^3.2.3",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
@@ -50,6 +52,7 @@
|
||||
"grunt-webpack": "^3.0.2",
|
||||
"html-loader": "^0.5.1",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"husky": "^0.14.3",
|
||||
"jshint-stylish": "~2.2.1",
|
||||
"json-loader": "^0.5.7",
|
||||
"karma": "1.7.0",
|
||||
@@ -61,17 +64,19 @@
|
||||
"karma-sinon": "^1.0.5",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-webpack": "^2.0.4",
|
||||
"lint-staged": "^4.2.3",
|
||||
"load-grunt-tasks": "3.5.2",
|
||||
"mocha": "3.5.0",
|
||||
"mocha": "^4.0.1",
|
||||
"ng-annotate-loader": "^0.6.1",
|
||||
"ng-annotate-webpack-plugin": "^0.2.1-pre",
|
||||
"ngtemplate-loader": "^2.0.1",
|
||||
"npm": "^5.4.2",
|
||||
"phantomjs-prebuilt": "^2.1.15",
|
||||
"postcss-browser-reporter": "^0.5.0",
|
||||
"postcss-loader": "^2.0.6",
|
||||
"postcss-reporter": "^5.0.0",
|
||||
"prettier": "1.7.3",
|
||||
"react-test-renderer": "^16.0.0",
|
||||
"rxjs": "^5.4.3",
|
||||
"sass-lint": "^1.10.2",
|
||||
"sass-loader": "^6.0.6",
|
||||
"sinon": "1.17.6",
|
||||
@@ -85,34 +90,32 @@
|
||||
"webpack-bundle-analyzer": "^2.9.0",
|
||||
"webpack-cleanup-plugin": "^0.5.1",
|
||||
"webpack-merge": "^4.1.0",
|
||||
"zone.js": "^0.7.2",
|
||||
"awesome-typescript-loader": "^3.2.3",
|
||||
"angular-mocks": "^1.6.6",
|
||||
"karma-sinon": "^1.0.5",
|
||||
"npm": "^5.4.2"
|
||||
"zone.js": "^0.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "./node_modules/.bin/webpack --progress --colors --config scripts/webpack/webpack.dev.js",
|
||||
"watch": "./node_modules/.bin/webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js",
|
||||
"build": "./node_modules/grunt-cli/bin/grunt build",
|
||||
"test": "./node_modules/grunt-cli/bin/grunt test",
|
||||
"build": "./node_modules/.bin/grunt build",
|
||||
"test": "./node_modules/.bin/grunt test",
|
||||
"lint": "./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --type-check",
|
||||
"watch-test": "./node_modules/grunt-cli/bin/grunt karma:dev"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"jquery": "^3.2.1",
|
||||
"angular": "^1.6.6",
|
||||
"angular-bindonce": "^0.3.1",
|
||||
"angular-mocks": "^1.6.6",
|
||||
"angular-native-dragdrop": "^1.2.2",
|
||||
"angular-route": "^1.6.6",
|
||||
"angular-sanitize": "^1.6.6",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"brace": "^0.10.0",
|
||||
"clipboard": "^1.7.1",
|
||||
"eventemitter3": "^2.0.2",
|
||||
"gridstack": "https://github.com/grafana/gridstack.js#grafana",
|
||||
"gemini-scrollbar": "https://github.com/grafana/gemini-scrollbar#grafana",
|
||||
"file-saver": "^1.3.3",
|
||||
"jquery": "^3.2.1",
|
||||
"lodash": "^4.17.4",
|
||||
"moment": "^2.18.1",
|
||||
"mousetrap": "^1.6.0",
|
||||
@@ -120,7 +123,9 @@
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"remarkable": "^1.7.1",
|
||||
"rxjs": "^5.4.3",
|
||||
"tether": "^1.4.0",
|
||||
"tether-drop": "https://github.com/torkelo/drop"
|
||||
"tether-drop": "https://github.com/torkelo/drop",
|
||||
"tinycolor2": "^1.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
)
|
||||
@@ -11,13 +12,12 @@ func GetAnnotations(c *middleware.Context) Response {
|
||||
query := &annotations.ItemQuery{
|
||||
From: c.QueryInt64("from") / 1000,
|
||||
To: c.QueryInt64("to") / 1000,
|
||||
Type: annotations.ItemType(c.Query("type")),
|
||||
OrgId: c.OrgId,
|
||||
AlertId: c.QueryInt64("alertId"),
|
||||
DashboardId: c.QueryInt64("dashboardId"),
|
||||
PanelId: c.QueryInt64("panelId"),
|
||||
Limit: c.QueryInt64("limit"),
|
||||
NewState: c.QueryStrings("newState"),
|
||||
Tags: c.QueryStrings("tags"),
|
||||
}
|
||||
|
||||
repo := annotations.GetRepository()
|
||||
@@ -27,25 +27,14 @@ func GetAnnotations(c *middleware.Context) Response {
|
||||
return ApiError(500, "Failed to get annotations", err)
|
||||
}
|
||||
|
||||
result := make([]dtos.Annotation, 0)
|
||||
|
||||
for _, item := range items {
|
||||
result = append(result, dtos.Annotation{
|
||||
AlertId: item.AlertId,
|
||||
Time: item.Epoch * 1000,
|
||||
Data: item.Data,
|
||||
NewState: item.NewState,
|
||||
PrevState: item.PrevState,
|
||||
Text: item.Text,
|
||||
Metric: item.Metric,
|
||||
Title: item.Title,
|
||||
PanelId: item.PanelId,
|
||||
RegionId: item.RegionId,
|
||||
Type: string(item.Type),
|
||||
})
|
||||
if item.Email != "" {
|
||||
item.AvatarUrl = dtos.GetGravatarUrl(item.Email)
|
||||
}
|
||||
item.Time = item.Time * 1000
|
||||
}
|
||||
|
||||
return Json(200, result)
|
||||
return Json(200, items)
|
||||
}
|
||||
|
||||
func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response {
|
||||
@@ -53,14 +42,13 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response
|
||||
|
||||
item := annotations.Item{
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
DashboardId: cmd.DashboardId,
|
||||
PanelId: cmd.PanelId,
|
||||
Epoch: cmd.Time / 1000,
|
||||
Title: cmd.Title,
|
||||
Text: cmd.Text,
|
||||
CategoryId: cmd.CategoryId,
|
||||
NewState: cmd.FillColor,
|
||||
Type: annotations.EventType,
|
||||
Data: cmd.Data,
|
||||
Tags: cmd.Tags,
|
||||
}
|
||||
|
||||
if err := repo.Save(&item); err != nil {
|
||||
@@ -71,12 +59,16 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response
|
||||
if cmd.IsRegion {
|
||||
item.RegionId = item.Id
|
||||
|
||||
if item.Data == nil {
|
||||
item.Data = simplejson.New()
|
||||
}
|
||||
|
||||
if err := repo.Update(&item); err != nil {
|
||||
return ApiError(500, "Failed set regionId on annotation", err)
|
||||
}
|
||||
|
||||
item.Id = 0
|
||||
item.Epoch = cmd.TimeEnd
|
||||
item.Epoch = cmd.TimeEnd / 1000
|
||||
|
||||
if err := repo.Save(&item); err != nil {
|
||||
return ApiError(500, "Failed save annotation for region end time", err)
|
||||
@@ -86,6 +78,41 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response
|
||||
return ApiSuccess("Annotation added")
|
||||
}
|
||||
|
||||
func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Response {
|
||||
annotationId := c.ParamsInt64(":annotationId")
|
||||
|
||||
repo := annotations.GetRepository()
|
||||
|
||||
item := annotations.Item{
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
Id: annotationId,
|
||||
Epoch: cmd.Time / 1000,
|
||||
Text: cmd.Text,
|
||||
Tags: cmd.Tags,
|
||||
}
|
||||
|
||||
if err := repo.Update(&item); err != nil {
|
||||
return ApiError(500, "Failed to update annotation", err)
|
||||
}
|
||||
|
||||
if cmd.IsRegion {
|
||||
itemRight := item
|
||||
itemRight.RegionId = item.Id
|
||||
itemRight.Epoch = cmd.TimeEnd / 1000
|
||||
|
||||
// We don't know id of region right event, so set it to 0 and find then using query like
|
||||
// ... WHERE region_id = <item.RegionId> AND id != <item.RegionId> ...
|
||||
itemRight.Id = 0
|
||||
|
||||
if err := repo.Update(&itemRight); err != nil {
|
||||
return ApiError(500, "Failed to update annotation for region end time", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ApiSuccess("Annotation updated")
|
||||
}
|
||||
|
||||
func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Response {
|
||||
repo := annotations.GetRepository()
|
||||
|
||||
@@ -101,3 +128,33 @@ func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Res
|
||||
|
||||
return ApiSuccess("Annotations deleted")
|
||||
}
|
||||
|
||||
func DeleteAnnotationById(c *middleware.Context) Response {
|
||||
repo := annotations.GetRepository()
|
||||
annotationId := c.ParamsInt64(":annotationId")
|
||||
|
||||
err := repo.Delete(&annotations.DeleteParams{
|
||||
Id: annotationId,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to delete annotation", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Annotation deleted")
|
||||
}
|
||||
|
||||
func DeleteAnnotationRegion(c *middleware.Context) Response {
|
||||
repo := annotations.GetRepository()
|
||||
regionId := c.ParamsInt64(":regionId")
|
||||
|
||||
err := repo.Delete(&annotations.DeleteParams{
|
||||
RegionId: regionId,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to delete annotation region", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Annotation region deleted")
|
||||
}
|
||||
|
||||
@@ -301,6 +301,9 @@ func (hs *HttpServer) registerRoutes() {
|
||||
|
||||
apiRoute.Group("/annotations", func(annotationsRoute RouteRegister) {
|
||||
annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), wrap(PostAnnotation))
|
||||
annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationById))
|
||||
annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation))
|
||||
annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion))
|
||||
}, reqEditorRole)
|
||||
|
||||
// error test
|
||||
|
||||
@@ -2,31 +2,22 @@ package dtos
|
||||
|
||||
import "github.com/grafana/grafana/pkg/components/simplejson"
|
||||
|
||||
type Annotation struct {
|
||||
AlertId int64 `json:"alertId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
NewState string `json:"newState"`
|
||||
PrevState string `json:"prevState"`
|
||||
Time int64 `json:"time"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
Metric string `json:"metric"`
|
||||
RegionId int64 `json:"regionId"`
|
||||
Type string `json:"type"`
|
||||
|
||||
Data *simplejson.Json `json:"data"`
|
||||
}
|
||||
|
||||
type PostAnnotationsCmd struct {
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
CategoryId int64 `json:"categoryId"`
|
||||
Time int64 `json:"time"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
Tags []string `json:"tags"`
|
||||
Data *simplejson.Json `json:"data"`
|
||||
IsRegion bool `json:"isRegion"`
|
||||
TimeEnd int64 `json:"timeEnd"`
|
||||
}
|
||||
|
||||
FillColor string `json:"fillColor"`
|
||||
type UpdateAnnotationsCmd struct {
|
||||
Id int64 `json:"id"`
|
||||
Time int64 `json:"time"`
|
||||
Text string `json:"text"`
|
||||
Tags []string `json:"tags"`
|
||||
IsRegion bool `json:"isRegion"`
|
||||
TimeEnd int64 `json:"timeEnd"`
|
||||
}
|
||||
@@ -35,4 +26,6 @@ type DeleteAnnotationsCmd struct {
|
||||
AlertId int64 `json:"alertId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
AnnotationId int64 `json:"annotationId"`
|
||||
RegionId int64 `json:"regionId"`
|
||||
}
|
||||
|
||||
@@ -158,7 +158,9 @@ func GetPluginMarkdown(c *middleware.Context) Response {
|
||||
|
||||
return ApiError(500, "Could not get markdown file", err)
|
||||
} else {
|
||||
return Respond(200, content)
|
||||
resp := Respond(200, content)
|
||||
resp.Header("Content-Type", "text/plain; charset=utf-8")
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,9 +30,15 @@ func NewImageUploader() (ImageUploader, error) {
|
||||
|
||||
bucket := s3sec.Key("bucket").MustString("")
|
||||
region := s3sec.Key("region").MustString("")
|
||||
path := s3sec.Key("path").MustString("")
|
||||
bucketUrl := s3sec.Key("bucket_url").MustString("")
|
||||
accessKey := s3sec.Key("access_key").MustString("")
|
||||
secretKey := s3sec.Key("secret_key").MustString("")
|
||||
|
||||
if path != "" && path[len(path)-1:] != "/" {
|
||||
path += "/"
|
||||
}
|
||||
|
||||
if bucket == "" || region == "" {
|
||||
info, err := getRegionAndBucketFromUrl(bucketUrl)
|
||||
if err != nil {
|
||||
@@ -42,7 +48,7 @@ func NewImageUploader() (ImageUploader, error) {
|
||||
region = info.region
|
||||
}
|
||||
|
||||
return NewS3Uploader(region, bucket, "public-read", accessKey, secretKey), nil
|
||||
return NewS3Uploader(region, bucket, path, "public-read", accessKey, secretKey), nil
|
||||
case "webdav":
|
||||
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
|
||||
if err != nil {
|
||||
|
||||
@@ -19,16 +19,18 @@ import (
|
||||
type S3Uploader struct {
|
||||
region string
|
||||
bucket string
|
||||
path string
|
||||
acl string
|
||||
secretKey string
|
||||
accessKey string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewS3Uploader(region, bucket, acl, accessKey, secretKey string) *S3Uploader {
|
||||
func NewS3Uploader(region, bucket, path, acl, accessKey, secretKey string) *S3Uploader {
|
||||
return &S3Uploader{
|
||||
region: region,
|
||||
bucket: bucket,
|
||||
path: path,
|
||||
acl: acl,
|
||||
accessKey: accessKey,
|
||||
secretKey: secretKey,
|
||||
@@ -56,7 +58,7 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string,
|
||||
}
|
||||
|
||||
s3_endpoint, _ := endpoints.DefaultResolver().EndpointFor("s3", u.region)
|
||||
key := util.GetRandomString(20) + ".png"
|
||||
key := u.path + util.GetRandomString(20) + ".png"
|
||||
image_url := s3_endpoint.URL + "/" + u.bucket + "/" + key
|
||||
log.Debug("Uploading image to s3", "url = ", image_url)
|
||||
|
||||
|
||||
60
pkg/models/tags.go
Normal file
60
pkg/models/tags.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
Id int64
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func ParseTagPairs(tagPairs []string) (tags []*Tag) {
|
||||
if tagPairs == nil {
|
||||
return []*Tag{}
|
||||
}
|
||||
|
||||
for _, tagPair := range tagPairs {
|
||||
var tag Tag
|
||||
|
||||
if strings.Contains(tagPair, ":") {
|
||||
keyValue := strings.Split(tagPair, ":")
|
||||
tag.Key = strings.Trim(keyValue[0], " ")
|
||||
tag.Value = strings.Trim(keyValue[1], " ")
|
||||
} else {
|
||||
tag.Key = strings.Trim(tagPair, " ")
|
||||
}
|
||||
|
||||
if tag.Key == "" || ContainsTag(tags, &tag) {
|
||||
continue
|
||||
}
|
||||
|
||||
tags = append(tags, &tag)
|
||||
}
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
func ContainsTag(existingTags []*Tag, tag *Tag) bool {
|
||||
for _, t := range existingTags {
|
||||
if t.Key == tag.Key && t.Value == tag.Value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func JoinTagPairs(tags []*Tag) []string {
|
||||
tagPairs := []string{}
|
||||
|
||||
for _, tag := range tags {
|
||||
if tag.Value != "" {
|
||||
tagPairs = append(tagPairs, tag.Key+":"+tag.Value)
|
||||
} else {
|
||||
tagPairs = append(tagPairs, tag.Key)
|
||||
}
|
||||
}
|
||||
|
||||
return tagPairs
|
||||
}
|
||||
95
pkg/models/tags_test.go
Normal file
95
pkg/models/tags_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestParsingTags(t *testing.T) {
|
||||
Convey("Testing parsing a tag pairs into tags", t, func() {
|
||||
Convey("Can parse one empty tag", func() {
|
||||
tags := ParseTagPairs([]string{""})
|
||||
So(len(tags), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Can parse valid tags", func() {
|
||||
tags := ParseTagPairs([]string{"outage", "type:outage", "error"})
|
||||
So(len(tags), ShouldEqual, 3)
|
||||
So(tags[0].Key, ShouldEqual, "outage")
|
||||
So(tags[0].Value, ShouldEqual, "")
|
||||
So(tags[1].Key, ShouldEqual, "type")
|
||||
So(tags[1].Value, ShouldEqual, "outage")
|
||||
So(tags[2].Key, ShouldEqual, "error")
|
||||
So(tags[2].Value, ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("Can parse tags with spaces", func() {
|
||||
tags := ParseTagPairs([]string{" outage ", " type : outage ", "error "})
|
||||
So(len(tags), ShouldEqual, 3)
|
||||
So(tags[0].Key, ShouldEqual, "outage")
|
||||
So(tags[0].Value, ShouldEqual, "")
|
||||
So(tags[1].Key, ShouldEqual, "type")
|
||||
So(tags[1].Value, ShouldEqual, "outage")
|
||||
So(tags[2].Key, ShouldEqual, "error")
|
||||
So(tags[2].Value, ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("Can parse empty tags", func() {
|
||||
tags := ParseTagPairs([]string{" outage ", "", "", ":", "type : outage ", "error ", "", ""})
|
||||
So(len(tags), ShouldEqual, 3)
|
||||
So(tags[0].Key, ShouldEqual, "outage")
|
||||
So(tags[0].Value, ShouldEqual, "")
|
||||
So(tags[1].Key, ShouldEqual, "type")
|
||||
So(tags[1].Value, ShouldEqual, "outage")
|
||||
So(tags[2].Key, ShouldEqual, "error")
|
||||
So(tags[2].Value, ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("Can parse tags with extra colons", func() {
|
||||
tags := ParseTagPairs([]string{" outage", "type : outage:outage2 :outage3 ", "error :"})
|
||||
So(len(tags), ShouldEqual, 3)
|
||||
So(tags[0].Key, ShouldEqual, "outage")
|
||||
So(tags[0].Value, ShouldEqual, "")
|
||||
So(tags[1].Key, ShouldEqual, "type")
|
||||
So(tags[1].Value, ShouldEqual, "outage")
|
||||
So(tags[2].Key, ShouldEqual, "error")
|
||||
So(tags[2].Value, ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("Can parse tags that contains key and values with spaces", func() {
|
||||
tags := ParseTagPairs([]string{" outage 1", "type 1: outage 1 ", "has error "})
|
||||
So(len(tags), ShouldEqual, 3)
|
||||
So(tags[0].Key, ShouldEqual, "outage 1")
|
||||
So(tags[0].Value, ShouldEqual, "")
|
||||
So(tags[1].Key, ShouldEqual, "type 1")
|
||||
So(tags[1].Value, ShouldEqual, "outage 1")
|
||||
So(tags[2].Key, ShouldEqual, "has error")
|
||||
So(tags[2].Value, ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("Can filter out duplicate tags", func() {
|
||||
tags := ParseTagPairs([]string{"test", "test", "key:val1", "key:val2"})
|
||||
So(len(tags), ShouldEqual, 3)
|
||||
So(tags[0].Key, ShouldEqual, "test")
|
||||
So(tags[0].Value, ShouldEqual, "")
|
||||
So(tags[1].Key, ShouldEqual, "key")
|
||||
So(tags[1].Value, ShouldEqual, "val1")
|
||||
So(tags[2].Key, ShouldEqual, "key")
|
||||
So(tags[2].Value, ShouldEqual, "val2")
|
||||
})
|
||||
|
||||
Convey("Can join tag pairs", func() {
|
||||
tagPairs := []*Tag{
|
||||
{Key: "key1", Value: "val1"},
|
||||
{Key: "key2", Value: ""},
|
||||
{Key: "key3"},
|
||||
}
|
||||
tags := JoinTagPairs(tagPairs)
|
||||
So(len(tags), ShouldEqual, 3)
|
||||
So(tags[0], ShouldEqual, "key1:val1")
|
||||
So(tags[1], ShouldEqual, "key2")
|
||||
So(tags[2], ShouldEqual, "key3")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -84,15 +84,17 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
message := evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text + "<br><a href=" + ruleUrl + ">Check Dashboard</a>"
|
||||
fields := make([]map[string]interface{}, 0)
|
||||
message += "<br>"
|
||||
attributes := make([]map[string]interface{}, 0)
|
||||
for index, evt := range evalContext.EvalMatches {
|
||||
message += evt.Metric + " :: " + strconv.FormatFloat(evt.Value.Float64, 'f', -1, 64) + "<br>"
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"title": evt.Metric,
|
||||
"value": evt.Value,
|
||||
"short": true,
|
||||
metricName := evt.Metric
|
||||
if len(metricName) > 50 {
|
||||
metricName = metricName[:50]
|
||||
}
|
||||
attributes = append(attributes, map[string]interface{}{
|
||||
"label": metricName,
|
||||
"value": map[string]interface{}{
|
||||
"label": strconv.FormatFloat(evt.Value.Float64, 'f', -1, 64),
|
||||
},
|
||||
})
|
||||
if index > maxFieldCount {
|
||||
break
|
||||
@@ -100,16 +102,23 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
}
|
||||
|
||||
if evalContext.Error != nil {
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"title": "Error message",
|
||||
"value": evalContext.Error.Error(),
|
||||
"short": false,
|
||||
attributes = append(attributes, map[string]interface{}{
|
||||
"label": "Error message",
|
||||
"value": map[string]interface{}{
|
||||
"label": evalContext.Error.Error(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
message := ""
|
||||
if evalContext.Rule.State != models.AlertStateOK { //dont add message when going back to alert state ok.
|
||||
message += " " + evalContext.Rule.Message
|
||||
}
|
||||
|
||||
if message == "" {
|
||||
message = evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text
|
||||
}
|
||||
|
||||
//HipChat has a set list of colors
|
||||
var color string
|
||||
switch evalContext.Rule.State {
|
||||
@@ -123,15 +132,24 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
|
||||
// Add a card with link to the dashboard
|
||||
card := map[string]interface{}{
|
||||
"style": "link",
|
||||
"style": "application",
|
||||
"url": ruleUrl,
|
||||
"id": "1",
|
||||
"title": evalContext.GetNotificationTitle(),
|
||||
"description": evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text,
|
||||
"description": message,
|
||||
"icon": map[string]interface{}{
|
||||
"url": "https://grafana.com/assets/img/fav32.png",
|
||||
},
|
||||
"date": evalContext.EndTime.Unix(),
|
||||
"attributes": attributes,
|
||||
}
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
card["thumbnail"] = map[string]interface{}{
|
||||
"url": evalContext.ImagePublicUrl,
|
||||
"url@2x": evalContext.ImagePublicUrl,
|
||||
"width": 1193,
|
||||
"height": 564,
|
||||
}
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
@@ -144,6 +162,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
|
||||
hipUrl := fmt.Sprintf("%s/v2/room/%s/notification?auth_token=%s", this.Url, this.RoomId, this.ApiKey)
|
||||
data, _ := json.Marshal(&body)
|
||||
this.log.Info("Request payload", "json", string(data))
|
||||
cmd := &models.SendWebhookSync{Url: hipUrl, Body: string(data)}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
|
||||
@@ -37,8 +37,7 @@ func init() {
|
||||
}
|
||||
|
||||
var (
|
||||
opsgenieCreateAlertURL string = "https://api.opsgenie.com/v1/json/alert"
|
||||
opsgenieCloseAlertURL string = "https://api.opsgenie.com/v1/json/alert/close"
|
||||
opsgenieAlertURL string = "https://api.opsgenie.com/v2/alerts"
|
||||
)
|
||||
|
||||
func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
@@ -87,7 +86,6 @@ func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) err
|
||||
}
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
bodyJSON.Set("apiKey", this.ApiKey)
|
||||
bodyJSON.Set("message", evalContext.Rule.Name)
|
||||
bodyJSON.Set("source", "Grafana")
|
||||
bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
|
||||
@@ -103,9 +101,13 @@ func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) err
|
||||
body, _ := bodyJSON.MarshalJSON()
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: opsgenieCreateAlertURL,
|
||||
Url: opsgenieAlertURL,
|
||||
Body: string(body),
|
||||
HttpMethod: "POST",
|
||||
HttpHeader: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": fmt.Sprintf("GenieKey %s", this.ApiKey),
|
||||
},
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
@@ -119,14 +121,17 @@ func (this *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) erro
|
||||
this.log.Info("Closing OpsGenie alert", "ruleId", evalContext.Rule.Id, "notification", this.Name)
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
bodyJSON.Set("apiKey", this.ApiKey)
|
||||
bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
|
||||
bodyJSON.Set("source", "Grafana")
|
||||
body, _ := bodyJSON.MarshalJSON()
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: opsgenieCloseAlertURL,
|
||||
Url: fmt.Sprintf("%s/alertId-%d/close?identifierType=alias", opsgenieAlertURL, evalContext.Rule.Id),
|
||||
Body: string(body),
|
||||
HttpMethod: "POST",
|
||||
HttpHeader: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": fmt.Sprintf("GenieKey %s", this.ApiKey),
|
||||
},
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
@@ -15,7 +19,7 @@ func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "slack",
|
||||
Name: "Slack",
|
||||
Description: "Sends notifications using Grafana server configured STMP settings",
|
||||
Description: "Sends notifications to Slack via Slack Webhooks",
|
||||
Factory: NewSlackNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Slack settings</h3>
|
||||
@@ -45,6 +49,17 @@ func init() {
|
||||
Mention a user or a group using @ when notifying in a channel
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-6">Token</span>
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.token"
|
||||
data-placement="right">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Provide a bot token to use the Slack file.upload API (starts with "xoxb")
|
||||
</info-popover>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
|
||||
@@ -58,12 +73,16 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
|
||||
recipient := model.Settings.Get("recipient").MustString()
|
||||
mention := model.Settings.Get("mention").MustString()
|
||||
token := model.Settings.Get("token").MustString()
|
||||
uploadImage := model.Settings.Get("uploadImage").MustBool(true)
|
||||
|
||||
return &SlackNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
Url: url,
|
||||
Recipient: recipient,
|
||||
Mention: mention,
|
||||
Token: token,
|
||||
Upload: uploadImage,
|
||||
log: log.New("alerting.notifier.slack"),
|
||||
}, nil
|
||||
}
|
||||
@@ -73,6 +92,8 @@ type SlackNotifier struct {
|
||||
Url string
|
||||
Recipient string
|
||||
Mention string
|
||||
Token string
|
||||
Upload bool
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
@@ -110,6 +131,11 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
|
||||
message += " " + evalContext.Rule.Message
|
||||
}
|
||||
image_url := ""
|
||||
// default to file.upload API method if a token is provided
|
||||
if this.Token == "" {
|
||||
image_url = evalContext.ImagePublicUrl
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"attachments": []map[string]interface{}{
|
||||
@@ -120,7 +146,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
"title_link": ruleUrl,
|
||||
"text": message,
|
||||
"fields": fields,
|
||||
"image_url": evalContext.ImagePublicUrl,
|
||||
"image_url": image_url,
|
||||
"footer": "Grafana v" + setting.BuildVersion,
|
||||
"footer_icon": "https://grafana.com/assets/img/fav32.png",
|
||||
"ts": time.Now().Unix(),
|
||||
@@ -133,14 +159,75 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
if this.Recipient != "" {
|
||||
body["channel"] = this.Recipient
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(&body)
|
||||
cmd := &m.SendWebhookSync{Url: this.Url, Body: string(data)}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send slack notification", "error", err, "webhook", this.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
if this.Token != "" && this.UploadImage {
|
||||
err = SlackFileUpload(evalContext, this.log, "https://slack.com/api/files.upload", this.Recipient, this.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SlackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error {
|
||||
if evalContext.ImageOnDiskPath == "" {
|
||||
evalContext.ImageOnDiskPath = "public/img/mixed_styles.png"
|
||||
}
|
||||
log.Info("Uploading to slack via file.upload API")
|
||||
headers, uploadBody, err := GenerateSlackBody(evalContext.ImageOnDiskPath, token, recipient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := &m.SendWebhookSync{Url: url, Body: uploadBody.String(), HttpHeader: headers, HttpMethod: "POST"}
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
log.Error("Failed to upload slack image", "error", err, "webhook", "file.upload")
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GenerateSlackBody(file string, token string, recipient string) (map[string]string, bytes.Buffer, error) {
|
||||
// Slack requires all POSTs to files.upload to present
|
||||
// an "application/x-www-form-urlencoded" encoded querystring
|
||||
// See https://api.slack.com/methods/files.upload
|
||||
var b bytes.Buffer
|
||||
w := multipart.NewWriter(&b)
|
||||
// Add the generated image file
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
}
|
||||
defer f.Close()
|
||||
fw, err := w.CreateFormFile("file", file)
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
}
|
||||
_, err = io.Copy(fw, f)
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
}
|
||||
// Add the authorization token
|
||||
err = w.WriteField("token", token)
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
}
|
||||
// Add the channel(s) to POST to
|
||||
err = w.WriteField("channels", recipient)
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
}
|
||||
w.Close()
|
||||
headers := map[string]string{
|
||||
"Content-Type": w.FormDataContentType(),
|
||||
"Authorization": "auth_token=\"" + token + "\"",
|
||||
}
|
||||
return headers, b, nil
|
||||
}
|
||||
|
||||
@@ -48,14 +48,16 @@ func TestSlackNotifier(t *testing.T) {
|
||||
So(slackNotifier.Url, ShouldEqual, "http://google.com")
|
||||
So(slackNotifier.Recipient, ShouldEqual, "")
|
||||
So(slackNotifier.Mention, ShouldEqual, "")
|
||||
So(slackNotifier.Token, ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("from settings with Recipient and Mention", func() {
|
||||
Convey("from settings with Recipient, Mention, and Token", func() {
|
||||
json := `
|
||||
{
|
||||
"url": "http://google.com",
|
||||
"recipient": "#ds-opentsdb",
|
||||
"mention": "@carl"
|
||||
"mention": "@carl",
|
||||
"token": "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
@@ -74,6 +76,7 @@ func TestSlackNotifier(t *testing.T) {
|
||||
So(slackNotifier.Url, ShouldEqual, "http://google.com")
|
||||
So(slackNotifier.Recipient, ShouldEqual, "#ds-opentsdb")
|
||||
So(slackNotifier.Mention, ShouldEqual, "@carl")
|
||||
So(slackNotifier.Token, ShouldEqual, "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX")
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -73,10 +73,8 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
OrgId: evalContext.Rule.OrgId,
|
||||
DashboardId: evalContext.Rule.DashboardId,
|
||||
PanelId: evalContext.Rule.PanelId,
|
||||
Type: annotations.AlertType,
|
||||
AlertId: evalContext.Rule.Id,
|
||||
Title: evalContext.Rule.Name,
|
||||
Text: evalContext.GetStateModel().Text,
|
||||
Text: "",
|
||||
NewState: string(evalContext.Rule.State),
|
||||
PrevState: string(evalContext.PrevAlertState),
|
||||
Epoch: time.Now().Unix(),
|
||||
|
||||
@@ -5,7 +5,7 @@ import "github.com/grafana/grafana/pkg/components/simplejson"
|
||||
type Repository interface {
|
||||
Save(item *Item) error
|
||||
Update(item *Item) error
|
||||
Find(query *ItemQuery) ([]*Item, error)
|
||||
Find(query *ItemQuery) ([]*ItemDTO, error)
|
||||
Delete(params *DeleteParams) error
|
||||
}
|
||||
|
||||
@@ -13,11 +13,10 @@ type ItemQuery struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
From int64 `json:"from"`
|
||||
To int64 `json:"to"`
|
||||
Type ItemType `json:"type"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
NewState []string `json:"newState"`
|
||||
Tags []string `json:"tags"`
|
||||
|
||||
Limit int64 `json:"limit"`
|
||||
}
|
||||
@@ -28,12 +27,15 @@ type PostParams struct {
|
||||
Epoch int64 `json:"epoch"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
type DeleteParams struct {
|
||||
Id int64 `json:"id"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
RegionId int64 `json:"regionId"`
|
||||
}
|
||||
|
||||
var repositoryInstance Repository
|
||||
@@ -46,29 +48,41 @@ func SetRepository(rep Repository) {
|
||||
repositoryInstance = rep
|
||||
}
|
||||
|
||||
type ItemType string
|
||||
|
||||
const (
|
||||
AlertType ItemType = "alert"
|
||||
EventType ItemType = "event"
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
UserId int64 `json:"userId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
CategoryId int64 `json:"categoryId"`
|
||||
RegionId int64 `json:"regionId"`
|
||||
Type ItemType `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
Metric string `json:"metric"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
UserId int64 `json:"userId"`
|
||||
PrevState string `json:"prevState"`
|
||||
NewState string `json:"newState"`
|
||||
Epoch int64 `json:"epoch"`
|
||||
Tags []string `json:"tags"`
|
||||
Data *simplejson.Json `json:"data"`
|
||||
|
||||
// needed until we remove it from db
|
||||
Type string
|
||||
Title string
|
||||
}
|
||||
|
||||
type ItemDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
AlertName string `json:"alertName"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
UserId int64 `json:"userId"`
|
||||
NewState string `json:"newState"`
|
||||
PrevState string `json:"prevState"`
|
||||
Time int64 `json:"time"`
|
||||
Text string `json:"text"`
|
||||
RegionId int64 `json:"regionId"`
|
||||
Tags []string `json:"tags"`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
Data *simplejson.Json `json:"data"`
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package sqlstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
)
|
||||
|
||||
@@ -13,19 +15,94 @@ type SqlAnnotationRepo struct {
|
||||
|
||||
func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
|
||||
tags := models.ParseTagPairs(item.Tags)
|
||||
item.Tags = models.JoinTagPairs(tags)
|
||||
if _, err := sess.Table("annotation").Insert(item); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if item.Tags != nil {
|
||||
if tags, err := r.ensureTagsExist(sess, tags); err != nil {
|
||||
return err
|
||||
} else {
|
||||
for _, tag := range tags {
|
||||
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", item.Id, tag.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Will insert if needed any new key/value pars and return ids
|
||||
func (r *SqlAnnotationRepo) ensureTagsExist(sess *DBSession, tags []*models.Tag) ([]*models.Tag, error) {
|
||||
for _, tag := range tags {
|
||||
var existingTag models.Tag
|
||||
|
||||
// check if it exists
|
||||
if exists, err := sess.Table("tag").Where("key=? AND value=?", tag.Key, tag.Value).Get(&existingTag); err != nil {
|
||||
return nil, err
|
||||
} else if exists {
|
||||
tag.Id = existingTag.Id
|
||||
} else {
|
||||
if _, err := sess.Table("tag").Insert(tag); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
var (
|
||||
isExist bool
|
||||
err error
|
||||
)
|
||||
existing := new(annotations.Item)
|
||||
|
||||
if _, err := sess.Table("annotation").Id(item.Id).Update(item); err != nil {
|
||||
if item.Id == 0 && item.RegionId != 0 {
|
||||
// Update region end time
|
||||
isExist, err = sess.Table("annotation").Where("region_id=? AND id!=? AND org_id=?", item.RegionId, item.RegionId, item.OrgId).Get(existing)
|
||||
} else {
|
||||
isExist, err = sess.Table("annotation").Where("id=? AND org_id=?", item.Id, item.OrgId).Get(existing)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isExist {
|
||||
return errors.New("Annotation not found")
|
||||
}
|
||||
|
||||
existing.Epoch = item.Epoch
|
||||
existing.Text = item.Text
|
||||
if item.RegionId != 0 {
|
||||
existing.RegionId = item.RegionId
|
||||
}
|
||||
|
||||
if item.Tags != nil {
|
||||
if tags, err := r.ensureTagsExist(sess, models.ParseTagPairs(item.Tags)); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if _, err := sess.Exec("DELETE FROM annotation_tag WHERE annotation_id = ?", existing.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", existing.Id, tag.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
existing.Tags = item.Tags
|
||||
|
||||
if _, err := sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -33,51 +110,79 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.Item, error) {
|
||||
func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.ItemDTO, error) {
|
||||
var sql bytes.Buffer
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
sql.WriteString(`SELECT *
|
||||
from annotation
|
||||
sql.WriteString(`
|
||||
SELECT
|
||||
annotation.id,
|
||||
annotation.epoch as time,
|
||||
annotation.dashboard_id,
|
||||
annotation.panel_id,
|
||||
annotation.new_state,
|
||||
annotation.prev_state,
|
||||
annotation.alert_id,
|
||||
annotation.region_id,
|
||||
annotation.text,
|
||||
annotation.tags,
|
||||
annotation.data,
|
||||
usr.email,
|
||||
usr.login,
|
||||
alert.name as alert_name
|
||||
FROM annotation
|
||||
LEFT OUTER JOIN ` + dialect.Quote("user") + ` as usr on usr.id = annotation.user_id
|
||||
LEFT OUTER JOIN alert on alert.id = annotation.alert_id
|
||||
`)
|
||||
|
||||
sql.WriteString(`WHERE org_id = ?`)
|
||||
sql.WriteString(`WHERE annotation.org_id = ?`)
|
||||
params = append(params, query.OrgId)
|
||||
|
||||
if query.AlertId != 0 {
|
||||
sql.WriteString(` AND alert_id = ?`)
|
||||
params = append(params, query.AlertId)
|
||||
}
|
||||
|
||||
if query.AlertId != 0 {
|
||||
sql.WriteString(` AND alert_id = ?`)
|
||||
sql.WriteString(` AND annotation.alert_id = ?`)
|
||||
params = append(params, query.AlertId)
|
||||
}
|
||||
|
||||
if query.DashboardId != 0 {
|
||||
sql.WriteString(` AND dashboard_id = ?`)
|
||||
sql.WriteString(` AND annotation.dashboard_id = ?`)
|
||||
params = append(params, query.DashboardId)
|
||||
}
|
||||
|
||||
if query.PanelId != 0 {
|
||||
sql.WriteString(` AND panel_id = ?`)
|
||||
sql.WriteString(` AND annotation.panel_id = ?`)
|
||||
params = append(params, query.PanelId)
|
||||
}
|
||||
|
||||
if query.From > 0 && query.To > 0 {
|
||||
sql.WriteString(` AND epoch BETWEEN ? AND ?`)
|
||||
sql.WriteString(` AND annotation.epoch BETWEEN ? AND ?`)
|
||||
params = append(params, query.From, query.To)
|
||||
}
|
||||
|
||||
if query.Type != "" {
|
||||
sql.WriteString(` AND type = ?`)
|
||||
params = append(params, string(query.Type))
|
||||
if len(query.Tags) > 0 {
|
||||
keyValueFilters := []string{}
|
||||
|
||||
tags := models.ParseTagPairs(query.Tags)
|
||||
for _, tag := range tags {
|
||||
if tag.Value == "" {
|
||||
keyValueFilters = append(keyValueFilters, "(tag.key = ?)")
|
||||
params = append(params, tag.Key)
|
||||
} else {
|
||||
keyValueFilters = append(keyValueFilters, "(tag.key = ? AND tag.value = ?)")
|
||||
params = append(params, tag.Key, tag.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if len(query.NewState) > 0 {
|
||||
sql.WriteString(` AND new_state IN (?` + strings.Repeat(",?", len(query.NewState)-1) + ")")
|
||||
for _, v := range query.NewState {
|
||||
params = append(params, v)
|
||||
if len(tags) > 0 {
|
||||
tagsSubQuery := fmt.Sprintf(`
|
||||
SELECT SUM(1) FROM annotation_tag at
|
||||
INNER JOIN tag on tag.id = at.tag_id
|
||||
WHERE at.annotation_id = annotation.id
|
||||
AND (
|
||||
%s
|
||||
)
|
||||
`, strings.Join(keyValueFilters, " OR "))
|
||||
|
||||
sql.WriteString(fmt.Sprintf(" AND (%s) = %d ", tagsSubQuery, len(tags)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +192,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
||||
|
||||
sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))
|
||||
|
||||
items := make([]*annotations.Item, 0)
|
||||
items := make([]*annotations.ItemDTO, 0)
|
||||
if err := x.Sql(sql.String(), params...).Find(&items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -97,11 +202,31 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
||||
|
||||
func (r *SqlAnnotationRepo) Delete(params *annotations.DeleteParams) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
var (
|
||||
sql string
|
||||
annoTagSql string
|
||||
queryParams []interface{}
|
||||
)
|
||||
|
||||
sql := "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ?"
|
||||
if params.RegionId != 0 {
|
||||
annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE region_id = ?)"
|
||||
sql = "DELETE FROM annotation WHERE region_id = ?"
|
||||
queryParams = []interface{}{params.RegionId}
|
||||
} else if params.Id != 0 {
|
||||
annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE id = ?)"
|
||||
sql = "DELETE FROM annotation WHERE id = ?"
|
||||
queryParams = []interface{}{params.Id}
|
||||
} else {
|
||||
annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE dashboard_id = ? AND panel_id = ?)"
|
||||
sql = "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ?"
|
||||
queryParams = []interface{}{params.DashboardId, params.PanelId}
|
||||
}
|
||||
|
||||
_, err := sess.Exec(sql, params.DashboardId, params.PanelId)
|
||||
if err != nil {
|
||||
if _, err := sess.Exec(annoTagSql, queryParams...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec(sql, queryParams...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
208
pkg/services/sqlstore/annotation_test.go
Normal file
208
pkg/services/sqlstore/annotation_test.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
)
|
||||
|
||||
func TestSavingTags(t *testing.T) {
|
||||
Convey("Testing annotation saving/loading", t, func() {
|
||||
InitTestDB(t)
|
||||
|
||||
repo := SqlAnnotationRepo{}
|
||||
|
||||
Convey("Can save tags", func() {
|
||||
tagPairs := []*models.Tag{
|
||||
{Key: "outage"},
|
||||
{Key: "type", Value: "outage"},
|
||||
{Key: "server", Value: "server-1"},
|
||||
{Key: "error"},
|
||||
}
|
||||
tags, err := repo.ensureTagsExist(newSession(), tagPairs)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(tags), ShouldEqual, 4)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnnotations(t *testing.T) {
|
||||
Convey("Testing annotation saving/loading", t, func() {
|
||||
InitTestDB(t)
|
||||
|
||||
repo := SqlAnnotationRepo{}
|
||||
|
||||
Convey("Can save annotation", func() {
|
||||
err := repo.Save(&annotations.Item{
|
||||
OrgId: 1,
|
||||
UserId: 1,
|
||||
DashboardId: 1,
|
||||
Text: "hello",
|
||||
Epoch: 10,
|
||||
Tags: []string{"outage", "error", "type:outage", "server:server-1"},
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Can query for annotation", func() {
|
||||
items, err := repo.Find(&annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 0,
|
||||
To: 15,
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(items, ShouldHaveLength, 1)
|
||||
|
||||
Convey("Can read tags", func() {
|
||||
So(items[0].Tags, ShouldResemble, []string{"outage", "error", "type:outage", "server:server-1"})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should not find any when item is outside time range", func() {
|
||||
items, err := repo.Find(&annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 12,
|
||||
To: 15,
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(items, ShouldHaveLength, 0)
|
||||
})
|
||||
|
||||
Convey("Should not find one when tag filter does not match", func() {
|
||||
items, err := repo.Find(&annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 1,
|
||||
To: 15,
|
||||
Tags: []string{"asd"},
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(items, ShouldHaveLength, 0)
|
||||
})
|
||||
|
||||
Convey("Should find one when all tag filters does match", func() {
|
||||
items, err := repo.Find(&annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 1,
|
||||
To: 15,
|
||||
Tags: []string{"outage", "error"},
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(items, ShouldHaveLength, 1)
|
||||
})
|
||||
|
||||
Convey("Should find one when all key value tag filters does match", func() {
|
||||
items, err := repo.Find(&annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 1,
|
||||
To: 15,
|
||||
Tags: []string{"type:outage", "server:server-1"},
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(items, ShouldHaveLength, 1)
|
||||
})
|
||||
|
||||
Convey("Can update annotation and remove all tags", func() {
|
||||
query := &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 0,
|
||||
To: 15,
|
||||
}
|
||||
items, err := repo.Find(query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
annotationId := items[0].Id
|
||||
|
||||
err = repo.Update(&annotations.Item{
|
||||
Id: annotationId,
|
||||
OrgId: 1,
|
||||
Text: "something new",
|
||||
Tags: []string{},
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
items, err = repo.Find(query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Can read tags", func() {
|
||||
So(items[0].Id, ShouldEqual, annotationId)
|
||||
So(len(items[0].Tags), ShouldEqual, 0)
|
||||
So(items[0].Text, ShouldEqual, "something new")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can update annotation with new tags", func() {
|
||||
query := &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 0,
|
||||
To: 15,
|
||||
}
|
||||
items, err := repo.Find(query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
annotationId := items[0].Id
|
||||
|
||||
err = repo.Update(&annotations.Item{
|
||||
Id: annotationId,
|
||||
OrgId: 1,
|
||||
Text: "something new",
|
||||
Tags: []string{"newtag1", "newtag2"},
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
items, err = repo.Find(query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Can read tags", func() {
|
||||
So(items[0].Id, ShouldEqual, annotationId)
|
||||
So(items[0].Tags, ShouldResemble, []string{"newtag1", "newtag2"})
|
||||
So(items[0].Text, ShouldEqual, "something new")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can delete annotation", func() {
|
||||
query := &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 0,
|
||||
To: 15,
|
||||
}
|
||||
items, err := repo.Find(query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
annotationId := items[0].Id
|
||||
|
||||
err = repo.Delete(&annotations.DeleteParams{Id: annotationId})
|
||||
|
||||
items, err = repo.Find(query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should be deleted", func() {
|
||||
So(len(items), ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -378,6 +378,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
|
||||
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
|
||||
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
|
||||
"DELETE FROM dashboard WHERE folder_id = ?",
|
||||
"DELETE FROM annotation WHERE dashboard_id = ?",
|
||||
}
|
||||
|
||||
for _, sql := range deletes {
|
||||
|
||||
@@ -57,4 +57,37 @@ func addAnnotationMig(mg *Migrator) {
|
||||
mg.AddMigration("Add column region_id to annotation table", NewAddColumnMigration(table, &Column{
|
||||
Name: "region_id", Type: DB_BigInt, Nullable: true, Default: "0",
|
||||
}))
|
||||
|
||||
categoryIdIndex := &Index{Cols: []string{"org_id", "category_id"}, Type: IndexType}
|
||||
mg.AddMigration("Drop category_id index", NewDropIndexMigration(table, categoryIdIndex))
|
||||
|
||||
mg.AddMigration("Add column tags to annotation table", NewAddColumnMigration(table, &Column{
|
||||
Name: "tags", Type: DB_NVarchar, Nullable: true, Length: 500,
|
||||
}))
|
||||
|
||||
///
|
||||
/// Annotation tag
|
||||
///
|
||||
annotationTagTable := Table{
|
||||
Name: "annotation_tag",
|
||||
Columns: []*Column{
|
||||
{Name: "annotation_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "tag_id", Type: DB_BigInt, Nullable: false},
|
||||
},
|
||||
Indices: []*Index{
|
||||
{Cols: []string{"annotation_id", "tag_id"}, Type: UniqueIndex},
|
||||
},
|
||||
}
|
||||
|
||||
mg.AddMigration("Create annotation_tag table v2", NewAddTableMigration(annotationTagTable))
|
||||
mg.AddMigration("Add unique index annotation_tag.annotation_id_tag_id", NewAddIndexMigration(annotationTagTable, annotationTagTable.Indices[0]))
|
||||
|
||||
//
|
||||
// clear alert text
|
||||
//
|
||||
updateTextFieldSql := "UPDATE annotation SET TEXT = '' WHERE alert_id > 0"
|
||||
mg.AddMigration("Update alert annotations and set TEXT to empty", new(RawSqlMigration).
|
||||
Sqlite(updateTextFieldSql).
|
||||
Postgres(updateTextFieldSql).
|
||||
Mysql(updateTextFieldSql))
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ func AddMigrations(mg *Migrator) {
|
||||
addDashboardVersionMigration(mg)
|
||||
addUserGroupMigrations(mg)
|
||||
addDashboardAclMigrations(mg)
|
||||
addTagMigration(mg)
|
||||
}
|
||||
|
||||
func addMigrationLogMigrations(mg *Migrator) {
|
||||
|
||||
24
pkg/services/sqlstore/migrations/tag_mig.go
Normal file
24
pkg/services/sqlstore/migrations/tag_mig.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package migrations
|
||||
|
||||
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
|
||||
func addTagMigration(mg *Migrator) {
|
||||
|
||||
tagTable := Table{
|
||||
Name: "tag",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "key", Type: DB_NVarchar, Length: 100, Nullable: false},
|
||||
{Name: "value", Type: DB_NVarchar, Length: 100, Nullable: false},
|
||||
},
|
||||
Indices: []*Index{
|
||||
{Cols: []string{"key", "value"}, Type: UniqueIndex},
|
||||
},
|
||||
}
|
||||
|
||||
// create table
|
||||
mg.AddMigration("create tag table", NewAddTableMigration(tagTable))
|
||||
|
||||
// create indices
|
||||
mg.AddMigration("add index tag.key_value", NewAddIndexMigration(tagTable, tagTable.Indices[0]))
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func (db *Postgres) SqlType(c *Column) string {
|
||||
|
||||
func (db *Postgres) TableCheckSql(tableName string) (string, []interface{}) {
|
||||
args := []interface{}{"grafana", tableName}
|
||||
sql := "SELECT `TABLE_NAME` from `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? and `TABLE_NAME`=?"
|
||||
sql := "SELECT table_name FROM information_schema.tables WHERE table_schema=? and table_name=?"
|
||||
return sql, args
|
||||
}
|
||||
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
var template = `
|
||||
<div class="graph-legend-popover">
|
||||
<div ng-show="ctrl.series" class="p-b-1">
|
||||
<label>Y Axis:</label>
|
||||
<button ng-click="ctrl.toggleAxis(yaxis);" class="btn btn-small"
|
||||
ng-class="{'btn-success': ctrl.series.yaxis === 1,
|
||||
'btn-inverse': ctrl.series.yaxis === 2}">
|
||||
Left
|
||||
</button>
|
||||
<button ng-click="ctrl.toggleAxis(yaxis);"
|
||||
class="btn btn-small"
|
||||
ng-class="{'btn-success': ctrl.series.yaxis === 2,
|
||||
'btn-inverse': ctrl.series.yaxis === 1}">
|
||||
Right
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="m-b-0">
|
||||
<i ng-repeat="color in ctrl.colors" class="pointer fa fa-circle"
|
||||
ng-style="{color:color}"
|
||||
ng-click="ctrl.colorSelected(color);"> </i>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export class ColorPickerCtrl {
|
||||
colors: any;
|
||||
autoClose: boolean;
|
||||
series: any;
|
||||
showAxisControls: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, $rootScope) {
|
||||
this.colors = $rootScope.colors;
|
||||
this.autoClose = $scope.autoClose;
|
||||
this.series = $scope.series;
|
||||
}
|
||||
|
||||
toggleAxis(yaxis) {
|
||||
this.$scope.toggleAxis();
|
||||
|
||||
if (this.$scope.autoClose) {
|
||||
this.$scope.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
colorSelected(color) {
|
||||
this.$scope.colorSelected(color);
|
||||
if (this.$scope.autoClose) {
|
||||
this.$scope.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function colorPicker() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
controller: ColorPickerCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
template: template,
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('gfColorPicker', colorPicker);
|
||||
45
public/app/core/components/colorpicker/ColorPalette.tsx
Normal file
45
public/app/core/components/colorpicker/ColorPalette.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { sortedColors } from 'app/core/utils/colors';
|
||||
|
||||
export interface IProps {
|
||||
color: string;
|
||||
onColorSelect: (c: string) => void;
|
||||
}
|
||||
|
||||
export class GfColorPalette extends React.Component<IProps, any> {
|
||||
paletteColors: string[];
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.paletteColors = sortedColors;
|
||||
this.onColorSelect = this.onColorSelect.bind(this);
|
||||
}
|
||||
|
||||
onColorSelect(color) {
|
||||
return () => {
|
||||
this.props.onColorSelect(color);
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const colorPaletteItems = this.paletteColors.map((paletteColor) => {
|
||||
const cssClass = paletteColor.toLowerCase() === this.props.color.toLowerCase() ? 'fa-circle-o' : 'fa-circle';
|
||||
return (
|
||||
<i key={paletteColor} className={"pointer fa " + cssClass}
|
||||
style={{'color': paletteColor}}
|
||||
onClick={this.onColorSelect(paletteColor)}>
|
||||
</i>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className="graph-legend-popover">
|
||||
<p className="m-b-0">{colorPaletteItems}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.directive('gfColorPalette', function (reactDirective) {
|
||||
return reactDirective(GfColorPalette, ['color', 'onColorSelect']);
|
||||
});
|
||||
84
public/app/core/components/colorpicker/ColorPicker.tsx
Normal file
84
public/app/core/components/colorpicker/ColorPicker.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import $ from 'jquery';
|
||||
import Drop from 'tether-drop';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
|
||||
export interface IProps {
|
||||
color: string;
|
||||
onChange: (c: string) => void;
|
||||
}
|
||||
|
||||
export class ColorPicker extends React.Component<IProps, any> {
|
||||
pickerElem: any;
|
||||
colorPickerDrop: any;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.openColorPicker = this.openColorPicker.bind(this);
|
||||
this.closeColorPicker = this.closeColorPicker.bind(this);
|
||||
this.setPickerElem = this.setPickerElem.bind(this);
|
||||
this.onColorSelect = this.onColorSelect.bind(this);
|
||||
}
|
||||
|
||||
setPickerElem(elem) {
|
||||
this.pickerElem = $(elem);
|
||||
}
|
||||
|
||||
openColorPicker() {
|
||||
const dropContent = (
|
||||
<ColorPickerPopover color={this.props.color} onColorSelect={this.onColorSelect} />
|
||||
);
|
||||
|
||||
let dropContentElem = document.createElement('div');
|
||||
ReactDOM.render(dropContent, dropContentElem);
|
||||
|
||||
let drop = new Drop({
|
||||
target: this.pickerElem[0],
|
||||
content: dropContentElem,
|
||||
position: 'top center',
|
||||
classes: 'drop-popover drop-popover--form',
|
||||
openOn: 'hover',
|
||||
hoverCloseDelay: 200,
|
||||
tetherOptions: {
|
||||
constraints: [{ to: 'scrollParent', attachment: "none both" }]
|
||||
}
|
||||
});
|
||||
|
||||
drop.on('close', this.closeColorPicker);
|
||||
|
||||
this.colorPickerDrop = drop;
|
||||
this.colorPickerDrop.open();
|
||||
}
|
||||
|
||||
closeColorPicker() {
|
||||
setTimeout(() => {
|
||||
if (this.colorPickerDrop && this.colorPickerDrop.tether) {
|
||||
this.colorPickerDrop.destroy();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
onColorSelect(color) {
|
||||
this.props.onChange(color);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="sp-replacer sp-light" onClick={this.openColorPicker} ref={this.setPickerElem}>
|
||||
<div className="sp-preview">
|
||||
<div className="sp-preview-inner" style={{backgroundColor: this.props.color}}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.directive('colorPicker', function (reactDirective) {
|
||||
return reactDirective(ColorPicker, [
|
||||
'color',
|
||||
['onChange', { watchDepth: 'reference', wrapApply: true }]
|
||||
]);
|
||||
});
|
||||
121
public/app/core/components/colorpicker/ColorPickerPopover.tsx
Normal file
121
public/app/core/components/colorpicker/ColorPickerPopover.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import $ from 'jquery';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { GfColorPalette } from './ColorPalette';
|
||||
import { GfSpectrumPicker } from './SpectrumPicker';
|
||||
|
||||
const DEFAULT_COLOR = '#000000';
|
||||
|
||||
export interface IProps {
|
||||
color: string;
|
||||
onColorSelect: (c: string) => void;
|
||||
}
|
||||
|
||||
export class ColorPickerPopover extends React.Component<IProps, any> {
|
||||
pickerNavElem: any;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tab: 'palette',
|
||||
color: this.props.color || DEFAULT_COLOR,
|
||||
colorString: this.props.color || DEFAULT_COLOR
|
||||
};
|
||||
}
|
||||
|
||||
setPickerNavElem(elem) {
|
||||
this.pickerNavElem = $(elem);
|
||||
}
|
||||
|
||||
setColor(color) {
|
||||
let newColor = tinycolor(color);
|
||||
if (newColor.isValid()) {
|
||||
this.setState({
|
||||
color: newColor.toString(),
|
||||
colorString: newColor.toString()
|
||||
});
|
||||
this.props.onColorSelect(color);
|
||||
}
|
||||
}
|
||||
|
||||
sampleColorSelected(color) {
|
||||
this.setColor(color);
|
||||
}
|
||||
|
||||
spectrumColorSelected(color) {
|
||||
let rgbColor = color.toRgbString();
|
||||
this.setColor(rgbColor);
|
||||
}
|
||||
|
||||
onColorStringChange(e) {
|
||||
let colorString = e.target.value;
|
||||
this.setState({
|
||||
colorString: colorString
|
||||
});
|
||||
|
||||
let newColor = tinycolor(colorString);
|
||||
if (newColor.isValid()) {
|
||||
// Update only color state
|
||||
this.setState({
|
||||
color: newColor.toString(),
|
||||
});
|
||||
this.props.onColorSelect(newColor);
|
||||
}
|
||||
}
|
||||
|
||||
onColorStringBlur(e) {
|
||||
let colorString = e.target.value;
|
||||
this.setColor(colorString);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.pickerNavElem.find('li:first').addClass('active');
|
||||
this.pickerNavElem.on('show', (e) => {
|
||||
// use href attr (#name => name)
|
||||
let tab = e.target.hash.slice(1);
|
||||
this.setState({
|
||||
tab: tab
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const paletteTab = (
|
||||
<div id="palette">
|
||||
<GfColorPalette color={this.state.color} onColorSelect={this.sampleColorSelected.bind(this)} />
|
||||
</div>
|
||||
);
|
||||
const spectrumTab = (
|
||||
<div id="spectrum">
|
||||
<GfSpectrumPicker color={this.state.color} onColorSelect={this.spectrumColorSelected.bind(this)} options={{}} />
|
||||
</div>
|
||||
);
|
||||
const currentTab = this.state.tab === 'palette' ? paletteTab : spectrumTab;
|
||||
|
||||
return (
|
||||
<div className="gf-color-picker">
|
||||
<ul className="nav nav-tabs" id="colorpickernav" ref={this.setPickerNavElem.bind(this)}>
|
||||
<li className="gf-tabs-item-colorpicker">
|
||||
<a href="#palette" data-toggle="tab">Colors</a>
|
||||
</li>
|
||||
<li className="gf-tabs-item-colorpicker">
|
||||
<a href="#spectrum" data-toggle="tab">Custom</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="gf-color-picker__body">
|
||||
{currentTab}
|
||||
</div>
|
||||
<div>
|
||||
<input className="gf-form-input gf-form-input--small" value={this.state.colorString}
|
||||
onChange={this.onColorStringChange.bind(this)} onBlur={this.onColorStringBlur.bind(this)}>
|
||||
</input>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.directive('gfColorPickerPopover', function (reactDirective) {
|
||||
return reactDirective(ColorPickerPopover, ['color', 'onColorSelect']);
|
||||
});
|
||||
55
public/app/core/components/colorpicker/SeriesColorPicker.tsx
Normal file
55
public/app/core/components/colorpicker/SeriesColorPicker.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import {ColorPickerPopover} from './ColorPickerPopover';
|
||||
|
||||
export interface IProps {
|
||||
series: any;
|
||||
onColorChange: (color: string) => void;
|
||||
onToggleAxis: () => void;
|
||||
}
|
||||
|
||||
export class SeriesColorPicker extends React.Component<IProps, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onColorChange = this.onColorChange.bind(this);
|
||||
this.onToggleAxis = this.onToggleAxis.bind(this);
|
||||
}
|
||||
|
||||
onColorChange(color) {
|
||||
this.props.onColorChange(color);
|
||||
}
|
||||
|
||||
onToggleAxis() {
|
||||
this.props.onToggleAxis();
|
||||
}
|
||||
|
||||
renderAxisSelection() {
|
||||
const leftButtonClass = this.props.series.yaxis === 1 ? 'btn-success' : 'btn-inverse';
|
||||
const rightButtonClass = this.props.series.yaxis === 2 ? 'btn-success' : 'btn-inverse';
|
||||
|
||||
return (
|
||||
<div className="p-b-1">
|
||||
<label className="small p-r-1">Y Axis:</label>
|
||||
<button onClick={this.onToggleAxis} className={'btn btn-small ' + leftButtonClass}>
|
||||
Left
|
||||
</button>
|
||||
<button onClick={this.onToggleAxis} className={'btn btn-small ' + rightButtonClass}>
|
||||
Right
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="graph-legend-popover">
|
||||
{this.props.series && this.renderAxisSelection()}
|
||||
<ColorPickerPopover color={this.props.series.color} onColorSelect={this.onColorChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.directive('seriesColorPicker', function(reactDirective) {
|
||||
return reactDirective(SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']);
|
||||
});
|
||||
76
public/app/core/components/colorpicker/SpectrumPicker.tsx
Normal file
76
public/app/core/components/colorpicker/SpectrumPicker.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import 'vendor/spectrum';
|
||||
|
||||
export interface IProps {
|
||||
color: string;
|
||||
options: object;
|
||||
onColorSelect: (c: string) => void;
|
||||
}
|
||||
|
||||
export class GfSpectrumPicker extends React.Component<IProps, any> {
|
||||
elem: any;
|
||||
isMoving: boolean;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onSpectrumMove = this.onSpectrumMove.bind(this);
|
||||
this.setComponentElem = this.setComponentElem.bind(this);
|
||||
}
|
||||
|
||||
setComponentElem(elem) {
|
||||
this.elem = $(elem);
|
||||
}
|
||||
|
||||
onSpectrumMove(color) {
|
||||
this.isMoving = true;
|
||||
this.props.onColorSelect(color);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let spectrumOptions = _.assignIn({
|
||||
flat: true,
|
||||
showAlpha: true,
|
||||
showButtons: false,
|
||||
color: this.props.color,
|
||||
appendTo: this.elem,
|
||||
move: this.onSpectrumMove,
|
||||
}, this.props.options);
|
||||
|
||||
this.elem.spectrum(spectrumOptions);
|
||||
this.elem.spectrum('show');
|
||||
this.elem.spectrum('set', this.props.color);
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
// If user move pointer over spectrum field this produce 'move' event and component
|
||||
// may update props.color. We don't want to update spectrum color in this case, so we can use
|
||||
// isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which
|
||||
// is called after updating occurs (when user finished moving).
|
||||
if (!this.isMoving) {
|
||||
this.elem.spectrum('set', nextProps.color);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.isMoving) {
|
||||
this.isMoving = false;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.elem.spectrum('destroy');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="spectrum-container" ref={this.setComponentElem}></div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.directive('gfSpectrumPicker', function (reactDirective) {
|
||||
return reactDirective(GfSpectrumPicker, ['color', 'options', 'onColorSelect']);
|
||||
});
|
||||
@@ -4,9 +4,6 @@ import coreModule from 'app/core/core_module';
|
||||
|
||||
var template = `
|
||||
<select class="gf-form-input" ng-model="ctrl.model" ng-options="f.value as f.text for f in ctrl.options"></select>
|
||||
<info-popover mode="right-absolute">
|
||||
Not finding dashboard you want? Star it first, then it should appear in this select box.
|
||||
</info-popover>
|
||||
`;
|
||||
|
||||
export class DashboardSelectorCtrl {
|
||||
|
||||
@@ -7,6 +7,7 @@ import $ from 'jquery';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import {profiler} from 'app/core/profiler';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import Drop from 'tether-drop';
|
||||
|
||||
export class GrafanaCtrl {
|
||||
|
||||
@@ -105,6 +106,11 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
if (data.params.kiosk) {
|
||||
appEvents.emit('toggle-kiosk-mode');
|
||||
}
|
||||
|
||||
// close all drops
|
||||
for (let drop of Drop.drops) {
|
||||
drop.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// handle kiosk mode
|
||||
|
||||
@@ -27,6 +27,8 @@ export function infoPopover() {
|
||||
|
||||
transclude(function(clone, newScope) {
|
||||
var content = document.createElement("div");
|
||||
content.className = 'markdown-html';
|
||||
|
||||
_.each(clone, (node) => {
|
||||
content.appendChild(node);
|
||||
});
|
||||
|
||||
@@ -1,77 +1,5 @@
|
||||
|
||||
/** Created by: Alex Wendland (me@alexwendland.com), 2014-08-06
|
||||
*
|
||||
* angular-json-tree
|
||||
*
|
||||
* Directive for creating a tree-view out of a JS Object. Only loads
|
||||
* sub-nodes on demand in order to improve performance of rendering large
|
||||
* objects.
|
||||
*
|
||||
* Attributes:
|
||||
* - object (Object, 2-way): JS object to build the tree from
|
||||
* - start-expanded (Boolean, 1-way, ?=true): should the tree default to expanded
|
||||
*
|
||||
* Usage:
|
||||
* // In the controller
|
||||
* scope.someObject = {
|
||||
* test: 'hello',
|
||||
* array: [1,1,2,3,5,8]
|
||||
* };
|
||||
* // In the html
|
||||
* <json-tree object="someObject"></json-tree>
|
||||
*
|
||||
* Dependencies:
|
||||
* - utils (json-tree.js)
|
||||
* - ajsRecursiveDirectiveHelper (json-tree.js)
|
||||
*
|
||||
* Test: json-tree-test.js
|
||||
*/
|
||||
|
||||
import angular from 'angular';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
var utils = {
|
||||
/* See link for possible type values to check against.
|
||||
* http://stackoverflow.com/questions/4622952/json-object-containing-array
|
||||
*
|
||||
* Value Class Type
|
||||
* -------------------------------------
|
||||
* "foo" String string
|
||||
* new String("foo") String object
|
||||
* 1.2 Number number
|
||||
* new Number(1.2) Number object
|
||||
* true Boolean boolean
|
||||
* new Boolean(true) Boolean object
|
||||
* new Date() Date object
|
||||
* new Error() Error object
|
||||
* [1,2,3] Array object
|
||||
* new Array(1, 2, 3) Array object
|
||||
* new Function("") Function function
|
||||
* /abc/g RegExp object (function in Nitro/V8)
|
||||
* new RegExp("meow") RegExp object (function in Nitro/V8)
|
||||
* {} Object object
|
||||
* new Object() Object object
|
||||
*/
|
||||
is: function is(obj, clazz) {
|
||||
return Object.prototype.toString.call(obj).slice(8, -1) === clazz;
|
||||
},
|
||||
|
||||
// See above for possible values
|
||||
whatClass: function whatClass(obj) {
|
||||
return Object.prototype.toString.call(obj).slice(8, -1);
|
||||
},
|
||||
|
||||
// Iterate over an objects keyset
|
||||
forKeys: function forKeys(obj, f) {
|
||||
for (var key in obj) {
|
||||
if (obj.hasOwnProperty(key) && typeof obj[key] !== 'function') {
|
||||
if (f(key, obj[key])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
import {JsonExplorer} from '../json_explorer/json_explorer';
|
||||
|
||||
coreModule.directive('jsonTree', [function jsonTreeDirective() {
|
||||
return{
|
||||
@@ -81,121 +9,14 @@ coreModule.directive('jsonTree', [function jsonTreeDirective() {
|
||||
startExpanded: '@',
|
||||
rootName: '@',
|
||||
},
|
||||
template: '<json-node key="rootName" value="object" start-expanded="startExpanded"></json-node>'
|
||||
};
|
||||
}]);
|
||||
link: function(scope, elem) {
|
||||
|
||||
coreModule.directive('jsonNode', ['ajsRecursiveDirectiveHelper', function jsonNodeDirective(ajsRecursiveDirectiveHelper) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
key: '=',
|
||||
value: '=',
|
||||
startExpanded: '@'
|
||||
},
|
||||
compile: function jsonNodeDirectiveCompile(elem) {
|
||||
return ajsRecursiveDirectiveHelper.compile(elem, this);
|
||||
},
|
||||
template: ' <span class="json-tree-key" ng-click="toggleExpanded()">{{key}}</span>' +
|
||||
' <span class="json-tree-leaf-value" ng-if="!isExpandable">{{value}}</span>' +
|
||||
' <span class="json-tree-branch-preview" ng-if="isExpandable" ng-show="!isExpanded" ng-click="toggleExpanded()">' +
|
||||
' {{preview}}</span>' +
|
||||
' <ul class="json-tree-branch-value" ng-if="isExpandable && shouldRender" ng-show="isExpanded">' +
|
||||
' <li ng-repeat="(subkey,subval) in value">' +
|
||||
' <json-node key="subkey" value="subval"></json-node>' +
|
||||
' </li>' +
|
||||
' </ul>',
|
||||
pre: function jsonNodeDirectiveLink(scope, elem, attrs) {
|
||||
// Set value's type as Class for CSS styling
|
||||
elem.addClass(utils.whatClass(scope.value).toLowerCase());
|
||||
// If the value is an Array or Object, use expandable view type
|
||||
if (utils.is(scope.value, 'Object') || utils.is(scope.value, 'Array')) {
|
||||
scope.isExpandable = true;
|
||||
// Add expandable class for CSS usage
|
||||
elem.addClass('expandable');
|
||||
// Setup preview text
|
||||
var isArray = utils.is(scope.value, 'Array');
|
||||
scope.preview = isArray ? '[ ' : '{ ';
|
||||
utils.forKeys(scope.value, function jsonNodeDirectiveLinkForKeys(key, value) {
|
||||
if (value === null) { scope.value[key] = 'null'; }
|
||||
if (isArray) {
|
||||
scope.preview += value + ', ';
|
||||
} else {
|
||||
scope.preview += key + ': ' + value + ', ';
|
||||
}
|
||||
});
|
||||
scope.preview = scope.preview.substring(0, scope.preview.length - (scope.preview.length > 2 ? 2 : 0)) + (isArray ? ' ]' : ' }');
|
||||
// If directive initially has isExpanded set, also set shouldRender to true
|
||||
if (scope.startExpanded) {
|
||||
scope.shouldRender = true;
|
||||
elem.addClass('expanded');
|
||||
}
|
||||
// Setup isExpanded state handling
|
||||
scope.isExpanded = scope.startExpanded;
|
||||
scope.toggleExpanded = function jsonNodeDirectiveToggleExpanded() {
|
||||
scope.isExpanded = !scope.isExpanded;
|
||||
if (scope.isExpanded) {
|
||||
elem.addClass('expanded');
|
||||
} else {
|
||||
elem.removeClass('expanded');
|
||||
}
|
||||
// For delaying subnode render until requested
|
||||
scope.shouldRender = true;
|
||||
};
|
||||
} else {
|
||||
scope.isExpandable = false;
|
||||
// Add expandable class for CSS usage
|
||||
elem.addClass('not-expandable');
|
||||
}
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
/** Added by: Alex Wendland (me@alexwendland.com), 2014-08-09
|
||||
* Source: http://stackoverflow.com/questions/14430655/recursion-in-angular-directives
|
||||
*
|
||||
* Used to allow for recursion within directives
|
||||
*/
|
||||
coreModule.factory('ajsRecursiveDirectiveHelper', ['$compile', function RecursiveDirectiveHelper($compile) {
|
||||
return {
|
||||
/**
|
||||
* Manually compiles the element, fixing the recursion loop.
|
||||
* @param element
|
||||
* @param [link] A post-link function, or an object with function(s) registered via pre and post properties.
|
||||
* @returns An object containing the linking functions.
|
||||
*/
|
||||
compile: function RecursiveDirectiveHelperCompile(element, link) {
|
||||
// Normalize the link parameter
|
||||
if (angular.isFunction(link)) {
|
||||
link = {
|
||||
post: link
|
||||
};
|
||||
}
|
||||
|
||||
// Break the recursion loop by removing the contents
|
||||
var contents = element.contents().remove();
|
||||
var compiledContents;
|
||||
return {
|
||||
pre: (link && link.pre) ? link.pre : null,
|
||||
/**
|
||||
* Compiles and re-adds the contents
|
||||
*/
|
||||
post: function RecursiveDirectiveHelperCompilePost(scope, element) {
|
||||
// Compile the contents
|
||||
if (!compiledContents) {
|
||||
compiledContents = $compile(contents);
|
||||
}
|
||||
// Re-add the compiled contents to the element
|
||||
compiledContents(scope, function (clone) {
|
||||
element.append(clone);
|
||||
var jsonExp = new JsonExplorer(scope.object, 3, {
|
||||
animateOpen: true
|
||||
});
|
||||
|
||||
// Call the post-linking function, if any
|
||||
if (link && link.post) {
|
||||
link.post.apply(null, arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
const html = jsonExp.render(true);
|
||||
elem.html(html);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
@@ -5,7 +5,6 @@ import "./directives/dropdown_typeahead";
|
||||
import "./directives/metric_segment";
|
||||
import "./directives/misc";
|
||||
import "./directives/ng_model_on_blur";
|
||||
import "./directives/spectrum_picker";
|
||||
import "./directives/tags";
|
||||
import "./directives/value_select_dropdown";
|
||||
import "./directives/rebuild_on_change";
|
||||
@@ -16,12 +15,13 @@ import './partials';
|
||||
import './components/jsontree/jsontree';
|
||||
import './components/code_editor/code_editor';
|
||||
import './utils/outline';
|
||||
import './components/colorpicker/ColorPicker';
|
||||
import './components/colorpicker/SeriesColorPicker';
|
||||
|
||||
import {grafanaAppDirective} from './components/grafana_app';
|
||||
import {sideMenuDirective} from './components/sidemenu/sidemenu';
|
||||
import {searchDirective} from './components/search/search';
|
||||
import {infoPopover} from './components/info_popover';
|
||||
import {colorPicker} from './components/colorpicker';
|
||||
import {navbarDirective} from './components/navbar/navbar';
|
||||
import {arrayJoin} from './directives/array_join';
|
||||
import {liveSrv} from './live/live_srv';
|
||||
@@ -58,7 +58,6 @@ export {
|
||||
sideMenuDirective,
|
||||
navbarDirective,
|
||||
searchDirective,
|
||||
colorPicker,
|
||||
liveSrv,
|
||||
layoutSelector,
|
||||
switchDirective,
|
||||
|
||||
@@ -79,7 +79,9 @@ function (_, $, coreModule) {
|
||||
$scope.$apply(function() {
|
||||
$scope.getOptions({ $query: query }).then(function(altSegments) {
|
||||
$scope.altSegments = altSegments;
|
||||
options = _.map($scope.altSegments, function(alt) { return alt.value; });
|
||||
options = _.map($scope.altSegments, function(alt) {
|
||||
return _.escape(alt.value);
|
||||
});
|
||||
|
||||
// add custom values
|
||||
if (segment.custom !== 'false') {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'../core_module',
|
||||
'vendor/spectrum',
|
||||
],
|
||||
function (angular, coreModule) {
|
||||
'use strict';
|
||||
|
||||
coreModule.default.directive('spectrumPicker', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
require: 'ngModel',
|
||||
scope: false,
|
||||
replace: true,
|
||||
template: "<span><input class='input-small' /></span>",
|
||||
link: function(scope, element, attrs, ngModel) {
|
||||
var input = element.find('input');
|
||||
var options = angular.extend({
|
||||
showAlpha: true,
|
||||
showButtons: false,
|
||||
color: ngModel.$viewValue,
|
||||
change: function(color) {
|
||||
scope.$apply(function() {
|
||||
ngModel.$setViewValue(color.toRgbString());
|
||||
});
|
||||
}
|
||||
}, scope.$eval(attrs.options));
|
||||
|
||||
ngModel.$render = function() {
|
||||
input.spectrum('set', ngModel.$viewValue || '');
|
||||
};
|
||||
|
||||
input.spectrum(options);
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
input.spectrum('destroy');
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -88,6 +88,7 @@ function (angular, $, coreModule) {
|
||||
typeahead: {
|
||||
source: angular.isFunction(scope.$parent[attrs.typeaheadSource]) ? scope.$parent[attrs.typeaheadSource] : null
|
||||
},
|
||||
widthClass: attrs.widthClass,
|
||||
itemValue: getItemProperty(scope, attrs.itemvalue),
|
||||
itemText : getItemProperty(scope, attrs.itemtext),
|
||||
tagClass : angular.isFunction(scope.$parent[attrs.tagclass]) ?
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ export class NavModelSrv {
|
||||
|
||||
menu.push({
|
||||
title: 'Annotations',
|
||||
icon: 'fa fa-fw fa-bolt',
|
||||
icon: 'fa fa-fw fa-comment',
|
||||
clickHandler: () => dashNavCtrl.openEditView('annotations')
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import _ from 'lodash';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
export const PALETTE_ROWS = 4;
|
||||
export const PALETTE_COLUMNS = 14;
|
||||
export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)';
|
||||
export const OK_COLOR = "rgba(11, 237, 50, 1)";
|
||||
export const ALERTING_COLOR = "rgba(237, 46, 24, 1)";
|
||||
export const NO_DATA_COLOR = "rgba(150, 150, 150, 1)";
|
||||
export const REGION_FILL_ALPHA = 0.09;
|
||||
|
||||
export default [
|
||||
let colors = [
|
||||
"#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0",
|
||||
"#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477",
|
||||
"#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0",
|
||||
@@ -10,3 +19,26 @@ export default [
|
||||
"#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"
|
||||
];
|
||||
|
||||
export function sortColorsByHue(hexColors) {
|
||||
let hslColors = _.map(hexColors, hexToHsl);
|
||||
|
||||
let sortedHSLColors = _.sortBy(hslColors, ['h']);
|
||||
sortedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS);
|
||||
sortedHSLColors = _.map(sortedHSLColors, chunk => {
|
||||
return _.sortBy(chunk, 'l');
|
||||
});
|
||||
sortedHSLColors = _.flattenDeep(_.zip(...sortedHSLColors));
|
||||
|
||||
return _.map(sortedHSLColors, hslToHex);
|
||||
}
|
||||
|
||||
export function hexToHsl(color) {
|
||||
return tinycolor(color).toHsl();
|
||||
}
|
||||
|
||||
export function hslToHex(color) {
|
||||
return tinycolor(color).toHexString();
|
||||
}
|
||||
|
||||
export let sortedColors = sortColorsByHue(colors);
|
||||
export default colors;
|
||||
|
||||
@@ -5,7 +5,7 @@ import moment from 'moment';
|
||||
|
||||
var units = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
|
||||
|
||||
export function parse(text, roundUp?) {
|
||||
export function parse(text, roundUp?, timezone?) {
|
||||
if (!text) { return undefined; }
|
||||
if (moment.isMoment(text)) { return text; }
|
||||
if (_.isDate(text)) { return moment(text); }
|
||||
@@ -16,7 +16,11 @@ export function parse(text, roundUp?) {
|
||||
var parseString;
|
||||
|
||||
if (text.substring(0, 3) === 'now') {
|
||||
if (timezone === 'utc') {
|
||||
time = moment.utc();
|
||||
} else {
|
||||
time = moment();
|
||||
}
|
||||
mathString = text.substring('now'.length);
|
||||
} else {
|
||||
index = text.indexOf('||');
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
declare var window: any;
|
||||
|
||||
const DEFAULT_DATETIME_FORMAT: String = 'YYYY-MM-DDTHH:mm:ssZ';
|
||||
const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ';
|
||||
|
||||
export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
|
||||
var text = excel ? 'sep=;\n' : '' + 'Series;Time;Value\n';
|
||||
var text = (excel ? 'sep=;\n' : '') + 'Series;Time;Value\n';
|
||||
_.each(seriesList, function(series) {
|
||||
_.each(series.datapoints, function(dp) {
|
||||
text += series.alias + ';' + moment(dp[1]).format(dateTimeFormat) + ';' + dp[0] + '\n';
|
||||
@@ -18,7 +16,7 @@ export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATET
|
||||
}
|
||||
|
||||
export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
|
||||
var text = excel ? 'sep=;\n' : '' + 'Time;';
|
||||
var text = (excel ? 'sep=;\n' : '') + 'Time;';
|
||||
// add header
|
||||
_.each(seriesList, function(series) {
|
||||
text += series.alias + ';';
|
||||
|
||||
@@ -402,6 +402,10 @@ function($, _, moment) {
|
||||
kbn.valueFormats.currencyRUB = kbn.formatBuilders.currency('₽');
|
||||
kbn.valueFormats.currencyUAH = kbn.formatBuilders.currency('₴');
|
||||
kbn.valueFormats.currencyBRL = kbn.formatBuilders.currency('R$');
|
||||
kbn.valueFormats.currencyDKK = kbn.formatBuilders.currency('kr');
|
||||
kbn.valueFormats.currencyISK = kbn.formatBuilders.currency('kr');
|
||||
kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr');
|
||||
kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr');
|
||||
|
||||
// Data (Binary)
|
||||
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
|
||||
@@ -756,6 +760,10 @@ function($, _, moment) {
|
||||
{text: 'Rubles (₽)', value: 'currencyRUB'},
|
||||
{text: 'Hryvnias (₴)', value: 'currencyUAH'},
|
||||
{text: 'Real (R$)', value: 'currencyBRL'},
|
||||
{text: 'Danish Krone (kr)', value: 'currencyDKK'},
|
||||
{text: 'Icelandic Krone (kr)', value: 'currencyISK'},
|
||||
{text: 'Norwegian Krone (kr)', value: 'currencyNOK'},
|
||||
{text: 'Swedish Krone (kr)', value: 'currencySEK'},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<form name="passwordForm" class="gf-form-group">
|
||||
<div class="gf-form" >
|
||||
<editor-checkbox text="Grafana Admin" model="permissions.isGrafanaAdmin" style="line-height: 1.5rem;"></editor-checkbox>
|
||||
<gf-form-switch class="gf-form" label="Grafana Admin" checked="permissions.isGrafanaAdmin" switch-class="max-width-6"></gf-form-switch>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
|
||||
@@ -128,7 +128,6 @@ function joinEvalMatches(matches, separator: string) {
|
||||
}
|
||||
|
||||
function getAlertAnnotationInfo(ah) {
|
||||
|
||||
// backward compatability, can be removed in grafana 5.x
|
||||
// old way stored evalMatches in data property directly,
|
||||
// new way stores it in evalMatches property on new data object
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import alertDef from '../alerting/alert_def';
|
||||
|
||||
/** @ngInject **/
|
||||
export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) {
|
||||
export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, popoverSrv, $compile) {
|
||||
|
||||
function sanitizeString(str) {
|
||||
try {
|
||||
@@ -21,6 +19,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
"event": "=",
|
||||
"onEdit": "&"
|
||||
},
|
||||
link: function(scope, element) {
|
||||
var event = scope.event;
|
||||
@@ -31,33 +30,46 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) {
|
||||
var tooltip = '<div class="graph-annotation">';
|
||||
var titleStateClass = '';
|
||||
|
||||
if (event.source.name === 'panel-alert') {
|
||||
if (event.alertId) {
|
||||
var stateModel = alertDef.getStateDisplayModel(event.newState);
|
||||
titleStateClass = stateModel.stateClass;
|
||||
title = `<i class="icon-gf ${stateModel.iconClass}"></i> ${stateModel.text}`;
|
||||
text = alertDef.getAlertAnnotationInfo(event);
|
||||
if (event.text) {
|
||||
text = text + '<br />' + event.text;
|
||||
}
|
||||
} else if (title) {
|
||||
text = title + '<br />' + text;
|
||||
title = '';
|
||||
}
|
||||
|
||||
tooltip += `
|
||||
<div class="graph-annotation-header">
|
||||
<span class="graph-annotation-title ${titleStateClass}">${sanitizeString(title)}</span>
|
||||
<span class="graph-annotation-time">${dashboard.formatDate(event.min)}</span>
|
||||
</div>
|
||||
var header = `<div class="graph-annotation__header">`;
|
||||
if (event.login) {
|
||||
header += `<div class="graph-annotation__user" bs-tooltip="'Created by ${event.login}'"><img src="${event.avatarUrl}" /></div>`;
|
||||
}
|
||||
header += `
|
||||
<span class="graph-annotation__title ${titleStateClass}">${sanitizeString(title)}</span>
|
||||
<span class="graph-annotation__time">${dashboard.formatDate(event.min)}</span>
|
||||
`;
|
||||
|
||||
tooltip += '<div class="graph-annotation-body">';
|
||||
// Show edit icon only for users with at least Editor role
|
||||
if (event.id && contextSrv.isEditor) {
|
||||
header += `
|
||||
<span class="pointer graph-annotation__edit-icon" ng-click="onEdit()">
|
||||
<i class="fa fa-pencil-square"></i>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
header += `</div>`;
|
||||
tooltip += header;
|
||||
tooltip += '<div class="graph-annotation__body">';
|
||||
|
||||
if (text) {
|
||||
tooltip += sanitizeString(text).replace(/\n/g, '<br>') + '<br>';
|
||||
tooltip += '<div>' + sanitizeString(text).replace(/\n/g, '<br>') + '</div>';
|
||||
}
|
||||
|
||||
var tags = event.tags;
|
||||
if (_.isString(event.tags)) {
|
||||
tags = event.tags.split(',');
|
||||
if (tags.length === 1) {
|
||||
tags = event.tags.split(' ');
|
||||
}
|
||||
}
|
||||
|
||||
if (tags && tags.length) {
|
||||
scope.tags = tags;
|
||||
@@ -65,6 +77,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) {
|
||||
}
|
||||
|
||||
tooltip += "</div>";
|
||||
tooltip += '</div>';
|
||||
|
||||
var $tooltip = $(tooltip);
|
||||
$tooltip.appendTo(element);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import './editor_ctrl';
|
||||
|
||||
import angular from 'angular';
|
||||
@@ -11,11 +9,7 @@ export class AnnotationsSrv {
|
||||
alertStatesPromise: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $rootScope,
|
||||
private $q,
|
||||
private datasourceSrv,
|
||||
private backendSrv,
|
||||
private timeSrv) {
|
||||
constructor(private $rootScope, private $q, private datasourceSrv, private backendSrv, private timeSrv) {
|
||||
$rootScope.onAppEvent('refresh', this.clearCache.bind(this), $rootScope);
|
||||
$rootScope.onAppEvent('dashboard-initialized', this.clearCache.bind(this), $rootScope);
|
||||
}
|
||||
@@ -26,66 +20,42 @@ export class AnnotationsSrv {
|
||||
}
|
||||
|
||||
getAnnotations(options) {
|
||||
return this.$q.all([
|
||||
this.getGlobalAnnotations(options),
|
||||
this.getPanelAnnotations(options),
|
||||
this.getAlertStates(options)
|
||||
]).then(results => {
|
||||
|
||||
return this.$q
|
||||
.all([this.getGlobalAnnotations(options), this.getAlertStates(options)])
|
||||
.then(results => {
|
||||
// combine the annotations and flatten results
|
||||
var annotations = _.flattenDeep([results[0], results[1]]);
|
||||
var annotations = _.flattenDeep(results[0]);
|
||||
|
||||
// filter out annotations that do not belong to requesting panel
|
||||
annotations = _.filter(annotations, item => {
|
||||
// shownIn === 1 requires annotation matching panel id
|
||||
if (item.source.showIn === 1) {
|
||||
if (item.panelId && options.panel.id === item.panelId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
// if event has panel id and query is of type dashboard then panel and requesting panel id must match
|
||||
if (item.panelId && item.source.type === 'dashboard') {
|
||||
return item.panelId === options.panel.id;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
annotations = dedupAnnotations(annotations);
|
||||
annotations = makeRegions(annotations, options);
|
||||
|
||||
// look for alert state for this panel
|
||||
var alertState = _.find(results[2], {panelId: options.panel.id});
|
||||
var alertState = _.find(results[1], {panelId: options.panel.id});
|
||||
|
||||
return {
|
||||
annotations: annotations,
|
||||
alertState: alertState,
|
||||
};
|
||||
|
||||
}).catch(err => {
|
||||
})
|
||||
.catch(err => {
|
||||
if (!err.message && err.data && err.data.message) {
|
||||
err.message = err.data.message;
|
||||
}
|
||||
this.$rootScope.appEvent('alert-error', ['Annotation Query Failed', (err.message || err)]);
|
||||
|
||||
console.log('AnnotationSrv.query error', err);
|
||||
this.$rootScope.appEvent('alert-error', ['Annotation Query Failed', err.message || err]);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
getPanelAnnotations(options) {
|
||||
var panel = options.panel;
|
||||
var dashboard = options.dashboard;
|
||||
|
||||
if (dashboard.id && panel && panel.alert) {
|
||||
return this.backendSrv.get('/api/annotations', {
|
||||
from: options.range.from.valueOf(),
|
||||
to: options.range.to.valueOf(),
|
||||
limit: 100,
|
||||
panelId: panel.id,
|
||||
dashboardId: dashboard.id,
|
||||
}).then(results => {
|
||||
// this built in annotation source name `panel-alert` is used in annotation tooltip
|
||||
// to know that this annotation is from panel alert
|
||||
return this.translateQueryResult({iconColor: '#AA0000', name: 'panel-alert'}, results);
|
||||
});
|
||||
}
|
||||
|
||||
return this.$q.when([]);
|
||||
}
|
||||
|
||||
getAlertStates(options) {
|
||||
if (!options.dashboard.id) {
|
||||
return this.$q.when([]);
|
||||
@@ -104,32 +74,42 @@ export class AnnotationsSrv {
|
||||
return this.alertStatesPromise;
|
||||
}
|
||||
|
||||
this.alertStatesPromise = this.backendSrv.get('/api/alerts/states-for-dashboard', {dashboardId: options.dashboard.id});
|
||||
this.alertStatesPromise = this.backendSrv.get('/api/alerts/states-for-dashboard', {
|
||||
dashboardId: options.dashboard.id,
|
||||
});
|
||||
return this.alertStatesPromise;
|
||||
}
|
||||
|
||||
getGlobalAnnotations(options) {
|
||||
var dashboard = options.dashboard;
|
||||
|
||||
if (dashboard.annotations.list.length === 0) {
|
||||
return this.$q.when([]);
|
||||
}
|
||||
|
||||
if (this.globalAnnotationsPromise) {
|
||||
return this.globalAnnotationsPromise;
|
||||
}
|
||||
|
||||
var annotations = _.filter(dashboard.annotations.list, {enable: true});
|
||||
var range = this.timeSrv.timeRange();
|
||||
var promises = [];
|
||||
|
||||
for (let annotation of dashboard.annotations.list) {
|
||||
if (!annotation.enable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.globalAnnotationsPromise = this.$q.all(_.map(annotations, annotation => {
|
||||
if (annotation.snapshotData) {
|
||||
return this.translateQueryResult(annotation, annotation.snapshotData);
|
||||
}
|
||||
|
||||
return this.datasourceSrv.get(annotation.datasource).then(datasource => {
|
||||
promises.push(
|
||||
this.datasourceSrv
|
||||
.get(annotation.datasource)
|
||||
.then(datasource => {
|
||||
// issue query against data source
|
||||
return datasource.annotationQuery({range: range, rangeRaw: range.raw, annotation: annotation});
|
||||
return datasource.annotationQuery({
|
||||
range: range,
|
||||
rangeRaw: range.raw,
|
||||
annotation: annotation,
|
||||
dashboard: dashboard,
|
||||
});
|
||||
})
|
||||
.then(results => {
|
||||
// store response in annotation object if this is a snapshot call
|
||||
@@ -138,9 +118,11 @@ export class AnnotationsSrv {
|
||||
}
|
||||
// translate result
|
||||
return this.translateQueryResult(annotation, results);
|
||||
});
|
||||
}));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
this.globalAnnotationsPromise = this.$q.all(promises);
|
||||
return this.globalAnnotationsPromise;
|
||||
}
|
||||
|
||||
@@ -149,6 +131,21 @@ export class AnnotationsSrv {
|
||||
return this.backendSrv.post('/api/annotations', annotation);
|
||||
}
|
||||
|
||||
updateAnnotationEvent(annotation) {
|
||||
this.globalAnnotationsPromise = null;
|
||||
return this.backendSrv.put(`/api/annotations/${annotation.id}`, annotation);
|
||||
}
|
||||
|
||||
deleteAnnotationEvent(annotation) {
|
||||
this.globalAnnotationsPromise = null;
|
||||
let deleteUrl = `/api/annotations/${annotation.id}`;
|
||||
if (annotation.isRegion) {
|
||||
deleteUrl = `/api/annotations/region/${annotation.regionId}`;
|
||||
}
|
||||
|
||||
return this.backendSrv.delete(deleteUrl);
|
||||
}
|
||||
|
||||
translateQueryResult(annotation, results) {
|
||||
// if annotation has snapshotData
|
||||
// make clone and remove it
|
||||
@@ -159,13 +156,88 @@ export class AnnotationsSrv {
|
||||
|
||||
for (var item of results) {
|
||||
item.source = annotation;
|
||||
item.min = item.time;
|
||||
item.max = item.time;
|
||||
item.scope = 1;
|
||||
item.eventType = annotation.name;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function converts annotation events into set
|
||||
* of single events and regions (event consist of two)
|
||||
* @param annotations
|
||||
* @param options
|
||||
*/
|
||||
function makeRegions(annotations, options) {
|
||||
let [regionEvents, singleEvents] = _.partition(annotations, 'regionId');
|
||||
let regions = getRegions(regionEvents, options.range);
|
||||
annotations = _.concat(regions, singleEvents);
|
||||
return annotations;
|
||||
}
|
||||
|
||||
function getRegions(events, range) {
|
||||
let region_events = _.filter(events, event => {
|
||||
return event.regionId;
|
||||
});
|
||||
let regions = _.groupBy(region_events, 'regionId');
|
||||
regions = _.compact(
|
||||
_.map(regions, region_events => {
|
||||
let region_obj = _.head(region_events);
|
||||
if (region_events && region_events.length > 1) {
|
||||
region_obj.timeEnd = region_events[1].time;
|
||||
region_obj.isRegion = true;
|
||||
return region_obj;
|
||||
} else {
|
||||
if (region_events && region_events.length) {
|
||||
// Don't change proper region object
|
||||
if (!region_obj.time || !region_obj.timeEnd) {
|
||||
// This is cut region
|
||||
if (isStartOfRegion(region_obj)) {
|
||||
region_obj.timeEnd = range.to.valueOf() - 1;
|
||||
} else {
|
||||
// Start time = null
|
||||
region_obj.timeEnd = region_obj.time;
|
||||
region_obj.time = range.from.valueOf() + 1;
|
||||
}
|
||||
region_obj.isRegion = true;
|
||||
}
|
||||
|
||||
return region_obj;
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
function isStartOfRegion(event): boolean {
|
||||
return event.id && event.id === event.regionId;
|
||||
}
|
||||
|
||||
function dedupAnnotations(annotations) {
|
||||
let dedup = [];
|
||||
|
||||
// Split events by annotationId property existance
|
||||
let events = _.partition(annotations, 'id');
|
||||
|
||||
let eventsById = _.groupBy(events[0], 'id');
|
||||
dedup = _.map(eventsById, eventGroup => {
|
||||
if (eventGroup.length > 1 && !_.every(eventGroup, isPanelAlert)) {
|
||||
// Get first non-panel alert
|
||||
return _.find(eventGroup, event => {
|
||||
return event.eventType !== 'panel-alert';
|
||||
});
|
||||
} else {
|
||||
return _.head(eventGroup);
|
||||
}
|
||||
});
|
||||
|
||||
dedup = _.concat(dedup, events[1]);
|
||||
return dedup;
|
||||
}
|
||||
|
||||
function isPanelAlert(event) {
|
||||
return event.eventType === 'panel-alert';
|
||||
}
|
||||
|
||||
coreModule.service('annotationsSrv', AnnotationsSrv);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
@@ -36,11 +34,7 @@ export class AnnotationsEditorCtrl {
|
||||
this.annotations = $scope.dashboard.annotations.list;
|
||||
this.reset();
|
||||
|
||||
$scope.$watch('mode', newVal => {
|
||||
if (newVal === 'new') {
|
||||
this.reset();
|
||||
}
|
||||
});
|
||||
this.onColorChange = this.onColorChange.bind(this);
|
||||
}
|
||||
|
||||
datasourceChanged() {
|
||||
@@ -71,6 +65,11 @@ export class AnnotationsEditorCtrl {
|
||||
this.$scope.broadcastRefresh();
|
||||
}
|
||||
|
||||
setupNew() {
|
||||
this.mode = 'new';
|
||||
this.reset();
|
||||
}
|
||||
|
||||
add() {
|
||||
this.annotations.push(this.currentAnnotation);
|
||||
this.reset();
|
||||
@@ -85,6 +84,18 @@ export class AnnotationsEditorCtrl {
|
||||
this.$scope.dashboard.updateSubmenuVisibility();
|
||||
this.$scope.broadcastRefresh();
|
||||
}
|
||||
|
||||
onColorChange(newColor) {
|
||||
this.currentAnnotation.iconColor = newColor;
|
||||
}
|
||||
|
||||
annotationEnabledChange() {
|
||||
this.$scope.broadcastRefresh();
|
||||
}
|
||||
|
||||
annotationHiddenChanged() {
|
||||
this.$scope.dashboard.updateSubmenuVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('AnnotationsEditorCtrl', AnnotationsEditorCtrl);
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
export class AnnotationEvent {
|
||||
dashboardId: number;
|
||||
panelId: number;
|
||||
userId: number;
|
||||
time: any;
|
||||
timeEnd: any;
|
||||
isRegion: boolean;
|
||||
title: string;
|
||||
text: string;
|
||||
type: string;
|
||||
tags: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import {coreModule} from 'app/core/core';
|
||||
import {MetricsPanelCtrl} from 'app/plugins/sdk';
|
||||
import {AnnotationEvent} from './event';
|
||||
@@ -11,11 +10,20 @@ export class EventEditorCtrl {
|
||||
timeRange: {from: number, to: number};
|
||||
form: any;
|
||||
close: any;
|
||||
timeFormated: string;
|
||||
|
||||
/** @ngInject **/
|
||||
constructor(private annotationsSrv) {
|
||||
this.event.panelId = this.panelCtrl.panel.id;
|
||||
this.event.dashboardId = this.panelCtrl.dashboard.id;
|
||||
|
||||
// Annotations query returns time as Unix timestamp in milliseconds
|
||||
this.event.time = tryEpochToMoment(this.event.time);
|
||||
if (this.event.isRegion) {
|
||||
this.event.timeEnd = tryEpochToMoment(this.event.timeEnd);
|
||||
}
|
||||
|
||||
this.timeFormated = this.panelCtrl.dashboard.formatDate(this.event.time);
|
||||
}
|
||||
|
||||
save() {
|
||||
@@ -28,7 +36,7 @@ export class EventEditorCtrl {
|
||||
saveModel.timeEnd = 0;
|
||||
|
||||
if (saveModel.isRegion) {
|
||||
saveModel.timeEnd = saveModel.timeEnd.valueOf();
|
||||
saveModel.timeEnd = this.event.timeEnd.valueOf();
|
||||
|
||||
if (saveModel.timeEnd < saveModel.time) {
|
||||
console.log('invalid time');
|
||||
@@ -36,14 +44,48 @@ export class EventEditorCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
this.annotationsSrv.saveAnnotationEvent(saveModel).then(() => {
|
||||
if (saveModel.id) {
|
||||
this.annotationsSrv.updateAnnotationEvent(saveModel)
|
||||
.then(() => {
|
||||
this.panelCtrl.refresh();
|
||||
this.close();
|
||||
})
|
||||
.catch(() => {
|
||||
this.panelCtrl.refresh();
|
||||
this.close();
|
||||
});
|
||||
} else {
|
||||
this.annotationsSrv.saveAnnotationEvent(saveModel)
|
||||
.then(() => {
|
||||
this.panelCtrl.refresh();
|
||||
this.close();
|
||||
})
|
||||
.catch(() => {
|
||||
this.panelCtrl.refresh();
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
timeChanged() {
|
||||
this.panelCtrl.render();
|
||||
delete() {
|
||||
return this.annotationsSrv.deleteAnnotationEvent(this.event)
|
||||
.then(() => {
|
||||
this.panelCtrl.refresh();
|
||||
this.close();
|
||||
})
|
||||
.catch(() => {
|
||||
this.panelCtrl.refresh();
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function tryEpochToMoment(timestamp) {
|
||||
if (timestamp && _.isNumber(timestamp)) {
|
||||
let epoch = Number(timestamp);
|
||||
return moment(epoch);
|
||||
} else {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import {MetricsPanelCtrl} from 'app/plugins/sdk';
|
||||
import {AnnotationEvent} from './event';
|
||||
import {OK_COLOR, ALERTING_COLOR, NO_DATA_COLOR, DEFAULT_ANNOTATION_COLOR, REGION_FILL_ALPHA} from 'app/core/utils/colors';
|
||||
|
||||
export class EventManager {
|
||||
event: AnnotationEvent;
|
||||
editorOpen: boolean;
|
||||
|
||||
constructor(private panelCtrl: MetricsPanelCtrl, private elem, private popoverSrv) {
|
||||
}
|
||||
constructor(private panelCtrl: MetricsPanelCtrl) {}
|
||||
|
||||
editorClosed() {
|
||||
console.log('editorClosed');
|
||||
this.event = null;
|
||||
this.editorOpen = false;
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
|
||||
updateTime(range) {
|
||||
let newEvent = true;
|
||||
editorOpened() {
|
||||
this.editorOpen = true;
|
||||
}
|
||||
|
||||
if (this.event) {
|
||||
newEvent = false;
|
||||
} else {
|
||||
// init new event
|
||||
updateTime(range) {
|
||||
if (!this.event) {
|
||||
this.event = new AnnotationEvent();
|
||||
this.event.dashboardId = this.panelCtrl.dashboard.id;
|
||||
this.event.panelId = this.panelCtrl.panel.id;
|
||||
@@ -35,25 +36,11 @@ export class EventManager {
|
||||
this.event.isRegion = true;
|
||||
}
|
||||
|
||||
// newEvent means the editor is not visible
|
||||
if (!newEvent) {
|
||||
this.panelCtrl.render();
|
||||
return;
|
||||
}
|
||||
|
||||
this.popoverSrv.show({
|
||||
element: this.elem[0],
|
||||
classNames: 'drop-popover drop-popover--form',
|
||||
position: 'bottom center',
|
||||
openOn: null,
|
||||
template: '<event-editor panel-ctrl="panelCtrl" event="event" close="dismiss()"></event-editor>',
|
||||
onClose: this.editorClosed.bind(this),
|
||||
model: {
|
||||
event: this.event,
|
||||
panelCtrl: this.panelCtrl,
|
||||
},
|
||||
});
|
||||
|
||||
editEvent(event, elem?) {
|
||||
this.event = event;
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
|
||||
@@ -63,36 +50,60 @@ export class EventManager {
|
||||
}
|
||||
|
||||
var types = {
|
||||
'$__alerting': {
|
||||
color: 'rgba(237, 46, 24, 1)',
|
||||
$__alerting: {
|
||||
color: ALERTING_COLOR,
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
},
|
||||
'$__ok': {
|
||||
color: 'rgba(11, 237, 50, 1)',
|
||||
$__ok: {
|
||||
color: OK_COLOR,
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
},
|
||||
'$__no_data': {
|
||||
color: 'rgba(150, 150, 150, 1)',
|
||||
$__no_data: {
|
||||
color: NO_DATA_COLOR,
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
},
|
||||
$__editing: {
|
||||
color: DEFAULT_ANNOTATION_COLOR,
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
},
|
||||
};
|
||||
|
||||
if (this.event) {
|
||||
if (this.event.isRegion) {
|
||||
annotations = [
|
||||
{
|
||||
isRegion: true,
|
||||
min: this.event.time.valueOf(),
|
||||
timeEnd: this.event.timeEnd.valueOf(),
|
||||
text: this.event.text,
|
||||
eventType: '$__editing',
|
||||
editModel: this.event,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
annotations = [
|
||||
{
|
||||
min: this.event.time.valueOf(),
|
||||
title: this.event.title,
|
||||
text: this.event.text,
|
||||
eventType: '$__alerting',
|
||||
}
|
||||
editModel: this.event,
|
||||
eventType: '$__editing',
|
||||
},
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// annotations from query
|
||||
for (var i = 0; i < annotations.length; i++) {
|
||||
var item = annotations[i];
|
||||
|
||||
// add properties used by jquery flot events
|
||||
item.min = item.time;
|
||||
item.max = item.time;
|
||||
item.eventType = item.source.name;
|
||||
|
||||
if (item.newState) {
|
||||
item.eventType = '$__' + item.newState;
|
||||
continue;
|
||||
@@ -108,10 +119,50 @@ export class EventManager {
|
||||
}
|
||||
}
|
||||
|
||||
let regions = getRegions(annotations);
|
||||
addRegionMarking(regions, flotOptions);
|
||||
|
||||
let eventSectionHeight = 20;
|
||||
let eventSectionMargin = 7;
|
||||
flotOptions.grid.eventSectionHeight = eventSectionMargin;
|
||||
flotOptions.xaxis.eventSectionHeight = eventSectionHeight;
|
||||
|
||||
flotOptions.events = {
|
||||
levels: _.keys(types).length + 1,
|
||||
data: annotations,
|
||||
types: types,
|
||||
manager: this,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getRegions(events) {
|
||||
return _.filter(events, 'isRegion');
|
||||
}
|
||||
|
||||
function addRegionMarking(regions, flotOptions) {
|
||||
let markings = flotOptions.grid.markings;
|
||||
let defaultColor = DEFAULT_ANNOTATION_COLOR;
|
||||
let fillColor;
|
||||
|
||||
_.each(regions, region => {
|
||||
if (region.source) {
|
||||
fillColor = region.source.iconColor || defaultColor;
|
||||
} else {
|
||||
fillColor = defaultColor;
|
||||
}
|
||||
|
||||
fillColor = addAlphaToRGB(fillColor, REGION_FILL_ALPHA);
|
||||
markings.push({xaxis: {from: region.min, to: region.timeEnd}, color: fillColor});
|
||||
});
|
||||
}
|
||||
|
||||
function addAlphaToRGB(colorString: string, alpha: number): string {
|
||||
let color = tinycolor(colorString);
|
||||
if (color.isValid()) {
|
||||
color.setAlpha(alpha);
|
||||
return color.toRgbString();
|
||||
} else {
|
||||
return colorString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +40,11 @@
|
||||
Annotations provide a way to integrate event data into your graphs. They are visualized as vertical lines and icons
|
||||
on all graph panels. When you hover over an annotation icon you can get title, tags, and text information for the event.
|
||||
In the <i>Queries</i> tab you can add queries that return annotation events.
|
||||
<br>
|
||||
<br>
|
||||
Checkout the <a class="external-link" target="_blank" href="http://docs.grafana.org/reference/annotations/">Annotations documentation</a> for more information.
|
||||
</p>
|
||||
<p>
|
||||
You can add annotations directly from grafana by holding CTRL or CMD + click on graph (or drag region). These will be stored in Grafana's annotation database.
|
||||
</p>
|
||||
Checkout the <a class="external-link" target="_blank" href="http://docs.grafana.org/reference/annotations/">Annotations documentation</a> for more information.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,13 +54,16 @@
|
||||
</div>
|
||||
<table class="grafana-options-table">
|
||||
<tr ng-repeat="annotation in ctrl.annotations">
|
||||
<td style="width:90%">
|
||||
<i class="fa fa-bolt" style="color:{{annotation.iconColor}}"></i>
|
||||
<td style="width:90%" ng-hide="annotation.builtIn">
|
||||
<i class="fa fa-comment" style="color:{{annotation.iconColor}}"></i>
|
||||
{{annotation.name}}
|
||||
</td>
|
||||
<td style="width:90%" ng-show="annotation.builtIn">
|
||||
<i class="fa fa-comment"></i>
|
||||
<em class="muted">{{annotation.name}} (Built-in)</em>
|
||||
</td>
|
||||
<td style="width: 1%"><i ng-click="_.move(ctrl.annotations,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
|
||||
<td style="width: 1%"><i ng-click="_.move(ctrl.annotations,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
|
||||
|
||||
<td style="width: 1%">
|
||||
<a ng-click="ctrl.edit(annotation)" class="btn btn-inverse btn-mini">
|
||||
<i class="fa fa-edit"></i>
|
||||
@@ -67,7 +71,7 @@
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 1%">
|
||||
<a ng-click="ctrl.removeAnnotation(annotation)" class="btn btn-danger btn-mini">
|
||||
<a ng-click="ctrl.removeAnnotation(annotation)" class="btn btn-danger btn-mini" ng-hide="annotation.builtIn">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
@@ -77,43 +81,49 @@
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.mode === 'list'">
|
||||
<div class="gf-form-button-row">
|
||||
<a type="button" class="btn gf-form-button btn-success" ng-click="ctrl.mode = 'new';"><i class="fa fa-plus" ></i> New</a>
|
||||
<a type="button" class="btn gf-form-button btn-success" ng-click="ctrl.setupNew()"><i class="fa fa-plus" ></i> New</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="annotations-basic-settings" ng-if="ctrl.mode === 'edit' || ctrl.mode === 'new'">
|
||||
<div>
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Options</h5>
|
||||
<h5 class="section-heading">General</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Name</span>
|
||||
<input type="text" class="gf-form-input width-12" ng-model='ctrl.currentAnnotation.name' placeholder="name"></input>
|
||||
<span class="gf-form-label width-7">Name</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model='ctrl.currentAnnotation.name' placeholder="name"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Data source</span>
|
||||
<div class="gf-form-select-wrapper width-12">
|
||||
<span class="gf-form-label width-7">Data source</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.currentAnnotation.datasource" ng-options="f.name as f.name for f in ctrl.datasources" ng-change="ctrl.datasourceChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<!-- <div class="gf-form"> -->
|
||||
<!-- <span class="gf-form-label width-7">Show in</span> -->
|
||||
<!-- <div class="gf-form-select-wrapper width-12"> -->
|
||||
<!-- <select class="gf-form-input" ng-model="ctrl.currentAnnotation.showIn" ng-options="f.value as f.text for f in ctrl.showOptions"></select> -->
|
||||
<!-- </div> -->
|
||||
<!-- </div> -->
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Hide toggle"
|
||||
label="Enabled"
|
||||
checked="ctrl.currentAnnotation.enable"
|
||||
on-change="ctrl.annotationEnabledChange()"
|
||||
label-class="width-7">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Hidden"
|
||||
tooltip="Hides the annotation query toggle from showing at the top of the dashboard"
|
||||
checked="ctrl.currentAnnotation.hide"
|
||||
label-class="width-9">
|
||||
on-change="ctrl.annotationHiddenChanged()"
|
||||
label-class="width-7">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">Color</label>
|
||||
<spectrum-picker class="gf-form-input width-3" ng-model="ctrl.currentAnnotation.iconColor"></spectrum-picker>
|
||||
<span class="gf-form-label">
|
||||
<color-picker color="ctrl.currentAnnotation.iconColor" onChange="ctrl.onColorChange"></color-picker>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,6 +141,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,35 @@
|
||||
|
||||
<h5 class="section-heading text-center">Add annotation</h5>
|
||||
<div class="graph-annotation">
|
||||
<div class="graph-annotation__header">
|
||||
<div class="graph-annotation__user" bs-tooltip="'Created by {{ctrl.login}}'">
|
||||
</div>
|
||||
|
||||
<form name="ctrl.form" class="text-center">
|
||||
<div class="graph-annotation__title">
|
||||
<span ng-if="!ctrl.event.id">Add Annotation</span>
|
||||
<span ng-if="ctrl.event.id">Edit Annotation</span>
|
||||
</div>
|
||||
|
||||
<div class="graph-annotation__time">{{ctrl.timeFormated}}</div>
|
||||
</div>
|
||||
|
||||
<form name="ctrl.form" class="graph-annotation__body text-center">
|
||||
<div style="display: inline-block">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Title</span>
|
||||
<input type="text" ng-model="ctrl.event.title" class="gf-form-input max-width-20" required>
|
||||
</div>
|
||||
<!-- single event -->
|
||||
<div ng-if="!ctrl.event.isRegion">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Time</span>
|
||||
<input type="text" ng-model="ctrl.event.time" class="gf-form-input max-width-20" input-datetime required ng-change="ctrl.timeChanged()">
|
||||
</div>
|
||||
</div>
|
||||
<!-- region event -->
|
||||
<div ng-if="ctrl.event.isRegion">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Start</span>
|
||||
<input type="text" ng-model="ctrl.event.time" class="gf-form-input max-width-20" input-datetime required ng-change="ctrl.timeChanged()">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">End</span>
|
||||
<input type="text" ng-model="ctrl.event.timeEnd" class="gf-form-input max-width-20" input-datetime required ng-change="ctrl.timeChanged()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-7">Description</span>
|
||||
<textarea class="gf-form-input width-20" rows="3" ng-model="ctrl.event.text" placeholder="Event description"></textarea>
|
||||
<textarea class="gf-form-input width-20" rows="2" ng-model="ctrl.event.text" placeholder="Description"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Tags</span>
|
||||
<bootstrap-tagsinput ng-model="ctrl.event.tags" tagclass="label label-tag" placeholder="add tags">
|
||||
</bootstrap-tagsinput>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn gf-form-btn btn-success" ng-click="ctrl.save()">Save</button>
|
||||
<button type="submit" class="btn btn-success" ng-click="ctrl.save()">Save</button>
|
||||
<button ng-if="ctrl.event.id" type="submit" class="btn btn-danger" ng-click="ctrl.delete()">Delete</button>
|
||||
<a class="btn-text" ng-click="ctrl.close();">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||
import '../annotations_srv';
|
||||
import helpers from 'test/specs/helpers';
|
||||
|
||||
describe('AnnotationsSrv', function() {
|
||||
var ctx = new helpers.ServiceTestContext();
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(() => {
|
||||
ctx.createService('annotationsSrv');
|
||||
});
|
||||
describe('When translating the query result', () => {
|
||||
const annotationSource = {
|
||||
datasource: '-- Grafana --',
|
||||
enable: true,
|
||||
hide: false,
|
||||
limit: 200,
|
||||
name: 'test',
|
||||
scope: 'global',
|
||||
tags: [
|
||||
'test'
|
||||
],
|
||||
type: 'event',
|
||||
};
|
||||
|
||||
const time = 1507039543000;
|
||||
const annotations = [{id: 1, panelId: 1, text: 'text', time: time}];
|
||||
let translatedAnnotations;
|
||||
|
||||
beforeEach(() => {
|
||||
translatedAnnotations = ctx.service.translateQueryResult(annotationSource, annotations);
|
||||
});
|
||||
|
||||
it('should set defaults', () => {
|
||||
expect(translatedAnnotations[0].source).to.eql(annotationSource);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import * as fileExport from 'app/core/utils/file_export';
|
||||
import appEvents from 'app/core/app_events';
|
||||
@@ -8,7 +6,7 @@ export class ExportDataModalCtrl {
|
||||
private data: any[];
|
||||
private panel: string;
|
||||
asRows: Boolean = true;
|
||||
dateTimeFormat: String = 'YYYY-MM-DDTHH:mm:ssZ';
|
||||
dateTimeFormat = 'YYYY-MM-DDTHH:mm:ssZ';
|
||||
excel: false;
|
||||
|
||||
export() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
|
||||
import {DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors';
|
||||
import {Emitter, contextSrv, appEvents} from 'app/core/core';
|
||||
import {DashboardRow} from './row/row_model';
|
||||
import sortByKeys from 'app/core/utils/sort_by_keys';
|
||||
@@ -82,10 +83,35 @@ export class DashboardModel {
|
||||
this.panels = data.panels || [];
|
||||
this.rows = [];
|
||||
|
||||
this.addBuiltInAnnotationQuery();
|
||||
this.initMeta(meta);
|
||||
this.updateSchema(data);
|
||||
}
|
||||
|
||||
addBuiltInAnnotationQuery() {
|
||||
let found = false;
|
||||
for (let item of this.annotations.list) {
|
||||
if (item.builtIn === 1) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.annotations.list.unshift({
|
||||
datasource: '-- Grafana --',
|
||||
name: 'Annotations & Alerts',
|
||||
type: 'dashboard',
|
||||
iconColor: DEFAULT_ANNOTATION_COLOR,
|
||||
enable: true,
|
||||
hide: true,
|
||||
builtIn: 1,
|
||||
});
|
||||
}
|
||||
|
||||
private initMeta(meta) {
|
||||
meta = meta || {};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user