mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 23:32:11 +08:00
Merge branch 'master' into develop
This commit is contained in:
@ -14,7 +14,7 @@
|
|||||||
* **Prometheus**: Adds /metrics endpoint for exposing Grafana metrics. [#9187](https://github.com/grafana/grafana/pull/9187)
|
* **Prometheus**: Adds /metrics endpoint for exposing Grafana metrics. [#9187](https://github.com/grafana/grafana/pull/9187)
|
||||||
* **Graph**: Add support for local formating in axis. [#1395](https://github.com/grafana/grafana/issues/1395), thx [@m0nhawk](https://github.com/m0nhawk)
|
* **Graph**: Add support for local formating in axis. [#1395](https://github.com/grafana/grafana/issues/1395), thx [@m0nhawk](https://github.com/m0nhawk)
|
||||||
* **Jaeger**: Add support for open tracing using jaeger in Grafana. [#9213](https://github.com/grafana/grafana/pull/9213)
|
* **Jaeger**: Add support for open tracing using jaeger in Grafana. [#9213](https://github.com/grafana/grafana/pull/9213)
|
||||||
* **Unit types**: New date & time unit types added, useful in singlestat to show dates & times. [#3678](https://github.com/grafana/grafana/issues/3678), [#6710](https://github.com/grafana/grafana/issues/6710), [#2764](https://github.com/grafana/grafana/issues/6710)
|
* **Unit types**: New date & time unit types added, useful in singlestat to show dates & times. [#3678](https://github.com/grafana/grafana/issues/3678), [#6710](https://github.com/grafana/grafana/issues/6710), [#2764](https://github.com/grafana/grafana/issues/2764)
|
||||||
* **CLI**: Make it possible to install plugins from any url [#5873](https://github.com/grafana/grafana/issues/5873)
|
* **CLI**: Make it possible to install plugins from any url [#5873](https://github.com/grafana/grafana/issues/5873)
|
||||||
* **Prometheus**: Add support for instant queries [#5765](https://github.com/grafana/grafana/issues/5765), thx [@mtanda](https://github.com/mtanda)
|
* **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)
|
* **Cloudwatch**: Add support for alerting using the cloudwatch datasource [#8050](https://github.com/grafana/grafana/pull/8050), thx [@mtanda](https://github.com/mtanda)
|
||||||
@ -23,6 +23,8 @@
|
|||||||
* **Prometheus**: Align $__interval with the step parameters. [#9226](https://github.com/grafana/grafana/pull/9226), thx [@alin-amana](https://github.com/alin-amana)
|
* **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)
|
* **Prometheus**: Autocomplete for label name and label value [#9208](https://github.com/grafana/grafana/pull/9208), thx [@mtanda](https://github.com/mtanda)
|
||||||
* **Postgres**: New Postgres data source [#9209](https://github.com/grafana/grafana/pull/9209), thx [@svenklemm](https://github.com/svenklemm)
|
* **Postgres**: New Postgres data source [#9209](https://github.com/grafana/grafana/pull/9209), thx [@svenklemm](https://github.com/svenklemm)
|
||||||
|
* **Datasources**: Make datasource HTTP requests verify TLS by default. closes [#9371](https://github.com/grafana/grafana/issues/9371), [#5334](https://github.com/grafana/grafana/issues/5334), [#8812](https://github.com/grafana/grafana/issues/8812), thx [@mattbostock](https://github.com/mattbostock)
|
||||||
|
* **OAuth**: Verify TLS during OAuth callback [#9373](https://github.com/grafana/grafana/issues/9373), thx [@mattbostock](https://github.com/mattbostock)
|
||||||
|
|
||||||
## Minor
|
## Minor
|
||||||
* **SMTP**: Make it possible to set specific EHLO for smtp client. [#9319](https://github.com/grafana/grafana/issues/9319)
|
* **SMTP**: Make it possible to set specific EHLO for smtp client. [#9319](https://github.com/grafana/grafana/issues/9319)
|
||||||
@ -33,9 +35,11 @@
|
|||||||
* **Opsgenie**: Use their latest API instead of old version [#9399](https://github.com/grafana/grafana/pull/9399), thx [@cglrkn](https://github.com/cglrkn)
|
* **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)
|
* **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)
|
* **Hipchat**: Add metrics, message and image to hipchat notifications [#9110](https://github.com/grafana/grafana/issues/9110), thx [@eloo](https://github.com/eloo)
|
||||||
|
* **Kafka**: Add support for sending alert notifications to kafka [#7104](https://github.com/grafana/grafana/issues/7104), thx [@utkarshcmu](https://github.com/utkarshcmu)
|
||||||
|
|
||||||
## Tech
|
## Tech
|
||||||
* **Go**: Grafana is now built using golang 1.9
|
* **Go**: Grafana is now built using golang 1.9
|
||||||
|
* **Webpack**: Changed from systemjs to webpack (see readme or building from source guide for new build instructions). Systemjs is still used to load plugins but now plugins can only import a limited set of dependencies. See [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) for more details on how this can effect some plugins.
|
||||||
|
|
||||||
# 4.5.2 (2017-09-22)
|
# 4.5.2 (2017-09-22)
|
||||||
|
|
||||||
|
28
PLUGIN_DEV.md
Normal file
28
PLUGIN_DEV.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Plugin Development
|
||||||
|
|
||||||
|
This document is not meant as complete guide for developing plugins but more as a changelog for changes in
|
||||||
|
Grafana that can impact plugin development. When ever you as plugin author encounter an issue with your plugin after
|
||||||
|
upgrading Grafana please check here before creating an issue.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [Datasource plugin written in typescript](https://github.com/grafana/typescript-template-datasource)
|
||||||
|
- [Simple json dataource plugin](https://github.com/grafana/simple-json-datasource)
|
||||||
|
- [Plugin development guide](http://docs.grafana.org/plugins/developing/development/)
|
||||||
|
|
||||||
|
## Changes in v4.6
|
||||||
|
|
||||||
|
This version of Grafana has big changes that will impact a limited set of plugins. We moved from systemjs to webpack
|
||||||
|
for built-in plugins & everything internal. External plugins still use systemjs but now with a limited
|
||||||
|
set of Grafana components they can import. Plugins can depend on libs like lodash & moment and internal components
|
||||||
|
like before using the same import paths. However since everything in Grafana is no longer accessible, a few plugins could encounter issues when importing a Grafana dependency.
|
||||||
|
|
||||||
|
[List of exposed components plugins can import/require](https://github.com/grafana/grafana/blob/master/public/app/features/plugins/plugin_loader.ts#L48)
|
||||||
|
|
||||||
|
If you think we missed exposing a crucial lib or Grafana component let us know by opening an issue.
|
||||||
|
|
||||||
|
### Deprecated components
|
||||||
|
|
||||||
|
The angular directive `<spectrum-picker>` is no deprecated (will still work for a version more) but we recommend plugin authors
|
||||||
|
to upgrade to new `<color-picker color="ctrl.color" onChange="ctrl.onSparklineColorChange"></color-picker>`
|
||||||
|
|
@ -82,10 +82,17 @@ You only need to add the options you want to override. Config files are applied
|
|||||||
In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode = development`.
|
In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode = development`.
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
If you have any idea for an improvement or found a bug do not hesitate to open an issue.
|
If you have any idea for an improvement or found a bug do not hesitate to open an issue.
|
||||||
And if you have time clone this repo and submit a pull request and help me make Grafana
|
And if you have time clone this repo and submit a pull request and help me make Grafana
|
||||||
the kickass metrics & devops dashboard we all dream about!
|
the kickass metrics & devops dashboard we all dream about!
|
||||||
|
|
||||||
|
## Plugin development
|
||||||
|
|
||||||
|
Checkout the [Plugin Development Guide](http://docs.grafana.org/plugins/developing/development/) and checkout the [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) file for changes in Grafana that relate to
|
||||||
|
plugin development.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Grafana is distributed under Apache 2.0 License.
|
Grafana is distributed under Apache 2.0 License.
|
||||||
Work in progress Grafana 2.0 (with included Grafana backend)
|
|
||||||
|
@ -115,6 +115,17 @@ In DingTalk PC Client:
|
|||||||
|
|
||||||
Dingtalk supports the following "message type": `text`, `link` and `markdown`. Only the `text` message type is supported.
|
Dingtalk supports the following "message type": `text`, `link` and `markdown`. Only the `text` message type is supported.
|
||||||
|
|
||||||
|
### Kafka
|
||||||
|
|
||||||
|
Notifications can be sent to a Kafka topic from Grafana using [Kafka REST Proxy](https://docs.confluent.io/1.0/kafka-rest/docs/index.html).
|
||||||
|
There are couple of configurations options which need to be set in Grafana UI under Kafka Settings:
|
||||||
|
|
||||||
|
1. Kafka REST Proxy endpoint.
|
||||||
|
|
||||||
|
2. Kafka Topic.
|
||||||
|
|
||||||
|
Once these two properties are set, you can send the alerts to Kafka for further processing or throttling them.
|
||||||
|
|
||||||
### Other Supported Notification Channels
|
### Other Supported Notification Channels
|
||||||
|
|
||||||
Grafana also supports the following Notification Channels:
|
Grafana also supports the following Notification Channels:
|
||||||
|
@ -135,6 +135,5 @@ Name | Description
|
|||||||
------------ | -------------
|
------------ | -------------
|
||||||
Query | You can leave the search query blank or specify a lucene query
|
Query | You can leave the search query blank or specify a lucene query
|
||||||
Time | The name of the time field, needs to be date field.
|
Time | The name of the time field, needs to be date field.
|
||||||
Title | The name of the field to use for the event title.
|
Text | Event description field.
|
||||||
Tags | Optional field name to use for event tags (can be an array or a CSV string).
|
Tags | Optional field name to use for event tags (can be an array or a CSV string).
|
||||||
Text | Optional field name to use event text body.
|
|
||||||
|
@ -18,7 +18,7 @@ The alert list panel allows you to display your dashbords alerts. The list can b
|
|||||||
|
|
||||||
## Alert List Options
|
## Alert List Options
|
||||||
|
|
||||||
{{< docs-imagebox img="/img/docs/v45/alert-list-options.png" max-width="600px" class="docs-image--no-shadow docs-image--right">}}
|
{{< docs-imagebox img="/img/docs/v45/alert-list-options.png" max-width="600px" class="docs-image--no-shadow docs-image--right" >}}
|
||||||
|
|
||||||
1. **Show**: Lets you choose between current state or recent state changes.
|
1. **Show**: Lets you choose between current state or recent state changes.
|
||||||
2. **Max Items**: Max items set the maximum of items in a list.
|
2. **Max Items**: Max items set the maximum of items in a list.
|
||||||
|
74
docs/sources/guides/whats-new-in-v4-6.md
Normal file
74
docs/sources/guides/whats-new-in-v4-6.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
+++
|
||||||
|
title = "What's New in Grafana v4.6"
|
||||||
|
description = "Feature & improvement highlights for Grafana v4.6"
|
||||||
|
keywords = ["grafana", "new", "documentation", "4.6"]
|
||||||
|
type = "docs"
|
||||||
|
[menu.docs]
|
||||||
|
name = "Version 4.6"
|
||||||
|
identifier = "v4.6"
|
||||||
|
parent = "whatsnew"
|
||||||
|
weight = -5
|
||||||
|
+++
|
||||||
|
|
||||||
|
# What's New in Grafana v4.6
|
||||||
|
|
||||||
|
Grafana v4.6 brings many enhancements to Annotations, Cloudwatch & Prometheus. It also adds support for Postgres as metric & table data source!
|
||||||
|
|
||||||
|
### Annotations
|
||||||
|
|
||||||
|
{{< docs-imagebox img="/img/docs/v46/add_annotation_region.png" max-width= "800px" >}}
|
||||||
|
|
||||||
|
You can now add annotation events and regions right from the graph panel! Just hold CTRL/CMD + click or drag region to open the **Add Annotation** view. The
|
||||||
|
[Annotations]({{< relref "reference/annotations.md" >}}) documentation is updated to include details on this new exciting feature.
|
||||||
|
|
||||||
|
### Cloudwatch
|
||||||
|
|
||||||
|
Cloudwatch now supports alerting. Setup alert rules for any Cloudwatch metric!
|
||||||
|
|
||||||
|
{{< docs-imagebox img="/img/docs/v46/cloudwatch_alerting.png" max-width= "800px" >}}
|
||||||
|
|
||||||
|
### Postgres
|
||||||
|
|
||||||
|
Grafana v4.6 now ships with a built-in datasource plugin for Postgres. Have logs or metric data in Postgres? You can now visualize that data and
|
||||||
|
define alert rules on it like any of our other data sources.
|
||||||
|
|
||||||
|
{{< docs-imagebox img="/img/docs/v46/postgres_table_query.png" max-width= "800px" >}}
|
||||||
|
|
||||||
|
### Prometheus
|
||||||
|
|
||||||
|
New enhancements include support for **instant queries** and improvements to query editor in the form of autocomplete for label names and label values.
|
||||||
|
This makes exploring and filtering Prometheus data much easier.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
* **GCS**: Adds support for Google Cloud Storage [#8370](https://github.com/grafana/grafana/issues/8370) thx [@chuhlomin](https://github.com/chuhlomin)
|
||||||
|
* **Prometheus**: Adds /metrics endpoint for exposing Grafana metrics. [#9187](https://github.com/grafana/grafana/pull/9187)
|
||||||
|
* **Graph**: Add support for local formating in axis. [#1395](https://github.com/grafana/grafana/issues/1395), thx [@m0nhawk](https://github.com/m0nhawk)
|
||||||
|
* **Jaeger**: Add support for open tracing using jaeger in Grafana. [#9213](https://github.com/grafana/grafana/pull/9213)
|
||||||
|
* **Unit types**: New date & time unit types added, useful in singlestat to show dates & times. [#3678](https://github.com/grafana/grafana/issues/3678), [#6710](https://github.com/grafana/grafana/issues/6710), [#2764](https://github.com/grafana/grafana/issues/2764)
|
||||||
|
* **CLI**: Make it possible to install plugins from any url [#5873](https://github.com/grafana/grafana/issues/5873)
|
||||||
|
* **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)
|
||||||
|
* **Postgres**: New Postgres data source [#9209](https://github.com/grafana/grafana/pull/9209), thx [@svenklemm](https://github.com/svenklemm)
|
||||||
|
* **Datasources**: closes [#9371](https://github.com/grafana/grafana/issues/9371), [#5334](https://github.com/grafana/grafana/issues/5334), [#8812](https://github.com/grafana/grafana/issues/8812), thx [@mattbostock](https://github.com/mattbostock)
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
* **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
|
||||||
|
|
@ -10,34 +10,37 @@ weight = 2
|
|||||||
|
|
||||||
# Annotations
|
# Annotations
|
||||||
|
|
||||||
|
{{< docs-imagebox img="/img/docs/v46/annotations.png" max-width="800px" >}}
|
||||||
|
|
||||||
Annotations provide a way to mark points on the graph with rich events. When you hover over an annotation
|
Annotations provide a way to mark points on the graph with rich events. When you hover over an annotation
|
||||||
you can get event description and event tags. The text field can include links to other systems with more detail.
|
you can get event description and event tags. The text field can include links to other systems with more detail.
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Native annotations
|
## Native annotations
|
||||||
|
|
||||||
Grafana v4.6+ comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the [HTTP API]({{< relref "http_api/annotations.md" >}})
|
Grafana v4.6+ comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the [HTTP API]({{< relref "http_api/annotations.md" >}}).
|
||||||
|
|
||||||
## Adding annotations
|
## Adding annotations
|
||||||
|
|
||||||
by holding down CTRL/CMD + mouse click. Add tags to the annotation will make it searchable from other dashboards.
|
By holding down CTRL/CMD + mouse click. Add tags to the annotation will make it searchable from other dashboards.
|
||||||
|
|
||||||
<!-- adding annoation gif animation -->
|
{{< docs-imagebox img="/img/docs/annotations/annotation-still.png"
|
||||||
|
max-width="600px" animated-gif="/img/docs/annotations/annotation.gif" >}}
|
||||||
|
|
||||||
### Adding regions events
|
### Adding regions events
|
||||||
|
|
||||||
You can also hold down CTRL/CMD and select region to create a region annotation.
|
You can also hold down CTRL/CMD and select region to create a region annotation.
|
||||||
|
|
||||||
<!-- region image/gif animation -->
|
{{< docs-imagebox img="/img/docs/annotations/region-annotation-still.png"
|
||||||
|
max-width="600px" animated-gif="/img/docs/annotations/region-annotation.gif" >}}
|
||||||
|
|
||||||
### Built in query
|
### Built in query
|
||||||
|
|
||||||
After you added an an annotation they will be still be visible. This is due to the built in annotation query that exists on all dashboards. This annotation query will
|
After you added an annotation they will still be visible. This is due to the built in annotation query that exists on all dashboards. This annotation query will
|
||||||
fetch all annotation events that originate from the current dashboard and show them on the panel where they where created. This includes alert state history annotations. You can
|
fetch all annotation events that originate from the current dashboard and show them on the panel where they where created. This includes alert state history annotations. You can
|
||||||
stop annotations from being fetched & drawn by opening the **Annotations** settings (via Dashboard cogs menu) and modifying the query named `Annotations & Alerts (Built-in)`.
|
stop annotations from being fetched & drawn by opening the **Annotations** settings (via Dashboard cogs menu) and modifying the query named `Annotations & Alerts (Built-in)`.
|
||||||
|
|
||||||
<!-- image of built in query -->
|
When you copy a dashboard using the **Save As** feature it will get a new dashboard id so annotations created on source dashboard will no longer be visible on the copy. You
|
||||||
|
can still show them if you add a new **Annotation Query** and filter by tags. But this only works if the annotations on the source dashboard had tags to filter by.
|
||||||
|
|
||||||
### Query by tag
|
### Query by tag
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"company": "Grafana Labs"
|
"company": "Grafana Labs"
|
||||||
},
|
},
|
||||||
"name": "grafana",
|
"name": "grafana",
|
||||||
"version": "4.6.0-pre1",
|
"version": "4.6.0-beta1",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "http://github.com/grafana/grafana.git"
|
"url": "http://github.com/grafana/grafana.git"
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
@ -78,6 +82,60 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response
|
|||||||
return ApiSuccess("Annotation added")
|
return ApiSuccess("Annotation added")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GraphiteAnnotationError struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GraphiteAnnotationError) Error() string {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatGraphiteAnnotation(what string, data string) string {
|
||||||
|
return fmt.Sprintf("%s\n%s", what, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotationsCmd) Response {
|
||||||
|
repo := annotations.GetRepository()
|
||||||
|
|
||||||
|
if cmd.When == 0 {
|
||||||
|
cmd.When = time.Now().Unix()
|
||||||
|
}
|
||||||
|
text := formatGraphiteAnnotation(cmd.What, cmd.Data)
|
||||||
|
|
||||||
|
// Support tags in prior to Graphite 0.10.0 format (string of tags separated by space)
|
||||||
|
var tagsArray []string
|
||||||
|
switch tags := cmd.Tags.(type) {
|
||||||
|
case string:
|
||||||
|
tagsArray = strings.Split(tags, " ")
|
||||||
|
case []interface{}:
|
||||||
|
for _, t := range tags {
|
||||||
|
if tagStr, ok := t.(string); ok {
|
||||||
|
tagsArray = append(tagsArray, tagStr)
|
||||||
|
} else {
|
||||||
|
err := &GraphiteAnnotationError{"tag should be a string"}
|
||||||
|
return ApiError(500, "Failed to save Graphite annotation", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err := &GraphiteAnnotationError{"unsupported tags format"}
|
||||||
|
return ApiError(500, "Failed to save Graphite annotation", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
item := annotations.Item{
|
||||||
|
OrgId: c.OrgId,
|
||||||
|
UserId: c.UserId,
|
||||||
|
Epoch: cmd.When,
|
||||||
|
Text: text,
|
||||||
|
Tags: tagsArray,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.Save(&item); err != nil {
|
||||||
|
return ApiError(500, "Failed to save Graphite annotation", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiSuccess("Graphite Annotation added")
|
||||||
|
}
|
||||||
|
|
||||||
func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Response {
|
func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Response {
|
||||||
annotationId := c.ParamsInt64(":annotationId")
|
annotationId := c.ParamsInt64(":annotationId")
|
||||||
|
|
||||||
|
@ -304,6 +304,7 @@ func (hs *HttpServer) registerRoutes() {
|
|||||||
annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationById))
|
annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationById))
|
||||||
annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation))
|
annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation))
|
||||||
annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion))
|
annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion))
|
||||||
|
annotationsRoute.Post("/graphite", bind(dtos.PostGraphiteAnnotationsCmd{}), wrap(PostGraphiteAnnotation))
|
||||||
}, reqEditorRole)
|
}, reqEditorRole)
|
||||||
|
|
||||||
// error test
|
// error test
|
||||||
|
@ -6,31 +6,33 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gopkg.in/macaron.v1"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/pluginproxy"
|
"github.com/grafana/grafana/pkg/api/pluginproxy"
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
macaron "gopkg.in/macaron.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
var pluginProxyTransport = &http.Transport{
|
var pluginProxyTransport *http.Transport
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
Renegotiation: tls.RenegotiateFreelyAsClient,
|
|
||||||
},
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
Dial: (&net.Dialer{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
DualStack: true,
|
|
||||||
}).Dial,
|
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitAppPluginRoutes(r *macaron.Macaron) {
|
func InitAppPluginRoutes(r *macaron.Macaron) {
|
||||||
|
pluginProxyTransport = &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: setting.PluginAppsSkipVerifyTLS,
|
||||||
|
Renegotiation: tls.RenegotiateFreelyAsClient,
|
||||||
|
},
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
Dial: (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
DualStack: true,
|
||||||
|
}).Dial,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
for _, plugin := range plugins.Apps {
|
for _, plugin := range plugins.Apps {
|
||||||
for _, route := range plugin.Routes {
|
for _, route := range plugin.Routes {
|
||||||
url := util.JoinUrlFragments("/api/plugin-proxy/"+plugin.Id, route.Path)
|
url := util.JoinUrlFragments("/api/plugin-proxy/"+plugin.Id, route.Path)
|
||||||
|
@ -29,3 +29,10 @@ type DeleteAnnotationsCmd struct {
|
|||||||
AnnotationId int64 `json:"annotationId"`
|
AnnotationId int64 `json:"annotationId"`
|
||||||
RegionId int64 `json:"regionId"`
|
RegionId int64 `json:"regionId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PostGraphiteAnnotationsCmd struct {
|
||||||
|
When int64 `json:"when"`
|
||||||
|
What string `json:"what"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
Tags interface{} `json:"tags"`
|
||||||
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
@ -14,8 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var grafanaComProxyTransport = &http.Transport{
|
var grafanaComProxyTransport = &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
|
Proxy: http.ProxyFromEnvironment,
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
Dial: (&net.Dialer{
|
Dial: (&net.Dialer{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
KeepAlive: 30 * time.Second,
|
KeepAlive: 30 * time.Second,
|
||||||
|
@ -11,6 +11,8 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
|
||||||
gocache "github.com/patrickmn/go-cache"
|
gocache "github.com/patrickmn/go-cache"
|
||||||
@ -19,7 +21,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/live"
|
"github.com/grafana/grafana/pkg/api/live"
|
||||||
httpstatic "github.com/grafana/grafana/pkg/api/static"
|
httpstatic "github.com/grafana/grafana/pkg/api/static"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
@ -153,7 +154,7 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
|
|||||||
|
|
||||||
for _, route := range plugins.StaticRoutes {
|
for _, route := range plugins.StaticRoutes {
|
||||||
pluginRoute := path.Join("/public/plugins/", route.PluginId)
|
pluginRoute := path.Join("/public/plugins/", route.PluginId)
|
||||||
logger.Debug("Plugins: Adding route", "route", pluginRoute, "dir", route.Directory)
|
hs.log.Debug("Plugins: Adding route", "route", pluginRoute, "dir", route.Directory)
|
||||||
hs.mapStatic(m, route.Directory, "", pluginRoute)
|
hs.mapStatic(m, route.Directory, "", pluginRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +188,9 @@ func (hs *HttpServer) metricsEndpoint(ctx *macaron.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
promhttp.Handler().ServeHTTP(ctx.Resp, ctx.Req.Request)
|
promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{
|
||||||
|
DisableCompression: true,
|
||||||
|
}).ServeHTTP(ctx.Resp, ctx.Req.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HttpServer) healthHandler(ctx *macaron.Context) {
|
func (hs *HttpServer) healthHandler(ctx *macaron.Context) {
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
@ -16,6 +15,7 @@ import (
|
|||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/metrics"
|
"github.com/grafana/grafana/pkg/metrics"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
@ -29,6 +29,7 @@ var (
|
|||||||
ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
|
ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
|
||||||
ErrUsersQuotaReached = errors.New("Users quota reached")
|
ErrUsersQuotaReached = errors.New("Users quota reached")
|
||||||
ErrNoEmail = errors.New("Login provider didn't return an email address")
|
ErrNoEmail = errors.New("Login provider didn't return an email address")
|
||||||
|
oauthLogger = log.New("oauth.login")
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenStateString() string {
|
func GenStateString() string {
|
||||||
@ -50,10 +51,11 @@ func OAuthLogin(ctx *middleware.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
error := ctx.Query("error")
|
errorParam := ctx.Query("error")
|
||||||
if error != "" {
|
if errorParam != "" {
|
||||||
errorDesc := ctx.Query("error_description")
|
errorDesc := ctx.Query("error_description")
|
||||||
redirectWithError(ctx, ErrProviderDeniedRequest, "error", error, "errorDesc", errorDesc)
|
oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc)
|
||||||
|
redirectWithError(ctx, ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,8 +71,12 @@ func OAuthLogin(ctx *middleware.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify state string
|
savedState, ok := ctx.Session.Get(middleware.SESS_KEY_OAUTH_STATE).(string)
|
||||||
savedState := ctx.Session.Get(middleware.SESS_KEY_OAUTH_STATE).(string)
|
if !ok {
|
||||||
|
ctx.Handle(500, "login.OAuthLogin(missing saved state)", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
queryState := ctx.Query("state")
|
queryState := ctx.Query("state")
|
||||||
if savedState != queryState {
|
if savedState != queryState {
|
||||||
ctx.Handle(500, "login.OAuthLogin(state mismatch)", nil)
|
ctx.Handle(500, "login.OAuthLogin(state mismatch)", nil)
|
||||||
@ -78,36 +84,37 @@ func OAuthLogin(ctx *middleware.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle call back
|
// handle call back
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: setting.OAuthService.OAuthInfos[name].TlsSkipVerify,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
oauthClient := &http.Client{
|
||||||
|
Transport: tr,
|
||||||
|
}
|
||||||
|
|
||||||
// initialize oauth2 context
|
if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" || setting.OAuthService.OAuthInfos[name].TlsClientKey != "" {
|
||||||
oauthCtx := oauth2.NoContext
|
|
||||||
if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" {
|
|
||||||
cert, err := tls.LoadX509KeyPair(setting.OAuthService.OAuthInfos[name].TlsClientCert, setting.OAuthService.OAuthInfos[name].TlsClientKey)
|
cert, err := tls.LoadX509KeyPair(setting.OAuthService.OAuthInfos[name].TlsClientCert, setting.OAuthService.OAuthInfos[name].TlsClientKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(1, "Failed to setup TlsClientCert", "oauth provider", name, "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load CA cert
|
tr.TLSClientConfig.Certificates = append(tr.TLSClientConfig.Certificates, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
if setting.OAuthService.OAuthInfos[name].TlsClientCa != "" {
|
||||||
caCert, err := ioutil.ReadFile(setting.OAuthService.OAuthInfos[name].TlsClientCa)
|
caCert, err := ioutil.ReadFile(setting.OAuthService.OAuthInfos[name].TlsClientCa)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(1, "Failed to setup TlsClientCa", "oauth provider", name, "error", err)
|
||||||
}
|
}
|
||||||
caCertPool := x509.NewCertPool()
|
caCertPool := x509.NewCertPool()
|
||||||
caCertPool.AppendCertsFromPEM(caCert)
|
caCertPool.AppendCertsFromPEM(caCert)
|
||||||
|
|
||||||
tr := &http.Transport{
|
tr.TLSClientConfig.RootCAs = caCertPool
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
Certificates: []tls.Certificate{cert},
|
|
||||||
RootCAs: caCertPool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
sslcli := &http.Client{Transport: tr}
|
|
||||||
|
|
||||||
oauthCtx = context.Background()
|
|
||||||
oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, sslcli)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oauthCtx := context.WithValue(context.Background(), oauth2.HTTPClient, oauthClient)
|
||||||
|
|
||||||
// get token from provider
|
// get token from provider
|
||||||
token, err := connect.Exchange(oauthCtx, code)
|
token, err := connect.Exchange(oauthCtx, code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -17,8 +17,6 @@ var version = "master"
|
|||||||
func main() {
|
func main() {
|
||||||
setupLogging()
|
setupLogging()
|
||||||
|
|
||||||
services.Init(version)
|
|
||||||
|
|
||||||
app := cli.NewApp()
|
app := cli.NewApp()
|
||||||
app.Name = "Grafana cli"
|
app.Name = "Grafana cli"
|
||||||
app.Usage = ""
|
app.Usage = ""
|
||||||
@ -44,12 +42,20 @@ func main() {
|
|||||||
Value: "",
|
Value: "",
|
||||||
EnvVar: "GF_PLUGIN_URL",
|
EnvVar: "GF_PLUGIN_URL",
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "insecure",
|
||||||
|
Usage: "Skip TLS verification (insecure)",
|
||||||
|
},
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "debug, d",
|
Name: "debug, d",
|
||||||
Usage: "enable debug logging",
|
Usage: "enable debug logging",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.Before = func(c *cli.Context) error {
|
||||||
|
services.Init(version, c.GlobalBool("insecure"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
app.Commands = commands.Commands
|
app.Commands = commands.Commands
|
||||||
app.CommandNotFound = cmdNotFound
|
app.CommandNotFound = cmdNotFound
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ var (
|
|||||||
grafanaVersion string
|
grafanaVersion string
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init(version string) {
|
func Init(version string, skipTLSVerify bool) {
|
||||||
grafanaVersion = version
|
grafanaVersion = version
|
||||||
|
|
||||||
tr := &http.Transport{
|
tr := &http.Transport{
|
||||||
@ -36,8 +36,9 @@ func Init(version string) {
|
|||||||
IdleConnTimeout: 90 * time.Second,
|
IdleConnTimeout: 90 * time.Second,
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
|
InsecureSkipVerify: skipTLSVerify,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpClient = http.Client{
|
HttpClient = http.Client{
|
||||||
|
@ -3,6 +3,7 @@ package models
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
@ -45,9 +46,16 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
|
|||||||
return t.Transport, nil
|
return t.Transport, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool
|
||||||
|
if ds.JsonData != nil {
|
||||||
|
tlsClientAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
|
||||||
|
tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
|
||||||
|
tlsSkipVerify = ds.JsonData.Get("tlsSkipVerify").MustBool(false)
|
||||||
|
}
|
||||||
|
|
||||||
transport := &http.Transport{
|
transport := &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: tlsSkipVerify,
|
||||||
Renegotiation: tls.RenegotiateFreelyAsClient,
|
Renegotiation: tls.RenegotiateFreelyAsClient,
|
||||||
},
|
},
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
@ -62,30 +70,24 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
|
|||||||
IdleConnTimeout: 90 * time.Second,
|
IdleConnTimeout: 90 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
var tlsAuth, tlsAuthWithCACert bool
|
if tlsClientAuth || tlsAuthWithCACert {
|
||||||
if ds.JsonData != nil {
|
|
||||||
tlsAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
|
|
||||||
tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tlsAuth {
|
|
||||||
transport.TLSClientConfig.InsecureSkipVerify = false
|
|
||||||
|
|
||||||
decrypted := ds.SecureJsonData.Decrypt()
|
decrypted := ds.SecureJsonData.Decrypt()
|
||||||
|
|
||||||
if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 {
|
if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 {
|
||||||
caPool := x509.NewCertPool()
|
caPool := x509.NewCertPool()
|
||||||
ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"]))
|
ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"]))
|
||||||
if ok {
|
if !ok {
|
||||||
transport.TLSClientConfig.RootCAs = caPool
|
return nil, errors.New("Failed to parse TLS CA PEM certificate")
|
||||||
}
|
}
|
||||||
|
transport.TLSClientConfig.RootCAs = caPool
|
||||||
}
|
}
|
||||||
|
|
||||||
cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"]))
|
if tlsClientAuth {
|
||||||
if err != nil {
|
cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"]))
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
|
||||||
}
|
}
|
||||||
transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ptc.cache[ds.Id] = cachedTransport{
|
ptc.cache[ds.Id] = cachedTransport{
|
||||||
|
@ -29,61 +29,140 @@ func TestDataSourceCache(t *testing.T) {
|
|||||||
Convey("Should be using the cached proxy", func() {
|
Convey("Should be using the cached proxy", func() {
|
||||||
So(t2, ShouldEqual, t1)
|
So(t2, ShouldEqual, t1)
|
||||||
})
|
})
|
||||||
|
Convey("Should verify TLS by default", func() {
|
||||||
|
So(t1.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
|
||||||
|
})
|
||||||
|
Convey("Should have no TLS client certificate configured", func() {
|
||||||
|
So(len(t1.TLSClientConfig.Certificates), ShouldEqual, 0)
|
||||||
|
})
|
||||||
|
Convey("Should have no user-supplied TLS CA onfigured", func() {
|
||||||
|
So(t1.TLSClientConfig.RootCAs, ShouldBeNil)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("When getting kubernetes datasource proxy", t, func() {
|
Convey("When caching a datasource proxy then updating it", t, func() {
|
||||||
|
clearCache()
|
||||||
|
setting.SecretKey = "password"
|
||||||
|
|
||||||
|
json := simplejson.New()
|
||||||
|
json.Set("tlsAuthWithCACert", true)
|
||||||
|
|
||||||
|
tlsCaCert, err := util.Encrypt([]byte(caCert), "password")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
ds := DataSource{
|
||||||
|
Id: 1,
|
||||||
|
Url: "http://k8s:8001",
|
||||||
|
Type: "Kubernetes",
|
||||||
|
SecureJsonData: map[string][]byte{"tlsCACert": tlsCaCert},
|
||||||
|
Updated: time.Now().Add(-2 * time.Minute),
|
||||||
|
}
|
||||||
|
|
||||||
|
t1, err := ds.GetHttpTransport()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
Convey("Should verify TLS by default", func() {
|
||||||
|
So(t1.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
|
||||||
|
})
|
||||||
|
Convey("Should have no TLS client certificate configured", func() {
|
||||||
|
So(len(t1.TLSClientConfig.Certificates), ShouldEqual, 0)
|
||||||
|
})
|
||||||
|
Convey("Should have no user-supplied TLS CA configured", func() {
|
||||||
|
So(t1.TLSClientConfig.RootCAs, ShouldBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
ds.JsonData = nil
|
||||||
|
ds.SecureJsonData = map[string][]byte{}
|
||||||
|
ds.Updated = time.Now()
|
||||||
|
|
||||||
|
t2, err := ds.GetHttpTransport()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
Convey("Should have no user-supplied TLS CA configured after the update", func() {
|
||||||
|
So(t2.TLSClientConfig.RootCAs, ShouldBeNil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When caching a datasource proxy with TLS client authentication enabled", t, func() {
|
||||||
clearCache()
|
clearCache()
|
||||||
setting.SecretKey = "password"
|
setting.SecretKey = "password"
|
||||||
|
|
||||||
json := simplejson.New()
|
json := simplejson.New()
|
||||||
json.Set("tlsAuth", true)
|
json.Set("tlsAuth", true)
|
||||||
|
|
||||||
|
tlsClientCert, err := util.Encrypt([]byte(clientCert), "password")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
tlsClientKey, err := util.Encrypt([]byte(clientKey), "password")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
ds := DataSource{
|
||||||
|
Id: 1,
|
||||||
|
Url: "http://k8s:8001",
|
||||||
|
Type: "Kubernetes",
|
||||||
|
JsonData: json,
|
||||||
|
SecureJsonData: map[string][]byte{
|
||||||
|
"tlsClientCert": tlsClientCert,
|
||||||
|
"tlsClientKey": tlsClientKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tr, err := ds.GetHttpTransport()
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
Convey("Should verify TLS by default", func() {
|
||||||
|
So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
|
||||||
|
})
|
||||||
|
Convey("Should have a TLS client certificate configured", func() {
|
||||||
|
So(len(tr.TLSClientConfig.Certificates), ShouldEqual, 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When caching a datasource proxy with a user-supplied TLS CA", t, func() {
|
||||||
|
clearCache()
|
||||||
|
setting.SecretKey = "password"
|
||||||
|
|
||||||
|
json := simplejson.New()
|
||||||
json.Set("tlsAuthWithCACert", true)
|
json.Set("tlsAuthWithCACert", true)
|
||||||
|
|
||||||
t := time.Now()
|
tlsCaCert, err := util.Encrypt([]byte(caCert), "password")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
ds := DataSource{
|
ds := DataSource{
|
||||||
Url: "http://k8s:8001",
|
Id: 1,
|
||||||
Type: "Kubernetes",
|
Url: "http://k8s:8001",
|
||||||
Updated: t.Add(-2 * time.Minute),
|
Type: "Kubernetes",
|
||||||
|
JsonData: json,
|
||||||
|
SecureJsonData: map[string][]byte{"tlsCACert": tlsCaCert},
|
||||||
}
|
}
|
||||||
|
|
||||||
transport, err := ds.GetHttpTransport()
|
tr, err := ds.GetHttpTransport()
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
Convey("Should have no cert", func() {
|
Convey("Should verify TLS by default", func() {
|
||||||
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
|
So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
|
||||||
})
|
})
|
||||||
|
Convey("Should have a TLS CA configured", func() {
|
||||||
|
So(len(tr.TLSClientConfig.RootCAs.Subjects()), ShouldEqual, 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
ds.JsonData = json
|
Convey("When caching a datasource proxy when user skips TLS verification", t, func() {
|
||||||
|
clearCache()
|
||||||
|
|
||||||
tlsCaCert, _ := util.Encrypt([]byte(caCert), "password")
|
json := simplejson.New()
|
||||||
tlsClientCert, _ := util.Encrypt([]byte(clientCert), "password")
|
json.Set("tlsSkipVerify", true)
|
||||||
tlsClientKey, _ := util.Encrypt([]byte(clientKey), "password")
|
|
||||||
|
|
||||||
ds.SecureJsonData = map[string][]byte{
|
ds := DataSource{
|
||||||
"tlsCACert": tlsCaCert,
|
Id: 1,
|
||||||
"tlsClientCert": tlsClientCert,
|
Url: "http://k8s:8001",
|
||||||
"tlsClientKey": tlsClientKey,
|
Type: "Kubernetes",
|
||||||
|
JsonData: json,
|
||||||
}
|
}
|
||||||
ds.Updated = t.Add(-1 * time.Minute)
|
|
||||||
|
|
||||||
transport, err = ds.GetHttpTransport()
|
tr, err := ds.GetHttpTransport()
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
Convey("Should add cert", func() {
|
Convey("Should skip TLS verification", func() {
|
||||||
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
|
So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
|
||||||
So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
ds.JsonData = nil
|
|
||||||
ds.SecureJsonData = map[string][]byte{}
|
|
||||||
ds.Updated = t
|
|
||||||
|
|
||||||
transport, err = ds.GetHttpTransport()
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
Convey("Should remove cert", func() {
|
|
||||||
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
|
|
||||||
So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 0)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -115,7 +194,8 @@ FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n
|
|||||||
3lb92xM=
|
3lb92xM=
|
||||||
-----END CERTIFICATE-----`
|
-----END CERTIFICATE-----`
|
||||||
|
|
||||||
const clientCert string = `-----BEGIN CERTIFICATE-----
|
const clientCert string = `
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj
|
MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj
|
||||||
YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w
|
YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w
|
||||||
GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD
|
GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||||
|
120
pkg/services/alerting/notifiers/kafka.go
Normal file
120
pkg/services/alerting/notifiers/kafka.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package notifiers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||||
|
Type: "kafka",
|
||||||
|
Name: "Kafka REST Proxy",
|
||||||
|
Description: "Sends notifications to Kafka Rest Proxy",
|
||||||
|
Factory: NewKafkaNotifier,
|
||||||
|
OptionsTemplate: `
|
||||||
|
<h3 class="page-heading">Kafka settings</h3>
|
||||||
|
<div class="gf-form">
|
||||||
|
<span class="gf-form-label width-14">Kafka REST Proxy</span>
|
||||||
|
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.kafkaRestProxy" placeholder="http://localhost:8082"></input>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<span class="gf-form-label width-14">Topic</span>
|
||||||
|
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.kafkaTopic" placeholder="topic1"></input>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKafkaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||||
|
endpoint := model.Settings.Get("kafkaRestProxy").MustString()
|
||||||
|
if endpoint == "" {
|
||||||
|
return nil, alerting.ValidationError{Reason: "Could not find kafka rest proxy endpoint property in settings"}
|
||||||
|
}
|
||||||
|
topic := model.Settings.Get("kafkaTopic").MustString()
|
||||||
|
if topic == "" {
|
||||||
|
return nil, alerting.ValidationError{Reason: "Could not find kafka topic property in settings"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &KafkaNotifier{
|
||||||
|
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||||
|
Endpoint: endpoint,
|
||||||
|
Topic: topic,
|
||||||
|
log: log.New("alerting.notifier.kafka"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type KafkaNotifier struct {
|
||||||
|
NotifierBase
|
||||||
|
Endpoint string
|
||||||
|
Topic string
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||||
|
|
||||||
|
state := evalContext.Rule.State
|
||||||
|
|
||||||
|
customData := "Triggered metrics:\n\n"
|
||||||
|
for _, evt := range evalContext.EvalMatches {
|
||||||
|
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log.Info("Notifying Kafka", "alert_state", state)
|
||||||
|
|
||||||
|
recordJSON := simplejson.New()
|
||||||
|
records := make([]interface{}, 1)
|
||||||
|
|
||||||
|
bodyJSON := simplejson.New()
|
||||||
|
bodyJSON.Set("description", evalContext.Rule.Name+" - "+evalContext.Rule.Message)
|
||||||
|
bodyJSON.Set("client", "Grafana")
|
||||||
|
bodyJSON.Set("details", customData)
|
||||||
|
bodyJSON.Set("incident_key", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
|
||||||
|
|
||||||
|
ruleUrl, err := evalContext.GetRuleUrl()
|
||||||
|
if err != nil {
|
||||||
|
this.log.Error("Failed get rule link", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bodyJSON.Set("client_url", ruleUrl)
|
||||||
|
|
||||||
|
if evalContext.ImagePublicUrl != "" {
|
||||||
|
contexts := make([]interface{}, 1)
|
||||||
|
imageJSON := simplejson.New()
|
||||||
|
imageJSON.Set("type", "image")
|
||||||
|
imageJSON.Set("src", evalContext.ImagePublicUrl)
|
||||||
|
contexts[0] = imageJSON
|
||||||
|
bodyJSON.Set("contexts", contexts)
|
||||||
|
}
|
||||||
|
|
||||||
|
valueJSON := simplejson.New()
|
||||||
|
valueJSON.Set("value", bodyJSON)
|
||||||
|
records[0] = valueJSON
|
||||||
|
recordJSON.Set("records", records)
|
||||||
|
body, _ := recordJSON.MarshalJSON()
|
||||||
|
|
||||||
|
topicUrl := this.Endpoint + "/topics/" + this.Topic
|
||||||
|
|
||||||
|
cmd := &m.SendWebhookSync{
|
||||||
|
Url: topicUrl,
|
||||||
|
Body: string(body),
|
||||||
|
HttpMethod: "POST",
|
||||||
|
HttpHeader: map[string]string{
|
||||||
|
"Content-Type": "application/vnd.kafka.json.v2+json",
|
||||||
|
"Accept": "application/vnd.kafka.v2+json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||||
|
this.log.Error("Failed to send notification to Kafka", "error", err, "body", string(body))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
55
pkg/services/alerting/notifiers/kafka_test.go
Normal file
55
pkg/services/alerting/notifiers/kafka_test.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package notifiers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKafkaNotifier(t *testing.T) {
|
||||||
|
Convey("Kafka notifier tests", t, func() {
|
||||||
|
|
||||||
|
Convey("Parsing alert notification from settings", func() {
|
||||||
|
Convey("empty settings should return error", func() {
|
||||||
|
json := `{ }`
|
||||||
|
|
||||||
|
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||||
|
model := &m.AlertNotification{
|
||||||
|
Name: "kafka_testing",
|
||||||
|
Type: "kafka",
|
||||||
|
Settings: settingsJSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := NewKafkaNotifier(model)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("settings should send an event to kafka", func() {
|
||||||
|
json := `
|
||||||
|
{
|
||||||
|
"kafkaRestProxy": "http://localhost:8082",
|
||||||
|
"kafkaTopic": "topic1"
|
||||||
|
}`
|
||||||
|
|
||||||
|
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||||
|
model := &m.AlertNotification{
|
||||||
|
Name: "kafka_testing",
|
||||||
|
Type: "kafka",
|
||||||
|
Settings: settingsJSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
not, err := NewKafkaNotifier(model)
|
||||||
|
kafkaNotifier := not.(*KafkaNotifier)
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(kafkaNotifier.Name, ShouldEqual, "kafka_testing")
|
||||||
|
So(kafkaNotifier.Type, ShouldEqual, "kafka")
|
||||||
|
So(kafkaNotifier.Endpoint, ShouldEqual, "http://localhost:8082")
|
||||||
|
So(kafkaNotifier.Topic, ShouldEqual, "topic1")
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -100,13 +100,13 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
|
|||||||
sql.WriteString(")")
|
sql.WriteString(")")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sql.WriteString(" ORDER BY name ASC")
|
||||||
|
|
||||||
if query.Limit != 0 {
|
if query.Limit != 0 {
|
||||||
sql.WriteString(" LIMIT ?")
|
sql.WriteString(" LIMIT ?")
|
||||||
params = append(params, query.Limit)
|
params = append(params, query.Limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
sql.WriteString(" ORDER BY name ASC")
|
|
||||||
|
|
||||||
alerts := make([]*m.Alert, 0)
|
alerts := make([]*m.Alert, 0)
|
||||||
if err := x.Sql(sql.String(), params...).Find(&alerts); err != nil {
|
if err := x.Sql(sql.String(), params...).Find(&alerts); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -122,6 +122,9 @@ var (
|
|||||||
// Basic Auth
|
// Basic Auth
|
||||||
BasicAuthEnabled bool
|
BasicAuthEnabled bool
|
||||||
|
|
||||||
|
// Plugin settings
|
||||||
|
PluginAppsSkipVerifyTLS bool
|
||||||
|
|
||||||
// Session settings.
|
// Session settings.
|
||||||
SessionOptions session.Options
|
SessionOptions session.Options
|
||||||
|
|
||||||
@ -560,6 +563,9 @@ func NewConfigContext(args *CommandLineArgs) error {
|
|||||||
authBasic := Cfg.Section("auth.basic")
|
authBasic := Cfg.Section("auth.basic")
|
||||||
BasicAuthEnabled = authBasic.Key("enabled").MustBool(true)
|
BasicAuthEnabled = authBasic.Key("enabled").MustBool(true)
|
||||||
|
|
||||||
|
// global plugin settings
|
||||||
|
PluginAppsSkipVerifyTLS = Cfg.Section("plugins").Key("app_tls_skip_verify_insecure").MustBool(false)
|
||||||
|
|
||||||
// PhantomJS rendering
|
// PhantomJS rendering
|
||||||
ImagesDir = filepath.Join(DataPath, "png")
|
ImagesDir = filepath.Join(DataPath, "png")
|
||||||
PhantomDir = filepath.Join(HomePath, "vendor/phantomjs")
|
PhantomDir = filepath.Join(HomePath, "vendor/phantomjs")
|
||||||
|
@ -13,6 +13,7 @@ type OAuthInfo struct {
|
|||||||
TlsClientCert string
|
TlsClientCert string
|
||||||
TlsClientKey string
|
TlsClientKey string
|
||||||
TlsClientCa string
|
TlsClientCa string
|
||||||
|
TlsSkipVerify bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuther struct {
|
type OAuther struct {
|
||||||
|
@ -66,6 +66,7 @@ func NewOAuthService() {
|
|||||||
TlsClientCert: sec.Key("tls_client_cert").String(),
|
TlsClientCert: sec.Key("tls_client_cert").String(),
|
||||||
TlsClientKey: sec.Key("tls_client_key").String(),
|
TlsClientKey: sec.Key("tls_client_key").String(),
|
||||||
TlsClientCa: sec.Key("tls_client_ca").String(),
|
TlsClientCa: sec.Key("tls_client_ca").String(),
|
||||||
|
TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if !info.Enabled {
|
if !info.Enabled {
|
||||||
|
23
public/app/core/components/colorpicker/spectrum_picker.ts
Normal file
23
public/app/core/components/colorpicker/spectrum_picker.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Wrapper for the new ngReact <color-picker> directive for backward compatibility.
|
||||||
|
* Allows remaining <spectrum-picker> untouched in outdated plugins.
|
||||||
|
* Technically, it's just a wrapper for react component with two-way data binding support.
|
||||||
|
*/
|
||||||
|
import coreModule from '../../core_module';
|
||||||
|
|
||||||
|
export function spectrumPicker() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
require: 'ngModel',
|
||||||
|
scope: true,
|
||||||
|
replace: true,
|
||||||
|
template: '<color-picker color="ngModel.$viewValue" onChange="onColorChange"></color-picker>',
|
||||||
|
link: function(scope, element, attrs, ngModel) {
|
||||||
|
scope.ngModel = ngModel;
|
||||||
|
scope.onColorChange = (color) => {
|
||||||
|
ngModel.$setViewValue(color);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
coreModule.directive('spectrumPicker', spectrumPicker);
|
@ -1,19 +1,21 @@
|
|||||||
define([
|
define([
|
||||||
'angular',
|
'angular',
|
||||||
|
'app/core/config',
|
||||||
'../core_module',
|
'../core_module',
|
||||||
],
|
],
|
||||||
function (angular, coreModule) {
|
function (angular, config, coreModule) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
coreModule.default.controller('ErrorCtrl', function($scope, contextSrv, navModelSrv) {
|
coreModule.default.controller('ErrorCtrl', function($scope, contextSrv, navModelSrv) {
|
||||||
|
|
||||||
$scope.navModel = navModelSrv.getNotFoundNav();
|
$scope.navModel = navModelSrv.getNotFoundNav();
|
||||||
|
$scope.appSubUrl = config.appSubUrl;
|
||||||
|
|
||||||
var showSideMenu = contextSrv.sidemenu;
|
var showSideMenu = contextSrv.sidemenu;
|
||||||
contextSrv.sidemenu = false;
|
contextSrv.sidemenu = false;
|
||||||
|
|
||||||
$scope.$on('$destroy', function() {
|
$scope.$on('$destroy', function() {
|
||||||
$scope.contextSrv.sidemenu = showSideMenu;
|
contextSrv.sidemenu = showSideMenu;
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -17,6 +17,7 @@ import './components/code_editor/code_editor';
|
|||||||
import './utils/outline';
|
import './utils/outline';
|
||||||
import './components/colorpicker/ColorPicker';
|
import './components/colorpicker/ColorPicker';
|
||||||
import './components/colorpicker/SeriesColorPicker';
|
import './components/colorpicker/SeriesColorPicker';
|
||||||
|
import './components/colorpicker/spectrum_picker';
|
||||||
|
|
||||||
import {grafanaAppDirective} from './components/grafana_app';
|
import {grafanaAppDirective} from './components/grafana_app';
|
||||||
import {sideMenuDirective} from './components/sidemenu/sidemenu';
|
import {sideMenuDirective} from './components/sidemenu/sidemenu';
|
||||||
|
@ -94,6 +94,7 @@ export class AlertTabCtrl {
|
|||||||
case "opsgenie": return "fa fa-bell";
|
case "opsgenie": return "fa fa-bell";
|
||||||
case "hipchat": return "fa fa-mail-forward";
|
case "hipchat": return "fa fa-mail-forward";
|
||||||
case "pushover": return "fa fa-mobile";
|
case "pushover": return "fa fa-mobile";
|
||||||
|
case "kafka": return "fa fa-random";
|
||||||
}
|
}
|
||||||
return 'fa fa-bell';
|
return 'fa fa-bell';
|
||||||
}
|
}
|
||||||
|
@ -39,13 +39,11 @@
|
|||||||
|
|
||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form width-6">
|
<div class="gf-form width-8">
|
||||||
<button type="submit" ng-click="ctrl.save()" class="btn btn-success">Save</button>
|
<button type="submit" ng-click="ctrl.save()" class="btn btn-success width-7">Save</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form width-20">
|
<div class="gf-form width-8">
|
||||||
<div class="gf-form">
|
<button type="submit" ng-click="ctrl.testNotification()" class="btn btn-secondary width-7">Send Test</button>
|
||||||
<button type="submit" ng-click="ctrl.testNotification()" class="btn btn-secondary">Send Test</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,7 +66,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv,
|
|||||||
tooltip += '<div class="graph-annotation__body">';
|
tooltip += '<div class="graph-annotation__body">';
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
tooltip += '<div>' + sanitizeString(text).replace(/\n/g, '<br>') + '</div>';
|
tooltip += '<div>' + sanitizeString(text.replace(/\n/g, '<br>')) + '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
var tags = event.tags;
|
var tags = event.tags;
|
||||||
|
@ -18,7 +18,7 @@ const template = `
|
|||||||
<div class="modal-content text-center">
|
<div class="modal-content text-center">
|
||||||
|
|
||||||
<div class="confirm-modal-text">
|
<div class="confirm-modal-text">
|
||||||
Do you want to save you changes?
|
Do you want to save your changes?
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="confirm-modal-buttons">
|
<div class="confirm-modal-buttons">
|
||||||
|
@ -81,11 +81,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-if="ctrl.tabIndex === 1" class="tab-content">
|
|
||||||
<dashboard-import-list plugin="ctrl.datasourceMeta" datasource="ctrl.current"></dashboard-import-list>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<h3 class="page-heading">Http settings</h3>
|
<h3 class="page-heading">HTTP settings</h3>
|
||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form max-width-30">
|
<div class="gf-form max-width-30">
|
||||||
<span class="gf-form-label width-7">Url</span>
|
<span class="gf-form-label width-7">URL</span>
|
||||||
<input class="gf-form-input" type="text"
|
<input class="gf-form-input" type="text"
|
||||||
ng-model='current.url' placeholder="{{suggestUrl}}"
|
ng-model='current.url' placeholder="{{suggestUrl}}"
|
||||||
bs-typeahead="getSuggestUrls" min-length="0"
|
bs-typeahead="getSuggestUrls" min-length="0"
|
||||||
ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input>
|
ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input>
|
||||||
<info-popover mode="right-absolute">
|
<info-popover mode="right-absolute">
|
||||||
<p>Specify a complete HTTP url (for example http://your_server:8080)</p>
|
<p>Specify a complete HTTP URL (for example http://your_server:8080)</p>
|
||||||
<span ng-show="current.access === 'direct'">
|
<span ng-show="current.access === 'direct'">
|
||||||
Your access method is <em>Direct</em>, this means the url
|
Your access method is <em>Direct</em>, this means the URL
|
||||||
needs to be accessible from the browser.
|
needs to be accessible from the browser.
|
||||||
</span>
|
</span>
|
||||||
<span ng-show="current.access === 'proxy'">
|
<span ng-show="current.access === 'proxy'">
|
||||||
Your access method is currently <em>Proxy</em>, this means the url
|
Your access method is currently <em>Proxy</em>, this means the URL
|
||||||
needs to be accessible from the grafana backend.
|
needs to be accessible from the grafana backend.
|
||||||
</span>
|
</span>
|
||||||
</info-popover>
|
</info-popover>
|
||||||
@ -30,7 +30,7 @@
|
|||||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--has-help-icon max-width-24">
|
<div class="gf-form-select-wrapper gf-form-select-wrapper--has-help-icon max-width-24">
|
||||||
<select class="gf-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
|
<select class="gf-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
|
||||||
<info-popover mode="right-absolute">
|
<info-popover mode="right-absolute">
|
||||||
Direct = url is used directly from browser<br>
|
Direct = URL is used directly from browser<br>
|
||||||
Proxy = Grafana backend will proxy the request
|
Proxy = Grafana backend will proxy the request
|
||||||
</info-popover>
|
</info-popover>
|
||||||
</div>
|
</div>
|
||||||
@ -38,27 +38,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="page-heading">Http Auth</h3>
|
<h3 class="page-heading">HTTP Auth</h3>
|
||||||
|
<div class="gf-form-group">
|
||||||
|
<div class="gf-form-inline">
|
||||||
|
<gf-form-switch class="gf-form" label="Basic Auth" checked="current.basicAuth" label-class="width-8" switch-class="max-width-6"></gf-form-switch>
|
||||||
|
<gf-form-switch class="gf-form" label="With Credentials" tooltip="Whether credentials such as cookies or auth headers should be sent with cross-site requests." checked="current.withCredentials" label-class="width-11" switch-class="max-width-6"></gf-form-switch>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form-inline">
|
||||||
|
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="TLS Client Auth" label-class="width-8" checked="current.jsonData.tlsAuth" switch-class="max-width-6"></gf-form-switch>
|
||||||
|
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="With CA Cert" tooltip="Needed for verifing self-signed TLS Certs" checked="current.jsonData.tlsAuthWithCACert" label-class="width-11" switch-class="max-width-6"></gf-form-switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<gf-form-switch class="gf-form"
|
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="Skip TLS Verification (Insecure)" label-class="width-16" checked="current.jsonData.tlsSkipVerify" switch-class="max-width-6"></gf-form-switch>
|
||||||
label="Basic Auth"
|
</div>
|
||||||
checked="current.basicAuth" label-class="width-8" switch-class="max-width-6">
|
|
||||||
</gf-form-switch>
|
|
||||||
<gf-form-switch class="gf-form"
|
|
||||||
label="With Credentials" tooltip="Whether credentials such as cookies or auth headers should be sent with cross-site requests."
|
|
||||||
checked="current.withCredentials" label-class="width-11" switch-class="max-width-6">
|
|
||||||
</gf-form-switch>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form-inline">
|
|
||||||
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'"
|
|
||||||
label="TLS Client Auth" label-class="width-8"
|
|
||||||
checked="current.jsonData.tlsAuth" switch-class="max-width-6">
|
|
||||||
</gf-form-switch>
|
|
||||||
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'"
|
|
||||||
label="With CA Cert" tooltip="Optional. Needed for self-signed TLS Certs."
|
|
||||||
checked="current.jsonData.tlsAuthWithCACert" label-class="width-11" switch-class="max-width-6">
|
|
||||||
</gf-form-switch>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -79,7 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-group" ng-if="current.jsonData.tlsAuth && current.access=='proxy'">
|
<div class="gf-form-group" ng-if="(current.jsonData.tlsAuth || current.jsonData.tlsAuthWithCACert) && current.access=='proxy'">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<h6>TLS Auth Details</h6>
|
<h6>TLS Auth Details</h6>
|
||||||
<info-popover mode="header">TLS Certs are encrypted and stored in the Grafana database.</info-popover>
|
<info-popover mode="header">TLS Certs are encrypted and stored in the Grafana database.</info-popover>
|
||||||
@ -90,7 +84,7 @@
|
|||||||
<label class="gf-form-label width-7">CA Cert</label>
|
<label class="gf-form-label width-7">CA Cert</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsCACert">
|
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsCACert">
|
||||||
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsCACert" placeholder="Begins with -----BEGIN CERTIFICATE-----. The CA Certificate is necessary if you are using self-signed certificates."></textarea>
|
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsCACert" placeholder="Begins with -----BEGIN CERTIFICATE-----"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsCACert">
|
<div class="gf-form" ng-if="current.secureJsonFields.tlsCACert">
|
||||||
@ -100,29 +94,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-inline">
|
<div ng-if="current.jsonData.tlsAuth">
|
||||||
<div class="gf-form gf-form--v-stretch">
|
<div class="gf-form-inline">
|
||||||
<label class="gf-form-label width-7">Client Cert</label>
|
<div class="gf-form gf-form--v-stretch">
|
||||||
|
<label class="gf-form-label width-7">Client Cert</label>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientCert">
|
||||||
|
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientCert" placeholder="Begins with -----BEGIN CERTIFICATE-----" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientCert">
|
||||||
|
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
|
||||||
|
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientCert = false">reset</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientCert">
|
|
||||||
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientCert" placeholder="Begins with -----BEGIN CERTIFICATE-----" required></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientCert">
|
|
||||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
|
|
||||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientCert = false">reset</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form gf-form--v-stretch">
|
<div class="gf-form gf-form--v-stretch">
|
||||||
<label class="gf-form-label width-7">Client Key</label>
|
<label class="gf-form-label width-7">Client Key</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientKey">
|
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientKey">
|
||||||
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientKey" placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" required></textarea>
|
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientKey" placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" required></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey">
|
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey">
|
||||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
|
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
|
||||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientKey = false">reset</a>
|
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientKey = false">reset</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,8 +42,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3>Sorry for the inconvenience</h3>
|
<h3>Sorry for the inconvenience</h3>
|
||||||
<p>Please go back to your <a href="#" class="error-link">home dashboard</a> and try again.</p>
|
<p>Please go back to your <a href="{{appSubUrl}}/" class="error-link">home dashboard</a> and try again.</p>
|
||||||
<p>If the error persists, seek help on the <a href="#" class="error-link">community site</a>.</p>
|
<p>If the error persists, seek help on the <a href="https://community.grafana.com" target="_blank" class="error-link">community site</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-6">Decimals</label>
|
<label class="gf-form-label width-6">Decimals</label>
|
||||||
<input type="number" class="gf-form-input max-width-20" placeholder="auto" bs-tooltip="'Override automatic decimal precision for y-axis'" data-placement="right" ng-model="yaxis.decimals" ng-change="ctrl.render()" ng-model-onblur>
|
<input type="number" class="gf-form-input max-width-20" placeholder="auto" empty-to-null bs-tooltip="'Override automatic decimal precision for y-axis'" data-placement="right" ng-model="yaxis.decimals" ng-change="ctrl.render()" ng-model-onblur>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
|
@ -497,8 +497,8 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
|
|||||||
show: panel.yaxes[0].show,
|
show: panel.yaxes[0].show,
|
||||||
index: 1,
|
index: 1,
|
||||||
logBase: panel.yaxes[0].logBase || 1,
|
logBase: panel.yaxes[0].logBase || 1,
|
||||||
min: panel.yaxes[0].min ? _.toNumber(panel.yaxes[0].min) : null,
|
min: parseNumber(panel.yaxes[0].min),
|
||||||
max: panel.yaxes[0].max ? _.toNumber(panel.yaxes[0].max) : null,
|
max: parseNumber(panel.yaxes[0].max),
|
||||||
tickDecimals: panel.yaxes[0].decimals
|
tickDecimals: panel.yaxes[0].decimals
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -510,9 +510,9 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
|
|||||||
secondY.show = panel.yaxes[1].show;
|
secondY.show = panel.yaxes[1].show;
|
||||||
secondY.logBase = panel.yaxes[1].logBase || 1;
|
secondY.logBase = panel.yaxes[1].logBase || 1;
|
||||||
secondY.position = 'right';
|
secondY.position = 'right';
|
||||||
secondY.min = panel.yaxes[1].min ? _.toNumber(panel.yaxes[1].min) : null;
|
secondY.min = parseNumber(panel.yaxes[1].min);
|
||||||
secondY.max = panel.yaxes[1].max ? _.toNumber(panel.yaxes[1].max) : null;
|
secondY.max = parseNumber(panel.yaxes[1].max);
|
||||||
secondY.tickDecimals = panel.yaxes[1].decimals !== null ? _.toNumber(panel.yaxes[1].decimals): null;
|
secondY.tickDecimals = panel.yaxes[1].decimals;
|
||||||
options.yaxes.push(secondY);
|
options.yaxes.push(secondY);
|
||||||
|
|
||||||
applyLogScale(options.yaxes[1], data);
|
applyLogScale(options.yaxes[1], data);
|
||||||
@ -522,6 +522,14 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
|
|||||||
configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? "percent" : panel.yaxes[0].format);
|
configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? "percent" : panel.yaxes[0].format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseNumber(value: any) {
|
||||||
|
if (value === null || typeof value === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.toNumber(value);
|
||||||
|
}
|
||||||
|
|
||||||
function applyLogScale(axis, data) {
|
function applyLogScale(axis, data) {
|
||||||
if (axis.logBase === 1) {
|
if (axis.logBase === 1) {
|
||||||
return;
|
return;
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-link {color: $yellow;}
|
.error-link {color: $orange;}
|
||||||
|
|
||||||
.error-minus {
|
.error-minus {
|
||||||
color: #7eb26d;
|
color: #7eb26d;
|
||||||
@ -57,4 +57,4 @@
|
|||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-text {margin: 0;}
|
.graph-text {margin: 0;}
|
||||||
|
@ -358,6 +358,8 @@
|
|||||||
// Remove icon clicked
|
// Remove icon clicked
|
||||||
self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
|
self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
|
||||||
self.remove($(event.target).closest('.tag').data('item'));
|
self.remove($(event.target).closest('.tag').data('item'));
|
||||||
|
// Grafana mod, if tags input used in popover the click event will bubble up and hide popover
|
||||||
|
event.stopPropagation();
|
||||||
}, self));
|
}, self));
|
||||||
|
|
||||||
// Only add existing value as tags when using strings as tags
|
// Only add existing value as tags when using strings as tags
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"no-string-throw": true,
|
"no-string-throw": true,
|
||||||
"no-unused-expression": true,
|
"no-unused-expression": true,
|
||||||
"no-duplicate-variable": true,
|
|
||||||
"no-unused-variable": true,
|
"no-unused-variable": true,
|
||||||
|
"no-duplicate-variable": true,
|
||||||
"curly": true,
|
"curly": true,
|
||||||
"class-name": true,
|
"class-name": true,
|
||||||
"semicolon": [true, "always", "ignore-bound-class-methods"],
|
"semicolon": [true, "always", "ignore-bound-class-methods"],
|
||||||
|
Reference in New Issue
Block a user