mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'develop' of github.com:grafana/grafana into develop
This commit is contained in:
commit
7e16254b1a
@ -14,7 +14,7 @@
|
||||
* **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/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)
|
||||
* **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)
|
||||
@ -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**: 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**: 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
|
||||
* **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)
|
||||
* **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)
|
||||
* **Kafka**: Add support for sending alert notifications to kafka [#7104](https://github.com/grafana/grafana/issues/7104), thx [@utkarshcmu](https://github.com/utkarshcmu)
|
||||
|
||||
## Tech
|
||||
* **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)
|
||||
|
||||
|
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`.
|
||||
|
||||
## Contribute
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
Grafana also supports the following Notification Channels:
|
||||
|
@ -13,6 +13,7 @@ Here you can find links to older versions of the documentation that might be bet
|
||||
of Grafana.
|
||||
|
||||
- [Latest](http://docs.grafana.org)
|
||||
- [Version 4.5](http://docs.grafana.org/v4.5)
|
||||
- [Version 4.4](http://docs.grafana.org/v4.4)
|
||||
- [Version 4.3](http://docs.grafana.org/v4.3)
|
||||
- [Version 4.2](http://docs.grafana.org/v4.2)
|
||||
|
@ -135,6 +135,5 @@ Name | Description
|
||||
------------ | -------------
|
||||
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.
|
||||
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).
|
||||
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
|
||||
|
||||
{{< 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.
|
||||
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
|
||||
|
@ -57,8 +57,7 @@ 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
|
||||
```
|
||||
|
@ -10,26 +10,42 @@ weight = 1
|
||||
|
||||
# Developer Guide
|
||||
|
||||
From grafana 3.0 it's very easy to develop your own plugins and share them with other grafana users.
|
||||
|
||||
There are two blog posts about authoring a plugin that might also be of interest to any plugin authors, [Timing is Everything. Writing the Clock Panel Plugin for Grafana 3.0- part 1](https://grafana.com/blog/2016/04/08/timing-is-everything.-writing-the-clock-panel-plugin-for-grafana-3.0/) and [Timing is Everything. Editor Mode in Grafana 3.0 for the Clock Panel Plugin](https://grafana.com/blog/2016/04/15/timing-is-everything.-editor-mode-in-grafana-3.0-for-the-clock-panel-plugin/).
|
||||
You can extend Grafana by writing your own plugins and then share then with other users in [our plugin repository](https://grafana.com/plugins).
|
||||
|
||||
## Short version
|
||||
|
||||
1. [Setup grafana](http://docs.grafana.org/project/building_from_source/)
|
||||
2. Clone an example plugin into ```/var/lib/grafana/plugins``` or `data/plugins` (relative to grafana git repo if you're running development version from source dir)
|
||||
3. Code away!
|
||||
3. You one of our example plugins as starting point
|
||||
|
||||
Example plugins
|
||||
|
||||
- [Typescript data source example](https://github.com/grafana/typescript-template-datasource)
|
||||
- [Simple json data source](https://github.com/grafana/simple-json-datasource)
|
||||
- [Clock panel](https://github.com/grafana/clock-panel)
|
||||
- [Pie chart panel](https://github.com/grafana/piechart-panel)
|
||||
|
||||
There are two blog posts about authoring a plugin that might also be of interest to any plugin authors.
|
||||
|
||||
- [Timing is Everything. Writing the Clock Panel Plugin for Grafana](https://grafana.com/blog/2016/04/08/timing-is-everything.-writing-the-clock-panel-plugin-for-grafana-3.0/)
|
||||
- [Timing is Everything. Editor Mode in Grafana for the Clock Panel Plugin](https://grafana.com/blog/2016/04/15/timing-is-everything.-editor-mode-in-grafana-3.0-for-the-clock-panel-plugin/).
|
||||
|
||||
## What languages?
|
||||
|
||||
Since everything turns into javascript it's up to you to choose which language you want. That said it's probably a good idea to choose es6 or typescript since we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo is you choose one of those languages.
|
||||
Since everything turns into javascript it's up to you to choose which language you want. That said it's probably a good idea to choose es6 or typescript since
|
||||
we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo is you choose one of those languages.
|
||||
|
||||
## Buildscript
|
||||
|
||||
You can use any build system you like that support systemjs. All the built content should end up in a folder named ```dist``` and committed to the repository.By committing the dist folder the person who installs your plugin does not have to run any buildscript.
|
||||
|
||||
You can use any build system you like that support systemjs. All the built content should end up in a folder named ```dist``` and committed to the repository.
|
||||
By committing the dist folder the person who installs your plugin does not have to run any buildscript.
|
||||
All our example plugins have build scripted configured.
|
||||
|
||||
## Keep your plugin up to date
|
||||
|
||||
New versions of Grafana can sometimes cause plugins to break. Checkout our [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) doc for changes in
|
||||
Grafana that can impact your plugin.
|
||||
|
||||
## Metadata
|
||||
|
||||
See the [coding styleguide]({{< relref "code-styleguide.md" >}}) for details on the metadata.
|
||||
|
@ -10,34 +10,37 @@ weight = 2
|
||||
|
||||
# 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
|
||||
you can get event description and event tags. The text field can include links to other systems with more detail.
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||
by holding down CTRL/CMD + mouse click. Add tags to the annotation will make it searchable from other dashboards.
|
||||
By holding down **CTRL** or **CMD** + 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
|
||||
|
||||
You can also hold down CTRL/CMD and select region to create a region annotation.
|
||||
You can also hold down **CTRL** or **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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
"company": "Grafana Labs"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "4.6.0-pre1",
|
||||
"version": "4.6.0-beta1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
|
@ -1,6 +1,10 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
@ -78,6 +82,60 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response
|
||||
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 {
|
||||
annotationId := c.ParamsInt64(":annotationId")
|
||||
|
||||
|
@ -240,16 +240,24 @@ func (hs *HttpServer) registerRoutes() {
|
||||
dashboardRoute.Get("/db/:slug", wrap(GetDashboard))
|
||||
dashboardRoute.Delete("/db/:slug", reqEditorRole, wrap(DeleteDashboard))
|
||||
|
||||
dashboardRoute.Get("/id/:dashboardId/versions", wrap(GetDashboardVersions))
|
||||
dashboardRoute.Get("/id/:dashboardId/versions/:id", wrap(GetDashboardVersion))
|
||||
dashboardRoute.Post("/id/:dashboardId/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
|
||||
|
||||
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff))
|
||||
|
||||
dashboardRoute.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
|
||||
dashboardRoute.Get("/home", wrap(GetHomeDashboard))
|
||||
dashboardRoute.Get("/tags", GetDashboardTags)
|
||||
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
|
||||
|
||||
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) {
|
||||
dashIdRoute.Get("/id/:dashboardId/versions", wrap(GetDashboardVersions))
|
||||
dashIdRoute.Get("/id/:dashboardId/versions/:id", wrap(GetDashboardVersion))
|
||||
dashIdRoute.Post("/id/:dashboardId/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
|
||||
|
||||
dashIdRoute.Group("/acl", func(aclRoute RouteRegister) {
|
||||
aclRoute.Get("/", wrap(GetDashboardAclList))
|
||||
aclRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardAcl))
|
||||
aclRoute.Delete("/:aclId", wrap(DeleteDashboardAcl))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Dashboard snapshots
|
||||
@ -304,6 +312,7 @@ func (hs *HttpServer) registerRoutes() {
|
||||
annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationById))
|
||||
annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation))
|
||||
annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion))
|
||||
annotationsRoute.Post("/graphite", bind(dtos.PostGraphiteAnnotationsCmd{}), wrap(PostGraphiteAnnotation))
|
||||
}, reqEditorRole)
|
||||
|
||||
// error test
|
||||
|
@ -6,31 +6,33 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/pluginproxy"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
var pluginProxyTransport *http.Transport
|
||||
|
||||
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 _, route := range plugin.Routes {
|
||||
url := util.JoinUrlFragments("/api/plugin-proxy/"+plugin.Id, route.Path)
|
||||
|
@ -29,3 +29,10 @@ type DeleteAnnotationsCmd struct {
|
||||
AnnotationId int64 `json:"annotationId"`
|
||||
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
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
@ -14,8 +13,7 @@ import (
|
||||
)
|
||||
|
||||
var grafanaComProxyTransport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
|
@ -11,6 +11,8 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
gocache "github.com/patrickmn/go-cache"
|
||||
@ -19,7 +21,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/live"
|
||||
httpstatic "github.com/grafana/grafana/pkg/api/static"
|
||||
"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/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
@ -153,7 +154,7 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
|
||||
|
||||
for _, route := range plugins.StaticRoutes {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -187,7 +188,9 @@ func (hs *HttpServer) metricsEndpoint(ctx *macaron.Context) {
|
||||
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) {
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
@ -16,6 +15,7 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@ -29,6 +29,7 @@ var (
|
||||
ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
|
||||
ErrUsersQuotaReached = errors.New("Users quota reached")
|
||||
ErrNoEmail = errors.New("Login provider didn't return an email address")
|
||||
oauthLogger = log.New("oauth.login")
|
||||
)
|
||||
|
||||
func GenStateString() string {
|
||||
@ -50,10 +51,11 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
error := ctx.Query("error")
|
||||
if error != "" {
|
||||
errorParam := ctx.Query("error")
|
||||
if errorParam != "" {
|
||||
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
|
||||
}
|
||||
|
||||
@ -69,8 +71,12 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// verify state string
|
||||
savedState := ctx.Session.Get(middleware.SESS_KEY_OAUTH_STATE).(string)
|
||||
savedState, ok := 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")
|
||||
if savedState != queryState {
|
||||
ctx.Handle(500, "login.OAuthLogin(state mismatch)", nil)
|
||||
@ -78,36 +84,37 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
}
|
||||
|
||||
// handle call back
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: setting.OAuthService.OAuthInfos[name].TlsSkipVerify,
|
||||
},
|
||||
}
|
||||
oauthClient := &http.Client{
|
||||
Transport: tr,
|
||||
}
|
||||
|
||||
// initialize oauth2 context
|
||||
oauthCtx := oauth2.NoContext
|
||||
if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" {
|
||||
if 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 {
|
||||
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)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatal(1, "Failed to setup TlsClientCa", "oauth provider", name, "error", err)
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
tr := &http.Transport{
|
||||
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)
|
||||
tr.TLSClientConfig.RootCAs = caCertPool
|
||||
}
|
||||
|
||||
oauthCtx := context.WithValue(context.Background(), oauth2.HTTPClient, oauthClient)
|
||||
|
||||
// get token from provider
|
||||
token, err := connect.Exchange(oauthCtx, code)
|
||||
if err != nil {
|
||||
|
@ -17,8 +17,6 @@ var version = "master"
|
||||
func main() {
|
||||
setupLogging()
|
||||
|
||||
services.Init(version)
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "Grafana cli"
|
||||
app.Usage = ""
|
||||
@ -44,12 +42,20 @@ func main() {
|
||||
Value: "",
|
||||
EnvVar: "GF_PLUGIN_URL",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "insecure",
|
||||
Usage: "Skip TLS verification (insecure)",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug, d",
|
||||
Usage: "enable debug logging",
|
||||
},
|
||||
}
|
||||
|
||||
app.Before = func(c *cli.Context) error {
|
||||
services.Init(version, c.GlobalBool("insecure"))
|
||||
return nil
|
||||
}
|
||||
app.Commands = commands.Commands
|
||||
app.CommandNotFound = cmdNotFound
|
||||
|
||||
|
@ -22,7 +22,7 @@ var (
|
||||
grafanaVersion string
|
||||
)
|
||||
|
||||
func Init(version string) {
|
||||
func Init(version string, skipTLSVerify bool) {
|
||||
grafanaVersion = version
|
||||
|
||||
tr := &http.Transport{
|
||||
@ -36,8 +36,9 @@ func Init(version string) {
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: skipTLSVerify,
|
||||
},
|
||||
}
|
||||
|
||||
HttpClient = http.Client{
|
||||
|
@ -3,6 +3,7 @@ package models
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
@ -45,9 +46,16 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
|
||||
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{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
InsecureSkipVerify: tlsSkipVerify,
|
||||
Renegotiation: tls.RenegotiateFreelyAsClient,
|
||||
},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
@ -62,30 +70,24 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
}
|
||||
|
||||
var tlsAuth, tlsAuthWithCACert bool
|
||||
if ds.JsonData != nil {
|
||||
tlsAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
|
||||
tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
|
||||
}
|
||||
|
||||
if tlsAuth {
|
||||
transport.TLSClientConfig.InsecureSkipVerify = false
|
||||
|
||||
if tlsClientAuth || tlsAuthWithCACert {
|
||||
decrypted := ds.SecureJsonData.Decrypt()
|
||||
|
||||
if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 {
|
||||
caPool := x509.NewCertPool()
|
||||
ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"]))
|
||||
if ok {
|
||||
transport.TLSClientConfig.RootCAs = caPool
|
||||
if !ok {
|
||||
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 err != nil {
|
||||
return nil, err
|
||||
if tlsClientAuth {
|
||||
cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
ptc.cache[ds.Id] = cachedTransport{
|
||||
|
@ -29,61 +29,140 @@ func TestDataSourceCache(t *testing.T) {
|
||||
Convey("Should be using the cached proxy", func() {
|
||||
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()
|
||||
setting.SecretKey = "password"
|
||||
|
||||
json := simplejson.New()
|
||||
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)
|
||||
|
||||
t := time.Now()
|
||||
tlsCaCert, err := util.Encrypt([]byte(caCert), "password")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
ds := DataSource{
|
||||
Url: "http://k8s:8001",
|
||||
Type: "Kubernetes",
|
||||
Updated: t.Add(-2 * time.Minute),
|
||||
Id: 1,
|
||||
Url: "http://k8s:8001",
|
||||
Type: "Kubernetes",
|
||||
JsonData: json,
|
||||
SecureJsonData: map[string][]byte{"tlsCACert": tlsCaCert},
|
||||
}
|
||||
|
||||
transport, err := ds.GetHttpTransport()
|
||||
tr, err := ds.GetHttpTransport()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should have no cert", func() {
|
||||
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
|
||||
Convey("Should verify TLS by default", func() {
|
||||
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")
|
||||
tlsClientCert, _ := util.Encrypt([]byte(clientCert), "password")
|
||||
tlsClientKey, _ := util.Encrypt([]byte(clientKey), "password")
|
||||
json := simplejson.New()
|
||||
json.Set("tlsSkipVerify", true)
|
||||
|
||||
ds.SecureJsonData = map[string][]byte{
|
||||
"tlsCACert": tlsCaCert,
|
||||
"tlsClientCert": tlsClientCert,
|
||||
"tlsClientKey": tlsClientKey,
|
||||
ds := DataSource{
|
||||
Id: 1,
|
||||
Url: "http://k8s:8001",
|
||||
Type: "Kubernetes",
|
||||
JsonData: json,
|
||||
}
|
||||
ds.Updated = t.Add(-1 * time.Minute)
|
||||
|
||||
transport, err = ds.GetHttpTransport()
|
||||
tr, err := ds.GetHttpTransport()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should add cert", func() {
|
||||
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
|
||||
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)
|
||||
Convey("Should skip TLS verification", func() {
|
||||
So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -115,7 +194,8 @@ FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n
|
||||
3lb92xM=
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
const clientCert string = `-----BEGIN CERTIFICATE-----
|
||||
const clientCert string = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj
|
||||
YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w
|
||||
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(" ORDER BY name ASC")
|
||||
|
||||
if query.Limit != 0 {
|
||||
sql.WriteString(" LIMIT ?")
|
||||
params = append(params, query.Limit)
|
||||
}
|
||||
|
||||
sql.WriteString(" ORDER BY name ASC")
|
||||
|
||||
alerts := make([]*m.Alert, 0)
|
||||
if err := x.Sql(sql.String(), params...).Find(&alerts); err != nil {
|
||||
return err
|
||||
|
@ -122,6 +122,9 @@ var (
|
||||
// Basic Auth
|
||||
BasicAuthEnabled bool
|
||||
|
||||
// Plugin settings
|
||||
PluginAppsSkipVerifyTLS bool
|
||||
|
||||
// Session settings.
|
||||
SessionOptions session.Options
|
||||
|
||||
@ -560,6 +563,9 @@ func NewConfigContext(args *CommandLineArgs) error {
|
||||
authBasic := Cfg.Section("auth.basic")
|
||||
BasicAuthEnabled = authBasic.Key("enabled").MustBool(true)
|
||||
|
||||
// global plugin settings
|
||||
PluginAppsSkipVerifyTLS = Cfg.Section("plugins").Key("app_tls_skip_verify_insecure").MustBool(false)
|
||||
|
||||
// PhantomJS rendering
|
||||
ImagesDir = filepath.Join(DataPath, "png")
|
||||
PhantomDir = filepath.Join(HomePath, "vendor/phantomjs")
|
||||
|
@ -13,6 +13,7 @@ type OAuthInfo struct {
|
||||
TlsClientCert string
|
||||
TlsClientKey string
|
||||
TlsClientCa string
|
||||
TlsSkipVerify bool
|
||||
}
|
||||
|
||||
type OAuther struct {
|
||||
|
@ -66,6 +66,7 @@ func NewOAuthService() {
|
||||
TlsClientCert: sec.Key("tls_client_cert").String(),
|
||||
TlsClientKey: sec.Key("tls_client_key").String(),
|
||||
TlsClientCa: sec.Key("tls_client_ca").String(),
|
||||
TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
|
||||
}
|
||||
|
||||
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);
|
7
public/app/core/constants.ts
Normal file
7
public/app/core/constants.ts
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
export const GRID_CELL_HEIGHT = 20;
|
||||
export const GRID_CELL_VMARGIN = 10;
|
||||
export const GRID_COLUMN_COUNT = 24;
|
||||
export const REPEAT_DIR_VERTICAL = 'v';
|
||||
|
||||
|
@ -1,19 +1,21 @@
|
||||
define([
|
||||
'angular',
|
||||
'app/core/config',
|
||||
'../core_module',
|
||||
],
|
||||
function (angular, coreModule) {
|
||||
function (angular, config, coreModule) {
|
||||
'use strict';
|
||||
|
||||
coreModule.default.controller('ErrorCtrl', function($scope, contextSrv, navModelSrv) {
|
||||
|
||||
$scope.navModel = navModelSrv.getNotFoundNav();
|
||||
$scope.appSubUrl = config.appSubUrl;
|
||||
|
||||
var showSideMenu = contextSrv.sidemenu;
|
||||
contextSrv.sidemenu = false;
|
||||
|
||||
$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 './components/colorpicker/ColorPicker';
|
||||
import './components/colorpicker/SeriesColorPicker';
|
||||
import './components/colorpicker/spectrum_picker';
|
||||
|
||||
import {grafanaAppDirective} from './components/grafana_app';
|
||||
import {sideMenuDirective} from './components/sidemenu/sidemenu';
|
||||
@ -50,8 +51,10 @@ import {userGroupPicker} from './components/user_group_picker';
|
||||
import {geminiScrollbar} from './components/scroll/scroll';
|
||||
import {gfPageDirective} from './components/gf_page';
|
||||
import {orgSwitcher} from './components/org_switcher';
|
||||
import {profiler} from './profiler';
|
||||
|
||||
export {
|
||||
profiler,
|
||||
arrayJoin,
|
||||
coreModule,
|
||||
grafanaAppDirective,
|
||||
|
@ -256,7 +256,7 @@ export class BackendSrv {
|
||||
gridPos: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
w: 8,
|
||||
h: 10
|
||||
}
|
||||
},
|
||||
@ -268,7 +268,7 @@ export class BackendSrv {
|
||||
gridPos: {
|
||||
x: 4,
|
||||
y: 0,
|
||||
w: 4,
|
||||
w: 8,
|
||||
h: 10
|
||||
}
|
||||
},
|
||||
@ -280,7 +280,7 @@ export class BackendSrv {
|
||||
gridPos: {
|
||||
x: 8,
|
||||
y: 0,
|
||||
w: 4,
|
||||
w: 8,
|
||||
h: 10
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +94,7 @@ export class AlertTabCtrl {
|
||||
case "opsgenie": return "fa fa-bell";
|
||||
case "hipchat": return "fa fa-mail-forward";
|
||||
case "pushover": return "fa fa-mobile";
|
||||
case "kafka": return "fa fa-random";
|
||||
}
|
||||
return 'fa fa-bell';
|
||||
}
|
||||
|
@ -39,13 +39,11 @@
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form width-6">
|
||||
<button type="submit" ng-click="ctrl.save()" class="btn btn-success">Save</button>
|
||||
<div class="gf-form width-8">
|
||||
<button type="submit" ng-click="ctrl.save()" class="btn btn-success width-7">Save</button>
|
||||
</div>
|
||||
<div class="gf-form width-20">
|
||||
<div class="gf-form">
|
||||
<button type="submit" ng-click="ctrl.testNotification()" class="btn btn-secondary">Send Test</button>
|
||||
</div>
|
||||
<div class="gf-form width-8">
|
||||
<button type="submit" ng-click="ctrl.testNotification()" class="btn btn-secondary width-7">Send Test</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -66,7 +66,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv,
|
||||
tooltip += '<div class="graph-annotation__body">';
|
||||
|
||||
if (text) {
|
||||
tooltip += '<div>' + sanitizeString(text).replace(/\n/g, '<br>') + '</div>';
|
||||
tooltip += '<div>' + sanitizeString(text.replace(/\n/g, '<br>')) + '</div>';
|
||||
}
|
||||
|
||||
var tags = event.tags;
|
||||
|
@ -1,722 +0,0 @@
|
||||
import angular from 'angular';
|
||||
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 {PanelModel} from './PanelModel';
|
||||
import sortByKeys from 'app/core/utils/sort_by_keys';
|
||||
|
||||
export const CELL_HEIGHT = 30;
|
||||
export const CELL_VMARGIN = 10;
|
||||
|
||||
export class DashboardModel {
|
||||
id: any;
|
||||
title: any;
|
||||
autoUpdate: any;
|
||||
description: any;
|
||||
tags: any;
|
||||
style: any;
|
||||
timezone: any;
|
||||
editable: any;
|
||||
graphTooltip: any;
|
||||
rows: DashboardRow[];
|
||||
time: any;
|
||||
timepicker: any;
|
||||
hideControls: any;
|
||||
templating: any;
|
||||
annotations: any;
|
||||
refresh: any;
|
||||
snapshot: any;
|
||||
schemaVersion: number;
|
||||
version: number;
|
||||
revision: number;
|
||||
links: any;
|
||||
gnetId: any;
|
||||
meta: any;
|
||||
events: any;
|
||||
editMode: boolean;
|
||||
folderId: number;
|
||||
panels: PanelModel[];
|
||||
|
||||
constructor(data, meta?) {
|
||||
if (!data) {
|
||||
data = {};
|
||||
}
|
||||
|
||||
this.events = new Emitter();
|
||||
this.id = data.id || null;
|
||||
this.revision = data.revision;
|
||||
this.title = data.title || 'No Title';
|
||||
this.autoUpdate = data.autoUpdate;
|
||||
this.description = data.description;
|
||||
this.tags = data.tags || [];
|
||||
this.style = data.style || "dark";
|
||||
this.timezone = data.timezone || '';
|
||||
this.editable = data.editable !== false;
|
||||
this.graphTooltip = data.graphTooltip || 0;
|
||||
this.hideControls = data.hideControls || false;
|
||||
this.time = data.time || { from: 'now-6h', to: 'now' };
|
||||
this.timepicker = data.timepicker || {};
|
||||
this.templating = this.ensureListExist(data.templating);
|
||||
this.annotations = this.ensureListExist(data.annotations);
|
||||
this.refresh = data.refresh;
|
||||
this.snapshot = data.snapshot;
|
||||
this.schemaVersion = data.schemaVersion || 0;
|
||||
this.version = data.version || 0;
|
||||
this.links = data.links || [];
|
||||
this.gnetId = data.gnetId || null;
|
||||
this.folderId = data.folderId || null;
|
||||
this.panels = _.map(data.panels || [], panelData => new PanelModel(panelData));
|
||||
|
||||
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 || {};
|
||||
|
||||
meta.canShare = meta.canShare !== false;
|
||||
meta.canSave = meta.canSave !== false;
|
||||
meta.canStar = meta.canStar !== false;
|
||||
meta.canEdit = meta.canEdit !== false;
|
||||
|
||||
if (!this.editable) {
|
||||
meta.canEdit = false;
|
||||
meta.canDelete = false;
|
||||
meta.canSave = false;
|
||||
}
|
||||
|
||||
this.meta = meta;
|
||||
}
|
||||
|
||||
// cleans meta data and other non peristent state
|
||||
getSaveModelClone() {
|
||||
// temp remove stuff
|
||||
var events = this.events;
|
||||
var meta = this.meta;
|
||||
var variables = this.templating.list;
|
||||
var panels = this.panels;
|
||||
|
||||
delete this.events;
|
||||
delete this.meta;
|
||||
delete this.panels;
|
||||
|
||||
// prepare save model
|
||||
this.templating.list = _.map(variables, variable => variable.getSaveModel ? variable.getSaveModel() : variable);
|
||||
this.panels = _.map(panels, panel => panel.getSaveModel());
|
||||
|
||||
// make clone
|
||||
var copy = $.extend(true, {}, this);
|
||||
// sort clone
|
||||
copy = sortByKeys(copy);
|
||||
console.log(copy.panels);
|
||||
|
||||
// restore properties
|
||||
this.events = events;
|
||||
this.meta = meta;
|
||||
this.templating.list = variables;
|
||||
this.panels = panels;
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
setViewMode(panel: PanelModel, fullscreen: boolean, isEditing: boolean) {
|
||||
this.meta.fullscreen = fullscreen;
|
||||
this.meta.isEditing = isEditing && this.meta.canEdit;
|
||||
|
||||
panel.setViewMode(fullscreen, this.meta.isEditing);
|
||||
|
||||
this.events.emit('view-mode-changed', panel);
|
||||
}
|
||||
|
||||
private ensureListExist(data) {
|
||||
if (!data) { data = {}; }
|
||||
if (!data.list) { data.list = []; }
|
||||
return data;
|
||||
}
|
||||
|
||||
getNextPanelId() {
|
||||
var j, panel, max = 0;
|
||||
for (j = 0; j < this.panels.length; j++) {
|
||||
panel = this.panels[j];
|
||||
if (panel.id > max) { max = panel.id; }
|
||||
}
|
||||
return max + 1;
|
||||
}
|
||||
|
||||
forEachPanel(callback) {
|
||||
for (let i = 0; i < this.panels.length; i++) {
|
||||
callback(this.panels[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
getPanelById(id) {
|
||||
for (let panel of this.panels) {
|
||||
if (panel.id === id) {
|
||||
return panel;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
addPanel(panel) {
|
||||
panel.id = this.getNextPanelId();
|
||||
this.panels.unshift(new PanelModel(panel));
|
||||
this.events.emit('panel-added', panel);
|
||||
}
|
||||
|
||||
removePanel(panel, ask?) {
|
||||
// confirm deletion
|
||||
if (ask !== false) {
|
||||
var text2, confirmText;
|
||||
if (panel.alert) {
|
||||
text2 = "Panel includes an alert rule, removing panel will also remove alert rule";
|
||||
confirmText = "YES";
|
||||
}
|
||||
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Remove Panel',
|
||||
text: 'Are you sure you want to remove this panel?',
|
||||
text2: text2,
|
||||
icon: 'fa-trash',
|
||||
confirmText: confirmText,
|
||||
yesText: 'Remove',
|
||||
onConfirm: () => {
|
||||
this.removePanel(panel, false);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var index = _.indexOf(this.panels, panel);
|
||||
this.panels.splice(index, 1);
|
||||
this.events.emit('panel-removed', panel);
|
||||
}
|
||||
|
||||
setPanelFocus(id) {
|
||||
this.meta.focusPanelId = id;
|
||||
}
|
||||
|
||||
updateSubmenuVisibility() {
|
||||
this.meta.submenuEnabled = (() => {
|
||||
if (this.links.length > 0) { return true; }
|
||||
|
||||
var visibleVars = _.filter(this.templating.list, variable => variable.hide !== 2);
|
||||
if (visibleVars.length > 0) { return true; }
|
||||
|
||||
var visibleAnnotations = _.filter(this.annotations.list, annotation => annotation.hide !== true);
|
||||
if (visibleAnnotations.length > 0) { return true; }
|
||||
|
||||
return false;
|
||||
})();
|
||||
}
|
||||
|
||||
getPanelInfoById(panelId) {
|
||||
var result: any = {};
|
||||
_.each(this.rows, function(row) {
|
||||
_.each(row.panels, function(panel, index) {
|
||||
if (panel.id === panelId) {
|
||||
result.panel = panel;
|
||||
result.row = row;
|
||||
result.index = index;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!result.panel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
duplicatePanel(panel, row) {
|
||||
var newPanel = angular.copy(panel);
|
||||
newPanel.id = this.getNextPanelId();
|
||||
|
||||
delete newPanel.repeat;
|
||||
delete newPanel.repeatIteration;
|
||||
delete newPanel.repeatPanelId;
|
||||
delete newPanel.scopedVars;
|
||||
if (newPanel.alert) {
|
||||
delete newPanel.thresholds;
|
||||
}
|
||||
delete newPanel.alert;
|
||||
|
||||
row.addPanel(newPanel);
|
||||
return newPanel;
|
||||
}
|
||||
|
||||
formatDate(date, format?) {
|
||||
date = moment.isMoment(date) ? date : moment(date);
|
||||
format = format || 'YYYY-MM-DD HH:mm:ss';
|
||||
let timezone = this.getTimezone();
|
||||
|
||||
return timezone === 'browser' ?
|
||||
moment(date).format(format) :
|
||||
moment.utc(date).format(format);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.events.removeAllListeners();
|
||||
for (let row of this.rows) {
|
||||
row.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
on(eventName, callback) {
|
||||
this.events.on(eventName, callback);
|
||||
}
|
||||
|
||||
off(eventName, callback?) {
|
||||
this.events.off(eventName, callback);
|
||||
}
|
||||
|
||||
cycleGraphTooltip() {
|
||||
this.graphTooltip = (this.graphTooltip + 1) % 3;
|
||||
}
|
||||
|
||||
sharedTooltipModeEnabled() {
|
||||
return this.graphTooltip > 0;
|
||||
}
|
||||
|
||||
sharedCrosshairModeOnly() {
|
||||
return this.graphTooltip === 1;
|
||||
}
|
||||
|
||||
getRelativeTime(date) {
|
||||
date = moment.isMoment(date) ? date : moment(date);
|
||||
|
||||
return this.timezone === 'browser' ?
|
||||
moment(date).fromNow() :
|
||||
moment.utc(date).fromNow();
|
||||
}
|
||||
|
||||
getNextQueryLetter(panel) {
|
||||
var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
return _.find(letters, function(refId) {
|
||||
return _.every(panel.targets, function(other) {
|
||||
return other.refId !== refId;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
isTimezoneUtc() {
|
||||
return this.getTimezone() === 'utc';
|
||||
}
|
||||
|
||||
getTimezone() {
|
||||
return this.timezone ? this.timezone : contextSrv.user.timezone;
|
||||
}
|
||||
|
||||
private updateSchema(old) {
|
||||
var i, j, k;
|
||||
var oldVersion = this.schemaVersion;
|
||||
var panelUpgrades = [];
|
||||
this.schemaVersion = 16;
|
||||
|
||||
if (oldVersion === this.schemaVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
// version 2 schema changes
|
||||
if (oldVersion < 2) {
|
||||
|
||||
if (old.services) {
|
||||
if (old.services.filter) {
|
||||
this.time = old.services.filter.time;
|
||||
this.templating.list = old.services.filter.list || [];
|
||||
}
|
||||
}
|
||||
|
||||
panelUpgrades.push(function(panel) {
|
||||
// rename panel type
|
||||
if (panel.type === 'graphite') {
|
||||
panel.type = 'graph';
|
||||
}
|
||||
|
||||
if (panel.type !== 'graph') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_.isBoolean(panel.legend)) { panel.legend = { show: panel.legend }; }
|
||||
|
||||
if (panel.grid) {
|
||||
if (panel.grid.min) {
|
||||
panel.grid.leftMin = panel.grid.min;
|
||||
delete panel.grid.min;
|
||||
}
|
||||
|
||||
if (panel.grid.max) {
|
||||
panel.grid.leftMax = panel.grid.max;
|
||||
delete panel.grid.max;
|
||||
}
|
||||
}
|
||||
|
||||
if (panel.y_format) {
|
||||
panel.y_formats[0] = panel.y_format;
|
||||
delete panel.y_format;
|
||||
}
|
||||
|
||||
if (panel.y2_format) {
|
||||
panel.y_formats[1] = panel.y2_format;
|
||||
delete panel.y2_format;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// schema version 3 changes
|
||||
if (oldVersion < 3) {
|
||||
// ensure panel ids
|
||||
var maxId = this.getNextPanelId();
|
||||
panelUpgrades.push(function(panel) {
|
||||
if (!panel.id) {
|
||||
panel.id = maxId;
|
||||
maxId += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// schema version 4 changes
|
||||
if (oldVersion < 4) {
|
||||
// move aliasYAxis changes
|
||||
panelUpgrades.push(function(panel) {
|
||||
if (panel.type !== 'graph') { return; }
|
||||
_.each(panel.aliasYAxis, function(value, key) {
|
||||
panel.seriesOverrides = [{ alias: key, yaxis: value }];
|
||||
});
|
||||
delete panel.aliasYAxis;
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 6) {
|
||||
// move pulldowns to new schema
|
||||
var annotations = _.find(old.pulldowns, { type: 'annotations' });
|
||||
|
||||
if (annotations) {
|
||||
this.annotations = {
|
||||
list: annotations.annotations || [],
|
||||
};
|
||||
}
|
||||
|
||||
// update template variables
|
||||
for (i = 0 ; i < this.templating.list.length; i++) {
|
||||
var variable = this.templating.list[i];
|
||||
if (variable.datasource === void 0) { variable.datasource = null; }
|
||||
if (variable.type === 'filter') { variable.type = 'query'; }
|
||||
if (variable.type === void 0) { variable.type = 'query'; }
|
||||
if (variable.allFormat === void 0) { variable.allFormat = 'glob'; }
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 7) {
|
||||
if (old.nav && old.nav.length) {
|
||||
this.timepicker = old.nav[0];
|
||||
}
|
||||
|
||||
// ensure query refIds
|
||||
panelUpgrades.push(function(panel) {
|
||||
_.each(panel.targets, function(target) {
|
||||
if (!target.refId) {
|
||||
target.refId = this.getNextQueryLetter(panel);
|
||||
}
|
||||
}.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 8) {
|
||||
panelUpgrades.push(function(panel) {
|
||||
_.each(panel.targets, function(target) {
|
||||
// update old influxdb query schema
|
||||
if (target.fields && target.tags && target.groupBy) {
|
||||
if (target.rawQuery) {
|
||||
delete target.fields;
|
||||
delete target.fill;
|
||||
} else {
|
||||
target.select = _.map(target.fields, function(field) {
|
||||
var parts = [];
|
||||
parts.push({type: 'field', params: [field.name]});
|
||||
parts.push({type: field.func, params: []});
|
||||
if (field.mathExpr) {
|
||||
parts.push({type: 'math', params: [field.mathExpr]});
|
||||
}
|
||||
if (field.asExpr) {
|
||||
parts.push({type: 'alias', params: [field.asExpr]});
|
||||
}
|
||||
return parts;
|
||||
});
|
||||
delete target.fields;
|
||||
_.each(target.groupBy, function(part) {
|
||||
if (part.type === 'time' && part.interval) {
|
||||
part.params = [part.interval];
|
||||
delete part.interval;
|
||||
}
|
||||
if (part.type === 'tag' && part.key) {
|
||||
part.params = [part.key];
|
||||
delete part.key;
|
||||
}
|
||||
});
|
||||
|
||||
if (target.fill) {
|
||||
target.groupBy.push({type: 'fill', params: [target.fill]});
|
||||
delete target.fill;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// schema version 9 changes
|
||||
if (oldVersion < 9) {
|
||||
// move aliasYAxis changes
|
||||
panelUpgrades.push(function(panel) {
|
||||
if (panel.type !== 'singlestat' && panel.thresholds !== "") { return; }
|
||||
|
||||
if (panel.thresholds) {
|
||||
var k = panel.thresholds.split(",");
|
||||
|
||||
if (k.length >= 3) {
|
||||
k.shift();
|
||||
panel.thresholds = k.join(",");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// schema version 10 changes
|
||||
if (oldVersion < 10) {
|
||||
// move aliasYAxis changes
|
||||
panelUpgrades.push(function(panel) {
|
||||
if (panel.type !== 'table') { return; }
|
||||
|
||||
_.each(panel.styles, function(style) {
|
||||
if (style.thresholds && style.thresholds.length >= 3) {
|
||||
var k = style.thresholds;
|
||||
k.shift();
|
||||
style.thresholds = k;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 12) {
|
||||
// update template variables
|
||||
_.each(this.templating.list, function(templateVariable) {
|
||||
if (templateVariable.refresh) { templateVariable.refresh = 1; }
|
||||
if (!templateVariable.refresh) { templateVariable.refresh = 0; }
|
||||
if (templateVariable.hideVariable) {
|
||||
templateVariable.hide = 2;
|
||||
} else if (templateVariable.hideLabel) {
|
||||
templateVariable.hide = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 12) {
|
||||
// update graph yaxes changes
|
||||
panelUpgrades.push(function(panel) {
|
||||
if (panel.type !== 'graph') { return; }
|
||||
if (!panel.grid) { return; }
|
||||
|
||||
if (!panel.yaxes) {
|
||||
panel.yaxes = [
|
||||
{
|
||||
show: panel['y-axis'],
|
||||
min: panel.grid.leftMin,
|
||||
max: panel.grid.leftMax,
|
||||
logBase: panel.grid.leftLogBase,
|
||||
format: panel.y_formats[0],
|
||||
label: panel.leftYAxisLabel,
|
||||
},
|
||||
{
|
||||
show: panel['y-axis'],
|
||||
min: panel.grid.rightMin,
|
||||
max: panel.grid.rightMax,
|
||||
logBase: panel.grid.rightLogBase,
|
||||
format: panel.y_formats[1],
|
||||
label: panel.rightYAxisLabel,
|
||||
}
|
||||
];
|
||||
|
||||
panel.xaxis = {
|
||||
show: panel['x-axis'],
|
||||
};
|
||||
|
||||
delete panel.grid.leftMin;
|
||||
delete panel.grid.leftMax;
|
||||
delete panel.grid.leftLogBase;
|
||||
delete panel.grid.rightMin;
|
||||
delete panel.grid.rightMax;
|
||||
delete panel.grid.rightLogBase;
|
||||
delete panel.y_formats;
|
||||
delete panel.leftYAxisLabel;
|
||||
delete panel.rightYAxisLabel;
|
||||
delete panel['y-axis'];
|
||||
delete panel['x-axis'];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 13) {
|
||||
// update graph yaxes changes
|
||||
panelUpgrades.push(function(panel) {
|
||||
if (panel.type !== 'graph') { return; }
|
||||
if (!panel.grid) { return; }
|
||||
|
||||
panel.thresholds = [];
|
||||
var t1: any = {}, t2: any = {};
|
||||
|
||||
if (panel.grid.threshold1 !== null) {
|
||||
t1.value = panel.grid.threshold1;
|
||||
if (panel.grid.thresholdLine) {
|
||||
t1.line = true;
|
||||
t1.lineColor = panel.grid.threshold1Color;
|
||||
t1.colorMode = 'custom';
|
||||
} else {
|
||||
t1.fill = true;
|
||||
t1.fillColor = panel.grid.threshold1Color;
|
||||
t1.colorMode = 'custom';
|
||||
}
|
||||
}
|
||||
|
||||
if (panel.grid.threshold2 !== null) {
|
||||
t2.value = panel.grid.threshold2;
|
||||
if (panel.grid.thresholdLine) {
|
||||
t2.line = true;
|
||||
t2.lineColor = panel.grid.threshold2Color;
|
||||
t2.colorMode = 'custom';
|
||||
} else {
|
||||
t2.fill = true;
|
||||
t2.fillColor = panel.grid.threshold2Color;
|
||||
t2.colorMode = 'custom';
|
||||
}
|
||||
}
|
||||
|
||||
if (_.isNumber(t1.value)) {
|
||||
if (_.isNumber(t2.value)) {
|
||||
if (t1.value > t2.value) {
|
||||
t1.op = t2.op = 'lt';
|
||||
panel.thresholds.push(t1);
|
||||
panel.thresholds.push(t2);
|
||||
} else {
|
||||
t1.op = t2.op = 'gt';
|
||||
panel.thresholds.push(t1);
|
||||
panel.thresholds.push(t2);
|
||||
}
|
||||
} else {
|
||||
t1.op = 'gt';
|
||||
panel.thresholds.push(t1);
|
||||
}
|
||||
}
|
||||
|
||||
delete panel.grid.threshold1;
|
||||
delete panel.grid.threshold1Color;
|
||||
delete panel.grid.threshold2;
|
||||
delete panel.grid.threshold2Color;
|
||||
delete panel.grid.thresholdLine;
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 14) {
|
||||
this.graphTooltip = old.sharedCrosshair ? 1 : 0;
|
||||
}
|
||||
|
||||
if (oldVersion < 16) {
|
||||
this.upgradeToGridLayout(old);
|
||||
}
|
||||
|
||||
if (panelUpgrades.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (j = 0; j < this.panels.length; j++) {
|
||||
for (k = 0; k < panelUpgrades.length; k++) {
|
||||
panelUpgrades[k].call(this, this.panels[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
upgradeToGridLayout(old) {
|
||||
let yPos = 0;
|
||||
//let rowIds = 1000;
|
||||
//
|
||||
|
||||
if (!old.rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let row of old.rows) {
|
||||
let xPos = 0;
|
||||
let height: any = row.height || 250;
|
||||
|
||||
// if (this.meta.keepRows) {
|
||||
// this.panels.push({
|
||||
// id: rowIds++,
|
||||
// type: 'row',
|
||||
// title: row.title,
|
||||
// x: 0,
|
||||
// y: yPos,
|
||||
// height: 1,
|
||||
// width: 12
|
||||
// });
|
||||
//
|
||||
// yPos += 1;
|
||||
// }
|
||||
|
||||
if (_.isString(height)) {
|
||||
height = parseInt(height.replace('px', ''), 10);
|
||||
}
|
||||
|
||||
const rowGridHeight = Math.ceil(height / CELL_HEIGHT);
|
||||
|
||||
for (let panel of row.panels) {
|
||||
// should wrap to next row?
|
||||
if (xPos + panel.span >= 12) {
|
||||
yPos += rowGridHeight;
|
||||
}
|
||||
|
||||
panel.gridPos = { x: xPos, y: yPos, w: panel.span, h: rowGridHeight };
|
||||
|
||||
delete panel.span;
|
||||
|
||||
xPos += panel.gridPos.w;
|
||||
|
||||
this.panels.push(new PanelModel(panel));
|
||||
}
|
||||
|
||||
yPos += rowGridHeight;
|
||||
}
|
||||
|
||||
console.log('panels', this.panels);
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import config from 'app/core/config';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import {PanelContainer} from './dashgrid/PanelContainer';
|
||||
import {DashboardModel} from './DashboardModel';
|
||||
import {DashboardModel} from './dashboard_model';
|
||||
|
||||
export class DashboardCtrl implements PanelContainer {
|
||||
dashboard: DashboardModel;
|
||||
@ -20,7 +20,6 @@ export class DashboardCtrl implements PanelContainer {
|
||||
private alertingSrv,
|
||||
private dashboardSrv,
|
||||
private unsavedChangesSrv,
|
||||
private dynamicDashboardSrv,
|
||||
private dashboardViewStateSrv,
|
||||
private panelLoader) {
|
||||
// temp hack due to way dashboards are loaded
|
||||
@ -57,10 +56,9 @@ export class DashboardCtrl implements PanelContainer {
|
||||
.catch(this.onInitFailed.bind(this, 'Templating init failed', false))
|
||||
// continue
|
||||
.finally(() => {
|
||||
this.dashboard = dashboard;
|
||||
|
||||
this.dynamicDashboardSrv.init(dashboard);
|
||||
this.dynamicDashboardSrv.process();
|
||||
this.dashboard = dashboard;
|
||||
this.dashboard.processRepeats();
|
||||
|
||||
this.unsavedChangesSrv.init(dashboard, this.$scope);
|
||||
|
||||
@ -97,7 +95,7 @@ export class DashboardCtrl implements PanelContainer {
|
||||
}
|
||||
|
||||
templateVariableUpdated() {
|
||||
this.dynamicDashboardSrv.process();
|
||||
this.dashboard.processRepeats();
|
||||
}
|
||||
|
||||
setWindowTitleAndTheme() {
|
||||
@ -135,8 +133,8 @@ export class DashboardCtrl implements PanelContainer {
|
||||
}
|
||||
|
||||
init(dashboard) {
|
||||
this.$scope.onAppEvent('show-json-editor', this.$scope.showJsonEditor);
|
||||
this.$scope.onAppEvent('template-variable-value-updated', this.$scope.templateVariableUpdated);
|
||||
this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this));
|
||||
this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this));
|
||||
this.setupDashboard(dashboard);
|
||||
}
|
||||
}
|
||||
|
860
public/app/features/dashboard/dashboard_model.ts
Normal file
860
public/app/features/dashboard/dashboard_model.ts
Normal file
@ -0,0 +1,860 @@
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
|
||||
import {GRID_COLUMN_COUNT, GRID_CELL_HEIGHT, REPEAT_DIR_VERTICAL} from 'app/core/constants';
|
||||
import {DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors';
|
||||
import {Emitter, contextSrv} from 'app/core/core';
|
||||
import sortByKeys from 'app/core/utils/sort_by_keys';
|
||||
|
||||
import {DashboardRow} from './row/row_model';
|
||||
import {PanelModel} from './panel_model';
|
||||
|
||||
export class DashboardModel {
|
||||
id: any;
|
||||
title: any;
|
||||
autoUpdate: any;
|
||||
description: any;
|
||||
tags: any;
|
||||
style: any;
|
||||
timezone: any;
|
||||
editable: any;
|
||||
graphTooltip: any;
|
||||
rows: DashboardRow[];
|
||||
time: any;
|
||||
timepicker: any;
|
||||
hideControls: any;
|
||||
templating: any;
|
||||
annotations: any;
|
||||
refresh: any;
|
||||
snapshot: any;
|
||||
schemaVersion: number;
|
||||
version: number;
|
||||
revision: number;
|
||||
links: any;
|
||||
gnetId: any;
|
||||
editMode: boolean;
|
||||
folderId: number;
|
||||
panels: PanelModel[];
|
||||
|
||||
// ------------------
|
||||
// not persisted
|
||||
// ------------------
|
||||
|
||||
// repeat process cycles
|
||||
iteration: number;
|
||||
meta: any;
|
||||
events: Emitter;
|
||||
|
||||
static nonPersistedProperties: {[str: string]: boolean} = {
|
||||
events: true,
|
||||
meta: true,
|
||||
panels: true, // needs special handling
|
||||
templating: true, // needs special handling
|
||||
};
|
||||
|
||||
constructor(data, meta?) {
|
||||
if (!data) {
|
||||
data = {};
|
||||
}
|
||||
|
||||
this.events = new Emitter();
|
||||
this.id = data.id || null;
|
||||
this.revision = data.revision;
|
||||
this.title = data.title || 'No Title';
|
||||
this.autoUpdate = data.autoUpdate;
|
||||
this.description = data.description;
|
||||
this.tags = data.tags || [];
|
||||
this.style = data.style || 'dark';
|
||||
this.timezone = data.timezone || '';
|
||||
this.editable = data.editable !== false;
|
||||
this.graphTooltip = data.graphTooltip || 0;
|
||||
this.hideControls = data.hideControls || false;
|
||||
this.time = data.time || {from: 'now-6h', to: 'now'};
|
||||
this.timepicker = data.timepicker || {};
|
||||
this.templating = this.ensureListExist(data.templating);
|
||||
this.annotations = this.ensureListExist(data.annotations);
|
||||
this.refresh = data.refresh;
|
||||
this.snapshot = data.snapshot;
|
||||
this.schemaVersion = data.schemaVersion || 0;
|
||||
this.version = data.version || 0;
|
||||
this.links = data.links || [];
|
||||
this.gnetId = data.gnetId || null;
|
||||
this.folderId = data.folderId || null;
|
||||
this.panels = _.map(data.panels || [], panelData => new PanelModel(panelData));
|
||||
|
||||
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 || {};
|
||||
|
||||
meta.canShare = meta.canShare !== false;
|
||||
meta.canSave = meta.canSave !== false;
|
||||
meta.canStar = meta.canStar !== false;
|
||||
meta.canEdit = meta.canEdit !== false;
|
||||
|
||||
if (!this.editable) {
|
||||
meta.canEdit = false;
|
||||
meta.canDelete = false;
|
||||
meta.canSave = false;
|
||||
}
|
||||
|
||||
this.meta = meta;
|
||||
}
|
||||
|
||||
// cleans meta data and other non peristent state
|
||||
getSaveModelClone() {
|
||||
// make clone
|
||||
var copy: any = {};
|
||||
for (var property in this) {
|
||||
if (DashboardModel.nonPersistedProperties[property] || !this.hasOwnProperty(property)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
copy[property] = _.cloneDeep(this[property]);
|
||||
}
|
||||
|
||||
// get variable save models
|
||||
copy.templating = {
|
||||
list: _.map(this.templating.list, variable => (variable.getSaveModel ? variable.getSaveModel() : variable)),
|
||||
};
|
||||
|
||||
// get panel save models
|
||||
copy.panels = _.map(this.panels, panel => panel.getSaveModel());
|
||||
|
||||
// sort by keys
|
||||
copy = sortByKeys(copy);
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
setViewMode(panel: PanelModel, fullscreen: boolean, isEditing: boolean) {
|
||||
this.meta.fullscreen = fullscreen;
|
||||
this.meta.isEditing = isEditing && this.meta.canEdit;
|
||||
|
||||
panel.setViewMode(fullscreen, this.meta.isEditing);
|
||||
|
||||
this.events.emit('view-mode-changed', panel);
|
||||
}
|
||||
|
||||
private ensureListExist(data) {
|
||||
if (!data) {
|
||||
data = {};
|
||||
}
|
||||
if (!data.list) {
|
||||
data.list = [];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
getNextPanelId() {
|
||||
let max = 0;
|
||||
|
||||
for (let panel of this.panels) {
|
||||
if (panel.id > max) {
|
||||
max = panel.id;
|
||||
}
|
||||
}
|
||||
|
||||
return max + 1;
|
||||
}
|
||||
|
||||
forEachPanel(callback) {
|
||||
for (let i = 0; i < this.panels.length; i++) {
|
||||
callback(this.panels[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
getPanelById(id) {
|
||||
for (let panel of this.panels) {
|
||||
if (panel.id === id) {
|
||||
return panel;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
addPanel(panel) {
|
||||
panel.id = this.getNextPanelId();
|
||||
|
||||
this.panels.unshift(new PanelModel(panel));
|
||||
|
||||
this.sortPanelsByGridPos();
|
||||
|
||||
this.events.emit('panel-added', panel);
|
||||
}
|
||||
|
||||
private sortPanelsByGridPos() {
|
||||
this.panels.sort(function(panelA, panelB) {
|
||||
if (panelA.gridPos.y === panelB.gridPos.y) {
|
||||
return panelA.gridPos.x - panelB.gridPos.x;
|
||||
} else {
|
||||
return panelA.gridPos.y - panelB.gridPos.y;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cleanUpRepeats() {
|
||||
this.processRepeats(true);
|
||||
}
|
||||
|
||||
processRepeats(cleanUpOnly?: boolean) {
|
||||
if (this.snapshot || this.templating.list.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.iteration = (this.iteration || new Date().getTime()) + 1;
|
||||
let panelsToRemove = [];
|
||||
|
||||
// cleanup scopedVars
|
||||
for (let panel of this.panels) {
|
||||
delete panel.scopedVars;
|
||||
}
|
||||
|
||||
for (let panel of this.panels) {
|
||||
if (panel.repeat) {
|
||||
if (!cleanUpOnly) {
|
||||
this.repeatPanel(panel);
|
||||
}
|
||||
} else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) {
|
||||
panelsToRemove.push(panel);
|
||||
}
|
||||
}
|
||||
|
||||
// remove panels
|
||||
_.pull(this.panels, ...panelsToRemove);
|
||||
|
||||
this.sortPanelsByGridPos();
|
||||
this.events.emit('repeats-processed');
|
||||
}
|
||||
|
||||
getRepeatClone(sourcePanel, index) {
|
||||
// if first clone return source
|
||||
if (index === 0) {
|
||||
return sourcePanel;
|
||||
}
|
||||
|
||||
var clone = new PanelModel(sourcePanel.getSaveModel());
|
||||
clone.id = this.getNextPanelId();
|
||||
this.panels.push(clone);
|
||||
|
||||
clone.repeatIteration = this.iteration;
|
||||
clone.repeatPanelId = sourcePanel.id;
|
||||
clone.repeat = null;
|
||||
return clone;
|
||||
}
|
||||
|
||||
repeatPanel(panel: PanelModel) {
|
||||
var variable = _.find(this.templating.list, {name: panel.repeat});
|
||||
if (!variable) {
|
||||
return;
|
||||
}
|
||||
|
||||
var selected;
|
||||
if (variable.current.text === 'All') {
|
||||
selected = variable.options.slice(1, variable.options.length);
|
||||
} else {
|
||||
selected = _.filter(variable.options, {selected: true});
|
||||
}
|
||||
|
||||
let minWidth = panel.minSpan || 6;
|
||||
let xIndex = 0;
|
||||
|
||||
for (let index = 0; index < selected.length; index++) {
|
||||
var option = selected[index];
|
||||
var copy = this.getRepeatClone(panel, index);
|
||||
|
||||
copy.scopedVars = {};
|
||||
copy.scopedVars[variable.name] = option;
|
||||
|
||||
if (panel.repeatDirection === REPEAT_DIR_VERTICAL) {
|
||||
if (index === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
copy.gridPos.y = panel.gridPos.y + panel.gridPos.h * index;
|
||||
} else {
|
||||
// set width based on how many are selected
|
||||
// assumed the repeated panels should take up full row width
|
||||
|
||||
copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selected.length, minWidth);
|
||||
copy.gridPos.x = copy.gridPos.w * xIndex;
|
||||
|
||||
// handle overflow by pushing down one row
|
||||
if (copy.gridPos.x + copy.gridPos.w > GRID_COLUMN_COUNT) {
|
||||
copy.gridPos.x = 0;
|
||||
xIndex = 0;
|
||||
} else {
|
||||
xIndex += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removePanel(panel: PanelModel) {
|
||||
var index = _.indexOf(this.panels, panel);
|
||||
this.panels.splice(index, 1);
|
||||
this.events.emit('panel-removed', panel);
|
||||
}
|
||||
|
||||
setPanelFocus(id) {
|
||||
this.meta.focusPanelId = id;
|
||||
}
|
||||
|
||||
updateSubmenuVisibility() {
|
||||
this.meta.submenuEnabled = (() => {
|
||||
if (this.links.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var visibleVars = _.filter(this.templating.list, variable => variable.hide !== 2);
|
||||
if (visibleVars.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var visibleAnnotations = _.filter(this.annotations.list, annotation => annotation.hide !== true);
|
||||
if (visibleAnnotations.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})();
|
||||
}
|
||||
|
||||
getPanelInfoById(panelId) {
|
||||
for (let i = 0; i < this.panels.length; i++) {
|
||||
if (this.panels[i].id === panelId) {
|
||||
return {
|
||||
panel: this.panels[i],
|
||||
index: i,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
duplicatePanel(panel) {
|
||||
const newPanel = panel.getSaveModel();
|
||||
newPanel.id = this.getNextPanelId();
|
||||
|
||||
delete newPanel.repeat;
|
||||
delete newPanel.repeatIteration;
|
||||
delete newPanel.repeatPanelId;
|
||||
delete newPanel.scopedVars;
|
||||
if (newPanel.alert) {
|
||||
delete newPanel.thresholds;
|
||||
}
|
||||
delete newPanel.alert;
|
||||
|
||||
// does it fit to the right?
|
||||
if (panel.gridPos.x + panel.gridPos.w * 2 <= GRID_COLUMN_COUNT) {
|
||||
newPanel.gridPos.x += panel.gridPos.w;
|
||||
} else {
|
||||
// add bellow
|
||||
newPanel.gridPos.y += panel.gridPos.h;
|
||||
}
|
||||
|
||||
this.addPanel(newPanel);
|
||||
return newPanel;
|
||||
}
|
||||
|
||||
formatDate(date, format?) {
|
||||
date = moment.isMoment(date) ? date : moment(date);
|
||||
format = format || 'YYYY-MM-DD HH:mm:ss';
|
||||
let timezone = this.getTimezone();
|
||||
|
||||
return timezone === 'browser' ? moment(date).format(format) : moment.utc(date).format(format);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.events.removeAllListeners();
|
||||
for (let row of this.rows) {
|
||||
row.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
on(eventName, callback) {
|
||||
this.events.on(eventName, callback);
|
||||
}
|
||||
|
||||
off(eventName, callback?) {
|
||||
this.events.off(eventName, callback);
|
||||
}
|
||||
|
||||
cycleGraphTooltip() {
|
||||
this.graphTooltip = (this.graphTooltip + 1) % 3;
|
||||
}
|
||||
|
||||
sharedTooltipModeEnabled() {
|
||||
return this.graphTooltip > 0;
|
||||
}
|
||||
|
||||
sharedCrosshairModeOnly() {
|
||||
return this.graphTooltip === 1;
|
||||
}
|
||||
|
||||
getRelativeTime(date) {
|
||||
date = moment.isMoment(date) ? date : moment(date);
|
||||
|
||||
return this.timezone === 'browser' ? moment(date).fromNow() : moment.utc(date).fromNow();
|
||||
}
|
||||
|
||||
getNextQueryLetter(panel) {
|
||||
var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
return _.find(letters, function(refId) {
|
||||
return _.every(panel.targets, function(other) {
|
||||
return other.refId !== refId;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
isTimezoneUtc() {
|
||||
return this.getTimezone() === 'utc';
|
||||
}
|
||||
|
||||
getTimezone() {
|
||||
return this.timezone ? this.timezone : contextSrv.user.timezone;
|
||||
}
|
||||
|
||||
private updateSchema(old) {
|
||||
var i, j, k;
|
||||
var oldVersion = this.schemaVersion;
|
||||
var panelUpgrades = [];
|
||||
this.schemaVersion = 16;
|
||||
|
||||
if (oldVersion === this.schemaVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
// version 2 schema changes
|
||||
if (oldVersion < 2) {
|
||||
if (old.services) {
|
||||
if (old.services.filter) {
|
||||
this.time = old.services.filter.time;
|
||||
this.templating.list = old.services.filter.list || [];
|
||||
}
|
||||
}
|
||||
|
||||
panelUpgrades.push(function(panel) {
|
||||
// rename panel type
|
||||
if (panel.type === 'graphite') {
|
||||
panel.type = 'graph';
|
||||
}
|
||||
|
||||
if (panel.type !== 'graph') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_.isBoolean(panel.legend)) {
|
||||
panel.legend = {show: panel.legend};
|
||||
}
|
||||
|
||||
if (panel.grid) {
|
||||
if (panel.grid.min) {
|
||||
panel.grid.leftMin = panel.grid.min;
|
||||
delete panel.grid.min;
|
||||
}
|
||||
|
||||
if (panel.grid.max) {
|
||||
panel.grid.leftMax = panel.grid.max;
|
||||
delete panel.grid.max;
|
||||
}
|
||||
}
|
||||
|
||||
if (panel.y_format) {
|
||||
panel.y_formats[0] = panel.y_format;
|
||||
delete panel.y_format;
|
||||
}
|
||||
|
||||
if (panel.y2_format) {
|
||||
panel.y_formats[1] = panel.y2_format;
|
||||
delete panel.y2_format;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// schema version 3 changes
|
||||
if (oldVersion < 3) {
|
||||
// ensure panel ids
|
||||
var maxId = this.getNextPanelId();
|
||||
panelUpgrades.push(function(panel) {
|
||||
if (!panel.id) {
|
||||
panel.id = maxId;
|
||||
maxId += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// schema version 4 changes
|
||||
if (oldVersion < 4) {
|
||||
// move aliasYAxis changes
|
||||
panelUpgrades.push(function(panel) {
|
||||
if (panel.type !== 'graph') {
|
||||
return;
|
||||
}
|
||||
_.each(panel.aliasYAxis, function(value, key) {
|
||||
panel.seriesOverrides = [{alias: key, yaxis: value}];
|
||||
});
|
||||
delete panel.aliasYAxis;
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 6) {
|
||||
// move pulldowns to new schema
|
||||
var annotations = _.find(old.pulldowns, {type: 'annotations'});
|
||||
|
||||
if (annotations) {
|
||||
this.annotations = {
|
||||
list: annotations.annotations || [],
|
||||
};
|
||||
}
|
||||
|
||||
// update template variables
|
||||
for (i = 0; i < this.templating.list.length; i++) {
|
||||
var variable = this.templating.list[i];
|
||||
if (variable.datasource === void 0) {
|
||||
variable.datasource = null;
|
||||
}
|
||||
if (variable.type === 'filter') {
|
||||
variable.type = 'query';
|
||||
}
|
||||
if (variable.type === void 0) {
|
||||
variable.type = 'query';
|
||||
}
|
||||
if (variable.allFormat === void 0) {
|
||||
variable.allFormat = 'glob';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 7) {
|
||||
if (old.nav && old.nav.length) {
|
||||
this.timepicker = old.nav[0];
|
||||
}
|
||||
|
||||
// ensure query refIds
|
||||
panelUpgrades.push(function(panel) {
|
||||
_.each(
|
||||
panel.targets,
|
||||
function(target) {
|
||||
if (!target.refId) {
|
||||
target.refId = this.getNextQueryLetter(panel);
|
||||
}
|
||||
}.bind(this),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 8) {
|
||||
panelUpgrades.push(function(panel) {
|
||||
_.each(panel.targets, function(target) {
|
||||
// update old influxdb query schema
|
||||
if (target.fields && target.tags && target.groupBy) {
|
||||
if (target.rawQuery) {
|
||||
delete target.fields;
|
||||
delete target.fill;
|
||||
} else {
|
||||
target.select = _.map(target.fields, function(field) {
|
||||
var parts = [];
|
||||
parts.push({type: 'field', params: [field.name]});
|
||||
parts.push({type: field.func, params: []});
|
||||
if (field.mathExpr) {
|
||||
parts.push({type: 'math', params: [field.mathExpr]});
|
||||
}
|
||||
if (field.asExpr) {
|
||||
parts.push({type: 'alias', params: [field.asExpr]});
|
||||
}
|
||||
return parts;
|
||||
});
|
||||
delete target.fields;
|
||||
_.each(target.groupBy, function(part) {
|
||||
if (part.type === 'time' && part.interval) {
|
||||
part.params = [part.interval];
|
||||
delete part.interval;
|
||||
}
|
||||
if (part.type === 'tag' && part.key) {
|
||||
part.params = [part.key];
|
||||
delete part.key;
|
||||
}
|
||||
});
|
||||
|
||||
if (target.fill) {
|
||||
target.groupBy.push({type: 'fill', params: [target.fill]});
|
||||
delete target.fill;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// schema version 9 changes
|
||||
if (oldVersion < 9) {
|
||||
// move aliasYAxis changes
|
||||
panelUpgrades.push(function(panel) {
|
||||
if (panel.type !== 'singlestat' && panel.thresholds !== '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (panel.thresholds) {
|
||||
var k = panel.thresholds.split(',');
|
||||
|
||||
if (k.length >= 3) {
|
||||
k.shift();
|
||||
panel.thresholds = k.join(',');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// schema version 10 changes
|
||||
if (oldVersion < 10) {
|
||||
// move aliasYAxis changes
|
||||
panelUpgrades.push(function(panel) {
|
||||
if (panel.type !== 'table') {
|
||||
return;
|
||||
}
|
||||
|
||||
_.each(panel.styles, function(style) {
|
||||
if (style.thresholds && style.thresholds.length >= 3) {
|
||||
var k = style.thresholds;
|
||||
k.shift();
|
||||
style.thresholds = k;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 12) {
|
||||
// update template variables
|
||||
_.each(this.templating.list, function(templateVariable) {
|
||||
if (templateVariable.refresh) {
|
||||
templateVariable.refresh = 1;
|
||||
}
|
||||
if (!templateVariable.refresh) {
|
||||
templateVariable.refresh = 0;
|
||||
}
|
||||
if (templateVariable.hideVariable) {
|
||||
templateVariable.hide = 2;
|
||||
} else if (templateVariable.hideLabel) {
|
||||
templateVariable.hide = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 12) {
|
||||
// update graph yaxes changes
|
||||
panelUpgrades.push(function(panel) {
|
||||
if (panel.type !== 'graph') {
|
||||
return;
|
||||
}
|
||||
if (!panel.grid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!panel.yaxes) {
|
||||
panel.yaxes = [
|
||||
{
|
||||
show: panel['y-axis'],
|
||||
min: panel.grid.leftMin,
|
||||
max: panel.grid.leftMax,
|
||||
logBase: panel.grid.leftLogBase,
|
||||
format: panel.y_formats[0],
|
||||
label: panel.leftYAxisLabel,
|
||||
},
|
||||
{
|
||||
show: panel['y-axis'],
|
||||
min: panel.grid.rightMin,
|
||||
max: panel.grid.rightMax,
|
||||
logBase: panel.grid.rightLogBase,
|
||||
format: panel.y_formats[1],
|
||||
label: panel.rightYAxisLabel,
|
||||
},
|
||||
];
|
||||
|
||||
panel.xaxis = {
|
||||
show: panel['x-axis'],
|
||||
};
|
||||
|
||||
delete panel.grid.leftMin;
|
||||
delete panel.grid.leftMax;
|
||||
delete panel.grid.leftLogBase;
|
||||
delete panel.grid.rightMin;
|
||||
delete panel.grid.rightMax;
|
||||
delete panel.grid.rightLogBase;
|
||||
delete panel.y_formats;
|
||||
delete panel.leftYAxisLabel;
|
||||
delete panel.rightYAxisLabel;
|
||||
delete panel['y-axis'];
|
||||
delete panel['x-axis'];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 13) {
|
||||
// update graph yaxes changes
|
||||
panelUpgrades.push(function(panel) {
|
||||
if (panel.type !== 'graph') {
|
||||
return;
|
||||
}
|
||||
if (!panel.grid) {
|
||||
return;
|
||||
}
|
||||
|
||||
panel.thresholds = [];
|
||||
var t1: any = {},
|
||||
t2: any = {};
|
||||
|
||||
if (panel.grid.threshold1 !== null) {
|
||||
t1.value = panel.grid.threshold1;
|
||||
if (panel.grid.thresholdLine) {
|
||||
t1.line = true;
|
||||
t1.lineColor = panel.grid.threshold1Color;
|
||||
t1.colorMode = 'custom';
|
||||
} else {
|
||||
t1.fill = true;
|
||||
t1.fillColor = panel.grid.threshold1Color;
|
||||
t1.colorMode = 'custom';
|
||||
}
|
||||
}
|
||||
|
||||
if (panel.grid.threshold2 !== null) {
|
||||
t2.value = panel.grid.threshold2;
|
||||
if (panel.grid.thresholdLine) {
|
||||
t2.line = true;
|
||||
t2.lineColor = panel.grid.threshold2Color;
|
||||
t2.colorMode = 'custom';
|
||||
} else {
|
||||
t2.fill = true;
|
||||
t2.fillColor = panel.grid.threshold2Color;
|
||||
t2.colorMode = 'custom';
|
||||
}
|
||||
}
|
||||
|
||||
if (_.isNumber(t1.value)) {
|
||||
if (_.isNumber(t2.value)) {
|
||||
if (t1.value > t2.value) {
|
||||
t1.op = t2.op = 'lt';
|
||||
panel.thresholds.push(t1);
|
||||
panel.thresholds.push(t2);
|
||||
} else {
|
||||
t1.op = t2.op = 'gt';
|
||||
panel.thresholds.push(t1);
|
||||
panel.thresholds.push(t2);
|
||||
}
|
||||
} else {
|
||||
t1.op = 'gt';
|
||||
panel.thresholds.push(t1);
|
||||
}
|
||||
}
|
||||
|
||||
delete panel.grid.threshold1;
|
||||
delete panel.grid.threshold1Color;
|
||||
delete panel.grid.threshold2;
|
||||
delete panel.grid.threshold2Color;
|
||||
delete panel.grid.thresholdLine;
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 14) {
|
||||
this.graphTooltip = old.sharedCrosshair ? 1 : 0;
|
||||
}
|
||||
|
||||
if (oldVersion < 16) {
|
||||
this.upgradeToGridLayout(old);
|
||||
}
|
||||
|
||||
if (panelUpgrades.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (j = 0; j < this.panels.length; j++) {
|
||||
for (k = 0; k < panelUpgrades.length; k++) {
|
||||
panelUpgrades[k].call(this, this.panels[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
upgradeToGridLayout(old) {
|
||||
let yPos = 0;
|
||||
let widthFactor = GRID_COLUMN_COUNT / 12;
|
||||
//let rowIds = 1000;
|
||||
//
|
||||
|
||||
if (!old.rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let row of old.rows) {
|
||||
let xPos = 0;
|
||||
let height: any = row.height || 250;
|
||||
|
||||
// if (this.meta.keepRows) {
|
||||
// this.panels.push({
|
||||
// id: rowIds++,
|
||||
// type: 'row',
|
||||
// title: row.title,
|
||||
// x: 0,
|
||||
// y: yPos,
|
||||
// height: 1,
|
||||
// width: 12
|
||||
// });
|
||||
//
|
||||
// yPos += 1;
|
||||
// }
|
||||
|
||||
if (_.isString(height)) {
|
||||
height = parseInt(height.replace('px', ''), 10);
|
||||
}
|
||||
|
||||
const rowGridHeight = Math.ceil(height / GRID_CELL_HEIGHT);
|
||||
|
||||
for (let panel of row.panels) {
|
||||
const panelWidth = Math.floor(panel.span) * widthFactor;
|
||||
|
||||
// should wrap to next row?
|
||||
if (xPos + panelWidth >= GRID_COLUMN_COUNT) {
|
||||
yPos += rowGridHeight;
|
||||
}
|
||||
|
||||
panel.gridPos = {x: xPos, y: yPos, w: panelWidth, h: rowGridHeight};
|
||||
|
||||
delete panel.span;
|
||||
|
||||
xPos += panel.gridPos.w;
|
||||
|
||||
this.panels.push(new PanelModel(panel));
|
||||
}
|
||||
|
||||
yPos += rowGridHeight;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import {DashboardModel} from './DashboardModel';
|
||||
import {DashboardModel} from './dashboard_model';
|
||||
|
||||
export class DashboardSrv {
|
||||
dash: any;
|
||||
|
@ -1,15 +1,14 @@
|
||||
import React from 'react';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import ReactGridLayout from 'react-grid-layout';
|
||||
import {CELL_HEIGHT, CELL_VMARGIN} from '../DashboardModel';
|
||||
import {GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT} from 'app/core/constants';
|
||||
import {DashboardPanel} from './DashboardPanel';
|
||||
import {DashboardModel} from '../DashboardModel';
|
||||
import {DashboardModel} from '../dashboard_model';
|
||||
import {PanelContainer} from './PanelContainer';
|
||||
import {PanelModel} from '../PanelModel';
|
||||
import {PanelModel} from '../panel_model';
|
||||
import classNames from 'classnames';
|
||||
import sizeMe from 'react-sizeme';
|
||||
|
||||
const COLUMN_COUNT = 12;
|
||||
let lastGridWidth = 1200;
|
||||
|
||||
function GridWrapper({size, layout, onLayoutChange, children, onResize, onResizeStop, onWidthChange}) {
|
||||
@ -31,10 +30,10 @@ function GridWrapper({size, layout, onLayoutChange, children, onResize, onResize
|
||||
isResizable={true}
|
||||
measureBeforeMount={false}
|
||||
containerPadding={[0, 0]}
|
||||
useCSSTransforms={true}
|
||||
margin={[CELL_VMARGIN, CELL_VMARGIN]}
|
||||
cols={COLUMN_COUNT}
|
||||
rowHeight={CELL_HEIGHT}
|
||||
useCSSTransforms={false}
|
||||
margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]}
|
||||
cols={GRID_COLUMN_COUNT}
|
||||
rowHeight={GRID_CELL_HEIGHT}
|
||||
draggableHandle=".grid-drag-handle"
|
||||
layout={layout}
|
||||
onResize={onResize}
|
||||
@ -68,6 +67,8 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||
// subscribe to dashboard events
|
||||
this.dashboard = this.panelContainer.getDashboard();
|
||||
this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
|
||||
this.dashboard.on('panel-removed', this.triggerForceUpdate.bind(this));
|
||||
this.dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this));
|
||||
this.dashboard.on('view-mode-changed', this.triggerForceUpdate.bind(this));
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import {PanelModel} from '../PanelModel';
|
||||
import {PanelModel} from '../panel_model';
|
||||
import {PanelContainer} from './PanelContainer';
|
||||
import {AttachedPanel} from './PanelLoader';
|
||||
|
||||
@ -31,6 +31,8 @@ export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div ref={element => this.element = element} />
|
||||
|
42
public/app/features/dashboard/dashgrid/DashboardRow.tsx
Normal file
42
public/app/features/dashboard/dashgrid/DashboardRow.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import {PanelModel} from '../panel_model';
|
||||
|
||||
export interface DashboardRowProps {
|
||||
panel: PanelModel;
|
||||
}
|
||||
|
||||
export class DashboardPanel extends React.Component<DashboardRowProps, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.toggle = this.toggle.bind(this);
|
||||
this.openSettings = this.openSettings.bind(this);
|
||||
}
|
||||
|
||||
toggle() {}
|
||||
|
||||
openSettings() {}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="dashboard-row__center">
|
||||
<div className="dashboard-row__actions-left">
|
||||
<i className="fa fa-chevron-down" />
|
||||
<i className="fa fa-chevron-right" />
|
||||
</div>
|
||||
<a className="dashboard-row__title pointer" onClick={this.toggle}>
|
||||
<span className="dashboard-row__title-text">{this.props.panel.title}</span>
|
||||
</a>
|
||||
<div className="dashboard-row__actions-right">
|
||||
<a className="pointer" onClick={this.openSettings}>
|
||||
<i className="fa fa-cog" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dashboard-row__panel_count">(0 hidden panels)</div>
|
||||
<div className="dashboard-row__drag grid-drag-handle" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import {DashboardModel} from '../DashboardModel';
|
||||
import {DashboardModel} from '../dashboard_model';
|
||||
import {PanelLoader} from './PanelLoader';
|
||||
|
||||
export interface PanelContainer {
|
||||
|
@ -4,7 +4,7 @@ import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import angular from 'angular';
|
||||
import {appEvents, NavModel} from 'app/core/core';
|
||||
import {DashboardModel} from '../DashboardModel';
|
||||
import {DashboardModel} from '../dashboard_model';
|
||||
|
||||
export class DashNavCtrl {
|
||||
dashboard: DashboardModel;
|
||||
@ -148,7 +148,7 @@ export class DashNavCtrl {
|
||||
addPanel() {
|
||||
this.dashboard.addPanel({
|
||||
type: 'graph',
|
||||
gridPos: {x: 0, y: 0, w: 6, h: 5},
|
||||
gridPos: {x: 0, y: 0, w: 12, h: 9},
|
||||
title: 'New Graph',
|
||||
});
|
||||
}
|
||||
|
@ -1,192 +0,0 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import {DashboardRow} from './row/row_model';
|
||||
|
||||
export class DynamicDashboardSrv {
|
||||
iteration: number;
|
||||
dashboard: any;
|
||||
variables: any;
|
||||
|
||||
init(dashboard) {
|
||||
this.dashboard = dashboard;
|
||||
this.variables = dashboard.templating.list;
|
||||
}
|
||||
|
||||
process(options?) {
|
||||
if (this.dashboard.snapshot || this.variables.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.iteration = (this.iteration || new Date().getTime()) + 1;
|
||||
|
||||
options = options || {};
|
||||
var cleanUpOnly = options.cleanUpOnly;
|
||||
var i, j, row, panel;
|
||||
|
||||
// cleanup scopedVars
|
||||
for (i = 0; i < this.dashboard.rows.length; i++) {
|
||||
row = this.dashboard.rows[i];
|
||||
delete row.scopedVars;
|
||||
|
||||
for (j = 0; j < row.panels.length; j++) {
|
||||
delete row.panels[j].scopedVars;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < this.dashboard.rows.length; i++) {
|
||||
row = this.dashboard.rows[i];
|
||||
|
||||
// handle row repeats
|
||||
if (row.repeat) {
|
||||
if (!cleanUpOnly) {
|
||||
this.repeatRow(row, i);
|
||||
}
|
||||
} else if (row.repeatRowId && row.repeatIteration !== this.iteration) {
|
||||
// clean up old left overs
|
||||
this.dashboard.removeRow(row, true);
|
||||
i = i - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// repeat panels
|
||||
for (j = 0; j < row.panels.length; j++) {
|
||||
panel = row.panels[j];
|
||||
if (panel.repeat) {
|
||||
if (!cleanUpOnly) {
|
||||
this.repeatPanel(panel, row);
|
||||
}
|
||||
} else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) {
|
||||
// clean up old left overs
|
||||
row.panels = _.without(row.panels, panel);
|
||||
j = j - 1;
|
||||
}
|
||||
}
|
||||
|
||||
row.panelSpanChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// returns a new row clone or reuses a clone from previous iteration
|
||||
getRowClone(sourceRow, repeatIndex, sourceRowIndex) {
|
||||
if (repeatIndex === 0) {
|
||||
return sourceRow;
|
||||
}
|
||||
|
||||
var i, panel, row, copy;
|
||||
var sourceRowId = sourceRowIndex + 1;
|
||||
|
||||
// look for row to reuse
|
||||
for (i = 0; i < this.dashboard.rows.length; i++) {
|
||||
row = this.dashboard.rows[i];
|
||||
if (row.repeatRowId === sourceRowId && row.repeatIteration !== this.iteration) {
|
||||
copy = row;
|
||||
copy.copyPropertiesFromRowSource(sourceRow);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!copy) {
|
||||
var modelCopy = angular.copy(sourceRow.getSaveModel());
|
||||
copy = new DashboardRow(modelCopy);
|
||||
this.dashboard.rows.splice(sourceRowIndex + repeatIndex, 0, copy);
|
||||
|
||||
// set new panel ids
|
||||
for (i = 0; i < copy.panels.length; i++) {
|
||||
panel = copy.panels[i];
|
||||
panel.id = this.dashboard.getNextPanelId();
|
||||
}
|
||||
}
|
||||
|
||||
copy.repeat = null;
|
||||
copy.repeatRowId = sourceRowId;
|
||||
copy.repeatIteration = this.iteration;
|
||||
return copy;
|
||||
}
|
||||
|
||||
// returns a new row clone or reuses a clone from previous iteration
|
||||
repeatRow(row, rowIndex) {
|
||||
var variable = _.find(this.variables, {name: row.repeat});
|
||||
if (!variable) {
|
||||
return;
|
||||
}
|
||||
|
||||
var selected, copy, i, panel;
|
||||
if (variable.current.text === 'All') {
|
||||
selected = variable.options.slice(1, variable.options.length);
|
||||
} else {
|
||||
selected = _.filter(variable.options, {selected: true});
|
||||
}
|
||||
|
||||
_.each(selected, (option, index) => {
|
||||
copy = this.getRowClone(row, index, rowIndex);
|
||||
copy.scopedVars = {};
|
||||
copy.scopedVars[variable.name] = option;
|
||||
|
||||
for (i = 0; i < copy.panels.length; i++) {
|
||||
panel = copy.panels[i];
|
||||
panel.scopedVars = {};
|
||||
panel.scopedVars[variable.name] = option;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getPanelClone(sourcePanel, row, index) {
|
||||
// if first clone return source
|
||||
if (index === 0) {
|
||||
return sourcePanel;
|
||||
}
|
||||
|
||||
var i, tmpId, panel, clone;
|
||||
|
||||
// first try finding an existing clone to use
|
||||
for (i = 0; i < row.panels.length; i++) {
|
||||
panel = row.panels[i];
|
||||
if (panel.repeatIteration !== this.iteration && panel.repeatPanelId === sourcePanel.id) {
|
||||
clone = panel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!clone) {
|
||||
clone = { id: this.dashboard.getNextPanelId() };
|
||||
row.panels.push(clone);
|
||||
}
|
||||
|
||||
// save id
|
||||
tmpId = clone.id;
|
||||
// copy properties from source
|
||||
angular.copy(sourcePanel, clone);
|
||||
// restore id
|
||||
clone.id = tmpId;
|
||||
clone.repeatIteration = this.iteration;
|
||||
clone.repeatPanelId = sourcePanel.id;
|
||||
clone.repeat = null;
|
||||
return clone;
|
||||
}
|
||||
|
||||
repeatPanel(panel, row) {
|
||||
var variable = _.find(this.variables, {name: panel.repeat});
|
||||
if (!variable) { return; }
|
||||
|
||||
var selected;
|
||||
if (variable.current.text === 'All') {
|
||||
selected = variable.options.slice(1, variable.options.length);
|
||||
} else {
|
||||
selected = _.filter(variable.options, {selected: true});
|
||||
}
|
||||
|
||||
_.each(selected, (option, index) => {
|
||||
var copy = this.getPanelClone(panel, row, index);
|
||||
copy.span = Math.max(12 / selected.length, panel.minSpan || 4);
|
||||
copy.scopedVars = copy.scopedVars || {};
|
||||
copy.scopedVars[variable.name] = option;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.service('dynamicDashboardSrv', DynamicDashboardSrv);
|
||||
|
@ -1,29 +1,24 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import {DynamicDashboardSrv} from '../dynamic_dashboard_srv';
|
||||
import {DashboardModel} from '../dashboard_model';
|
||||
|
||||
export class DashboardExporter {
|
||||
|
||||
constructor(private datasourceSrv) {
|
||||
}
|
||||
|
||||
makeExportable(dashboard) {
|
||||
var dynSrv = new DynamicDashboardSrv();
|
||||
|
||||
makeExportable(dashboard: DashboardModel) {
|
||||
// clean up repeated rows and panels,
|
||||
// this is done on the live real dashboard instance, not on a clone
|
||||
// so we need to undo this
|
||||
// this is pretty hacky and needs to be changed
|
||||
dynSrv.init(dashboard);
|
||||
dynSrv.process({cleanUpOnly: true});
|
||||
dashboard.cleanUpRepeats();
|
||||
|
||||
var saveModel = dashboard.getSaveModelClone();
|
||||
saveModel.id = null;
|
||||
|
||||
// undo repeat cleanup
|
||||
dynSrv.process();
|
||||
dashboard.processRepeats();
|
||||
|
||||
var inputs = [];
|
||||
var requires = {};
|
||||
@ -69,29 +64,27 @@ export class DashboardExporter {
|
||||
};
|
||||
|
||||
// check up panel data sources
|
||||
for (let row of saveModel.rows) {
|
||||
for (let panel of row.panels) {
|
||||
if (panel.datasource !== undefined) {
|
||||
templateizeDatasourceUsage(panel);
|
||||
}
|
||||
for (let panel of saveModel.panels) {
|
||||
if (panel.datasource !== undefined) {
|
||||
templateizeDatasourceUsage(panel);
|
||||
}
|
||||
|
||||
if (panel.targets) {
|
||||
for (let target of panel.targets) {
|
||||
if (target.datasource !== undefined) {
|
||||
templateizeDatasourceUsage(target);
|
||||
}
|
||||
if (panel.targets) {
|
||||
for (let target of panel.targets) {
|
||||
if (target.datasource !== undefined) {
|
||||
templateizeDatasourceUsage(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var panelDef = config.panels[panel.type];
|
||||
if (panelDef) {
|
||||
requires['panel' + panelDef.id] = {
|
||||
type: 'panel',
|
||||
id: panelDef.id,
|
||||
name: panelDef.name,
|
||||
version: panelDef.info.version,
|
||||
};
|
||||
}
|
||||
var panelDef = config.panels[panel.type];
|
||||
if (panelDef) {
|
||||
requires['panel' + panelDef.id] = {
|
||||
type: 'panel',
|
||||
id: panelDef.id,
|
||||
name: panelDef.name,
|
||||
version: panelDef.info.version,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
|
@ -6,7 +6,7 @@ import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
import moment from 'moment';
|
||||
|
||||
import {DashboardModel} from '../DashboardModel';
|
||||
import {DashboardModel} from '../dashboard_model';
|
||||
import {HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv} from './history_srv';
|
||||
|
||||
export class HistoryListCtrl {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import {DashboardModel} from '../DashboardModel';
|
||||
import {DashboardModel} from '../dashboard_model';
|
||||
|
||||
export interface HistoryListOpts {
|
||||
limit: number;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {Emitter} from 'app/core/core';
|
||||
import _ from 'lodash';
|
||||
|
||||
export interface GridPos {
|
||||
x: number;
|
||||
@ -19,6 +20,12 @@ export class PanelModel {
|
||||
type: string;
|
||||
title: string;
|
||||
alert?: any;
|
||||
scopedVars?: any;
|
||||
repeat?: string;
|
||||
repeatIteration?: number;
|
||||
repeatPanelId?: number;
|
||||
repeatDirection?: string;
|
||||
minSpan?: number;
|
||||
|
||||
// non persisted
|
||||
fullscreen: boolean;
|
||||
@ -32,6 +39,10 @@ export class PanelModel {
|
||||
for (var property in model) {
|
||||
this[property] = model[property];
|
||||
}
|
||||
|
||||
if (!this.gridPos) {
|
||||
this.gridPos = {x: 0, y: 0, h: 3, w: 6};
|
||||
}
|
||||
}
|
||||
|
||||
getSaveModel() {
|
||||
@ -41,7 +52,7 @@ export class PanelModel {
|
||||
continue;
|
||||
}
|
||||
|
||||
model[property] = this[property];
|
||||
model[property] = _.cloneDeep(this[property]);
|
||||
}
|
||||
|
||||
return model;
|
@ -1,11 +1,9 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import {coreModule} from 'app/core/core';
|
||||
|
||||
var template = `
|
||||
<div class="gf-form-select-wrapper max-width-13">
|
||||
<select class="gf-form-input" ng-model="model.repeat" ng-options="f.value as f.text for f in variables">
|
||||
<option value=""></option>
|
||||
<select class="gf-form-input" ng-model="model.repeat" ng-options="f.value as f.text for f in variables" ng-change="optionChanged()">
|
||||
<option value=""></option>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -29,6 +27,17 @@ function dashRepeatOptionDirective(variableSrv) {
|
||||
}
|
||||
|
||||
scope.variables.unshift({text: 'Disabled', value: null});
|
||||
|
||||
// if repeat is set and no direction set to horizontal
|
||||
if (scope.panel.repeat && !scope.panel.repeatDirection) {
|
||||
scope.panel.repeatDirection = 'h';
|
||||
}
|
||||
|
||||
scope.optionChanged = function() {
|
||||
if (scope.panel.repeat) {
|
||||
scope.panel.repeatDirection = 'h';
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import {describe, beforeEach, it, expect} from 'test/lib/common';
|
||||
|
||||
import _ from 'lodash';
|
||||
import {DashboardModel} from '../DashboardModel';
|
||||
import {DashboardModel} from '../dashboard_model';
|
||||
import {PanelModel} from '../panel_model';
|
||||
|
||||
describe('DashboardModel', function() {
|
||||
|
||||
@ -22,7 +23,7 @@ describe('DashboardModel', function() {
|
||||
});
|
||||
|
||||
it('should have default properties', function() {
|
||||
expect(model.rows.length).to.be(0);
|
||||
expect(model.panels.length).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -31,7 +32,7 @@ describe('DashboardModel', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
model = new DashboardModel({
|
||||
rows: [{ panels: [{ id: 5 }]}]
|
||||
panels: [{ id: 5 }]
|
||||
});
|
||||
});
|
||||
|
||||
@ -46,8 +47,8 @@ describe('DashboardModel', function() {
|
||||
var saveModel = model.getSaveModelClone();
|
||||
var keys = _.keys(saveModel);
|
||||
|
||||
expect(keys[0]).to.be('addBuiltInAnnotationQuery');
|
||||
expect(keys[1]).to.be('addEmptyRow');
|
||||
expect(keys[0]).to.be('annotations');
|
||||
expect(keys[1]).to.be('autoUpdate');
|
||||
});
|
||||
});
|
||||
|
||||
@ -58,39 +59,30 @@ describe('DashboardModel', function() {
|
||||
dashboard = new DashboardModel({});
|
||||
});
|
||||
|
||||
it('adding default should split span in half', function() {
|
||||
dashboard.addEmptyRow();
|
||||
dashboard.rows[0].addPanel({span: 12});
|
||||
dashboard.rows[0].addPanel({span: 12});
|
||||
it('adding panel should new up panel model', function() {
|
||||
dashboard.addPanel({type: 'test', title: 'test'});
|
||||
|
||||
expect(dashboard.rows[0].panels[0].span).to.be(6);
|
||||
expect(dashboard.rows[0].panels[1].span).to.be(6);
|
||||
expect(dashboard.panels[0] instanceof PanelModel).to.be(true);
|
||||
});
|
||||
|
||||
it('duplicate panel should try to add it to same row', function() {
|
||||
var panel = { span: 4, attr: '123', id: 10 };
|
||||
it('duplicate panel should try to add to the right if there is space', function() {
|
||||
var panel = {id: 10, gridPos: {x: 0, y: 0, w: 6, h: 2}};
|
||||
|
||||
dashboard.addEmptyRow();
|
||||
dashboard.rows[0].addPanel(panel);
|
||||
dashboard.duplicatePanel(panel, dashboard.rows[0]);
|
||||
dashboard.addPanel(panel);
|
||||
dashboard.duplicatePanel(dashboard.panels[0]);
|
||||
|
||||
expect(dashboard.rows[0].panels[0].span).to.be(4);
|
||||
expect(dashboard.rows[0].panels[1].span).to.be(4);
|
||||
expect(dashboard.rows[0].panels[1].attr).to.be('123');
|
||||
expect(dashboard.rows[0].panels[1].id).to.be(11);
|
||||
expect(dashboard.panels[1].gridPos).to.eql({x: 6, y: 0, h: 2, w: 6});
|
||||
});
|
||||
|
||||
it('duplicate panel should remove repeat data', function() {
|
||||
var panel = { span: 4, attr: '123', id: 10, repeat: 'asd', scopedVars: { test: 'asd' }};
|
||||
var panel = {id: 10, gridPos: {x: 0, y: 0, w: 6, h: 2}, repeat: 'asd', scopedVars: {test: 'asd'}};
|
||||
|
||||
dashboard.addEmptyRow();
|
||||
dashboard.rows[0].addPanel(panel);
|
||||
dashboard.duplicatePanel(panel, dashboard.rows[0]);
|
||||
dashboard.addPanel(panel);
|
||||
dashboard.duplicatePanel(dashboard.panels[0]);
|
||||
|
||||
expect(dashboard.rows[0].panels[1].repeat).to.be(undefined);
|
||||
expect(dashboard.rows[0].panels[1].scopedVars).to.be(undefined);
|
||||
expect(dashboard.panels[1].repeat).to.be(undefined);
|
||||
expect(dashboard.panels[1].scopedVars).to.be(undefined);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when creating dashboard with old schema', function() {
|
||||
@ -106,43 +98,39 @@ describe('DashboardModel', function() {
|
||||
{type: 'filtering', enable: true},
|
||||
{type: 'annotations', enable: true, annotations: [{name: 'old'}]}
|
||||
],
|
||||
rows: [
|
||||
panels: [
|
||||
{
|
||||
panels: [
|
||||
{
|
||||
type: 'graph', legend: true, aliasYAxis: { test: 2 },
|
||||
y_formats: ['kbyte', 'ms'],
|
||||
grid: {
|
||||
min: 1,
|
||||
max: 10,
|
||||
rightMin: 5,
|
||||
rightMax: 15,
|
||||
leftLogBase: 1,
|
||||
rightLogBase: 2,
|
||||
threshold1: 200,
|
||||
threshold2: 400,
|
||||
threshold1Color: 'yellow',
|
||||
threshold2Color: 'red',
|
||||
},
|
||||
leftYAxisLabel: 'left label',
|
||||
targets: [{refId: 'A'}, {}],
|
||||
},
|
||||
{
|
||||
type: 'singlestat', legend: true, thresholds: '10,20,30', aliasYAxis: { test: 2 }, grid: { min: 1, max: 10 },
|
||||
targets: [{refId: 'A'}, {}],
|
||||
},
|
||||
{
|
||||
type: 'table', legend: true, styles: [{ thresholds: ["10", "20", "30"]}, { thresholds: ["100", "200", "300"]}],
|
||||
targets: [{refId: 'A'}, {}],
|
||||
}
|
||||
]
|
||||
type: 'graph', legend: true, aliasYAxis: { test: 2 },
|
||||
y_formats: ['kbyte', 'ms'],
|
||||
grid: {
|
||||
min: 1,
|
||||
max: 10,
|
||||
rightMin: 5,
|
||||
rightMax: 15,
|
||||
leftLogBase: 1,
|
||||
rightLogBase: 2,
|
||||
threshold1: 200,
|
||||
threshold2: 400,
|
||||
threshold1Color: 'yellow',
|
||||
threshold2Color: 'red',
|
||||
},
|
||||
leftYAxisLabel: 'left label',
|
||||
targets: [{refId: 'A'}, {}],
|
||||
},
|
||||
{
|
||||
type: 'singlestat', legend: true, thresholds: '10,20,30', aliasYAxis: { test: 2 }, grid: { min: 1, max: 10 },
|
||||
targets: [{refId: 'A'}, {}],
|
||||
},
|
||||
{
|
||||
type: 'table', legend: true, styles: [{ thresholds: ["10", "20", "30"]}, { thresholds: ["100", "200", "300"]}],
|
||||
targets: [{refId: 'A'}, {}],
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
graph = model.rows[0].panels[0];
|
||||
singlestat = model.rows[0].panels[1];
|
||||
table = model.rows[0].panels[2];
|
||||
graph = model.panels[0];
|
||||
singlestat = model.panels[1];
|
||||
table = model.panels[2];
|
||||
});
|
||||
|
||||
it('should have title', function() {
|
||||
@ -207,7 +195,7 @@ describe('DashboardModel', function() {
|
||||
});
|
||||
|
||||
it('dashboard schema version should be set to latest', function() {
|
||||
expect(model.schemaVersion).to.be(14);
|
||||
expect(model.schemaVersion).to.be(16);
|
||||
});
|
||||
|
||||
it('graph thresholds should be migrated', function() {
|
||||
@ -244,52 +232,50 @@ describe('DashboardModel', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
model = new DashboardModel({
|
||||
rows: [{
|
||||
panels: [{
|
||||
type: 'graph',
|
||||
grid: {},
|
||||
yaxes: [{}, {}],
|
||||
targets: [{
|
||||
"alias": "$tag_datacenter $tag_source $col",
|
||||
"column": "value",
|
||||
"measurement": "logins.count",
|
||||
"fields": [
|
||||
{
|
||||
"func": "mean",
|
||||
"name": "value",
|
||||
"mathExpr": "*2",
|
||||
"asExpr": "value"
|
||||
},
|
||||
{
|
||||
"name": "one-minute",
|
||||
"func": "mean",
|
||||
"mathExpr": "*3",
|
||||
"asExpr": "one-minute"
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"fill": "previous",
|
||||
"function": "mean",
|
||||
"groupBy": [
|
||||
{
|
||||
"interval": "auto",
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"key": "source",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"type": "tag",
|
||||
"key": "datacenter"
|
||||
}
|
||||
],
|
||||
}]
|
||||
panels: [{
|
||||
type: 'graph',
|
||||
grid: {},
|
||||
yaxes: [{}, {}],
|
||||
targets: [{
|
||||
"alias": "$tag_datacenter $tag_source $col",
|
||||
"column": "value",
|
||||
"measurement": "logins.count",
|
||||
"fields": [
|
||||
{
|
||||
"func": "mean",
|
||||
"name": "value",
|
||||
"mathExpr": "*2",
|
||||
"asExpr": "value"
|
||||
},
|
||||
{
|
||||
"name": "one-minute",
|
||||
"func": "mean",
|
||||
"mathExpr": "*3",
|
||||
"asExpr": "one-minute"
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"fill": "previous",
|
||||
"function": "mean",
|
||||
"groupBy": [
|
||||
{
|
||||
"interval": "auto",
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"key": "source",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"type": "tag",
|
||||
"key": "datacenter"
|
||||
}
|
||||
],
|
||||
}]
|
||||
}]
|
||||
});
|
||||
|
||||
target = model.rows[0].panels[0].targets[0];
|
||||
target = model.panels[0].targets[0];
|
||||
});
|
||||
|
||||
it('should update query schema', function() {
|
||||
@ -414,19 +400,163 @@ describe('DashboardModel', function() {
|
||||
});
|
||||
|
||||
describe('updateSubmenuVisibility with hidden annotation toggle', function() {
|
||||
var model;
|
||||
var dashboard;
|
||||
|
||||
beforeEach(function() {
|
||||
model = new DashboardModel({
|
||||
dashboard = new DashboardModel({
|
||||
annotations: {
|
||||
list: [{hide: true}]
|
||||
}
|
||||
});
|
||||
model.updateSubmenuVisibility();
|
||||
dashboard.updateSubmenuVisibility();
|
||||
});
|
||||
|
||||
it('should not enable submmenu', function() {
|
||||
expect(model.meta.submenuEnabled).to.be(false);
|
||||
expect(dashboard.meta.submenuEnabled).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given dashboard with panel repeat in horizontal direction', function(ctx) {
|
||||
var dashboard;
|
||||
|
||||
beforeEach(function() {
|
||||
dashboard = new DashboardModel({
|
||||
panels: [{id: 2, repeat: 'apps', repeatDirection: 'h', gridPos: {x: 0, y: 0, h: 2, w: 24}}],
|
||||
templating: {
|
||||
list: [{
|
||||
name: 'apps',
|
||||
current: {
|
||||
text: 'se1, se2, se3',
|
||||
value: ['se1', 'se2', 'se3']
|
||||
},
|
||||
options: [
|
||||
{text: 'se1', value: 'se1', selected: true},
|
||||
{text: 'se2', value: 'se2', selected: true},
|
||||
{text: 'se3', value: 'se3', selected: true},
|
||||
{text: 'se4', value: 'se4', selected: false}
|
||||
]
|
||||
}]
|
||||
}
|
||||
});
|
||||
dashboard.processRepeats();
|
||||
});
|
||||
|
||||
it('should repeat panel 3 times', function() {
|
||||
expect(dashboard.panels.length).to.be(3);
|
||||
});
|
||||
|
||||
it('should mark panel repeated', function() {
|
||||
expect(dashboard.panels[0].repeat).to.be('apps');
|
||||
expect(dashboard.panels[1].repeatPanelId).to.be(2);
|
||||
});
|
||||
|
||||
it('should set scopedVars on panels', function() {
|
||||
expect(dashboard.panels[0].scopedVars.apps.value).to.be('se1');
|
||||
expect(dashboard.panels[1].scopedVars.apps.value).to.be('se2');
|
||||
expect(dashboard.panels[2].scopedVars.apps.value).to.be('se3');
|
||||
});
|
||||
|
||||
it('should place on first row and adjust width so all fit', function() {
|
||||
expect(dashboard.panels[0].gridPos).to.eql({x: 0, y: 0, h: 2, w: 8});
|
||||
expect(dashboard.panels[1].gridPos).to.eql({x: 8, y: 0, h: 2, w: 8});
|
||||
expect(dashboard.panels[2].gridPos).to.eql({x: 16, y: 0, h: 2, w: 8});
|
||||
});
|
||||
|
||||
describe('After a second iteration', function() {
|
||||
var repeatedPanelAfterIteration1;
|
||||
|
||||
beforeEach(function() {
|
||||
repeatedPanelAfterIteration1 = dashboard.panels[1];
|
||||
dashboard.panels[0].fill = 10;
|
||||
dashboard.processRepeats();
|
||||
});
|
||||
|
||||
it('reused panel should copy properties from source', function() {
|
||||
expect(dashboard.panels[1].fill).to.be(10);
|
||||
});
|
||||
|
||||
it('should have same panel count', function() {
|
||||
expect(dashboard.panels.length).to.be(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('After a second iteration with different variable', function() {
|
||||
beforeEach(function() {
|
||||
dashboard.templating.list.push({
|
||||
name: 'server',
|
||||
current: { text: 'se1, se2, se3', value: ['se1']},
|
||||
options: [{text: 'se1', value: 'se1', selected: true}]
|
||||
});
|
||||
dashboard.panels[0].repeat = "server";
|
||||
dashboard.processRepeats();
|
||||
});
|
||||
|
||||
it('should remove scopedVars value for last variable', function() {
|
||||
expect(dashboard.panels[0].scopedVars.apps).to.be(undefined);
|
||||
});
|
||||
|
||||
it('should have new variable value in scopedVars', function() {
|
||||
expect(dashboard.panels[0].scopedVars.server.value).to.be("se1");
|
||||
});
|
||||
});
|
||||
|
||||
describe('After a second iteration and selected values reduced', function() {
|
||||
beforeEach(function() {
|
||||
dashboard.templating.list[0].options[1].selected = false;
|
||||
dashboard.processRepeats();
|
||||
});
|
||||
|
||||
it('should clean up repeated panel', function() {
|
||||
expect(dashboard.panels.length).to.be(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('After a second iteration and panel repeat is turned off', function() {
|
||||
beforeEach(function() {
|
||||
dashboard.panels[0].repeat = null;
|
||||
dashboard.processRepeats();
|
||||
});
|
||||
|
||||
it('should clean up repeated panel', function() {
|
||||
expect(dashboard.panels.length).to.be(1);
|
||||
});
|
||||
|
||||
it('should remove scoped vars from reused panel', function() {
|
||||
expect(dashboard.panels[0].scopedVars).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('given dashboard with panel repeat in vertical direction', function(ctx) {
|
||||
var dashboard;
|
||||
|
||||
beforeEach(function() {
|
||||
dashboard = new DashboardModel({
|
||||
panels: [{id: 2, repeat: 'apps', repeatDirection: 'v', gridPos: {x: 5, y: 0, h: 2, w: 8}}],
|
||||
templating: {
|
||||
list: [{
|
||||
name: 'apps',
|
||||
current: {
|
||||
text: 'se1, se2, se3',
|
||||
value: ['se1', 'se2', 'se3']
|
||||
},
|
||||
options: [
|
||||
{text: 'se1', value: 'se1', selected: true},
|
||||
{text: 'se2', value: 'se2', selected: true},
|
||||
{text: 'se3', value: 'se3', selected: true},
|
||||
{text: 'se4', value: 'se4', selected: false}
|
||||
]
|
||||
}]
|
||||
}
|
||||
});
|
||||
dashboard.processRepeats();
|
||||
});
|
||||
|
||||
it('should place on items on top of each other and keep witdh', function() {
|
||||
expect(dashboard.panels[0].gridPos).to.eql({x: 5, y: 0, h: 2, w: 8});
|
||||
expect(dashboard.panels[1].gridPos).to.eql({x: 5, y: 2, h: 2, w: 8});
|
||||
expect(dashboard.panels[2].gridPos).to.eql({x: 5, y: 4, h: 2, w: 8});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,287 +0,0 @@
|
||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||
|
||||
import '../dashboard_srv';
|
||||
import {DynamicDashboardSrv} from '../dynamic_dashboard_srv';
|
||||
|
||||
function dynamicDashScenario(desc, func) {
|
||||
|
||||
describe(desc, function() {
|
||||
var ctx: any = {};
|
||||
|
||||
ctx.setup = function (setupFunc) {
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(angularMocks.module(function($provide) {
|
||||
$provide.value('contextSrv', {
|
||||
user: { timezone: 'utc'}
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(angularMocks.inject(function(dashboardSrv) {
|
||||
ctx.dashboardSrv = dashboardSrv;
|
||||
|
||||
var model = {
|
||||
rows: [],
|
||||
templating: { list: [] }
|
||||
};
|
||||
|
||||
setupFunc(model);
|
||||
ctx.dash = ctx.dashboardSrv.create(model);
|
||||
ctx.dynamicDashboardSrv = new DynamicDashboardSrv();
|
||||
ctx.dynamicDashboardSrv.init(ctx.dash);
|
||||
ctx.dynamicDashboardSrv.process();
|
||||
ctx.rows = ctx.dash.rows;
|
||||
}));
|
||||
};
|
||||
|
||||
func(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
dynamicDashScenario('given dashboard with panel repeat', function(ctx) {
|
||||
ctx.setup(function(dash) {
|
||||
dash.rows.push({
|
||||
panels: [{id: 2, repeat: 'apps'}]
|
||||
});
|
||||
dash.templating.list.push({
|
||||
name: 'apps',
|
||||
current: {
|
||||
text: 'se1, se2, se3',
|
||||
value: ['se1', 'se2', 'se3']
|
||||
},
|
||||
options: [
|
||||
{text: 'se1', value: 'se1', selected: true},
|
||||
{text: 'se2', value: 'se2', selected: true},
|
||||
{text: 'se3', value: 'se3', selected: true},
|
||||
{text: 'se4', value: 'se4', selected: false}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should repeat panel one time', function() {
|
||||
expect(ctx.rows[0].panels.length).to.be(3);
|
||||
});
|
||||
|
||||
it('should mark panel repeated', function() {
|
||||
expect(ctx.rows[0].panels[0].repeat).to.be('apps');
|
||||
expect(ctx.rows[0].panels[1].repeatPanelId).to.be(2);
|
||||
});
|
||||
|
||||
it('should set scopedVars on panels', function() {
|
||||
expect(ctx.rows[0].panels[0].scopedVars.apps.value).to.be('se1');
|
||||
expect(ctx.rows[0].panels[1].scopedVars.apps.value).to.be('se2');
|
||||
expect(ctx.rows[0].panels[2].scopedVars.apps.value).to.be('se3');
|
||||
});
|
||||
|
||||
describe('After a second iteration', function() {
|
||||
var repeatedPanelAfterIteration1;
|
||||
|
||||
beforeEach(function() {
|
||||
repeatedPanelAfterIteration1 = ctx.rows[0].panels[1];
|
||||
ctx.rows[0].panels[0].fill = 10;
|
||||
ctx.dynamicDashboardSrv.process();
|
||||
});
|
||||
|
||||
it('should have reused same panel instances', function() {
|
||||
expect(ctx.rows[0].panels[1]).to.be(repeatedPanelAfterIteration1);
|
||||
});
|
||||
|
||||
it('reused panel should copy properties from source', function() {
|
||||
expect(ctx.rows[0].panels[1].fill).to.be(10);
|
||||
});
|
||||
|
||||
it('should have same panel count', function() {
|
||||
expect(ctx.rows[0].panels.length).to.be(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('After a second iteration with different variable', function() {
|
||||
beforeEach(function() {
|
||||
ctx.dash.templating.list.push({
|
||||
name: 'server',
|
||||
current: { text: 'se1, se2, se3', value: ['se1']},
|
||||
options: [{text: 'se1', value: 'se1', selected: true}]
|
||||
});
|
||||
ctx.rows[0].panels[0].repeat = "server";
|
||||
ctx.dynamicDashboardSrv.process();
|
||||
});
|
||||
|
||||
it('should remove scopedVars value for last variable', function() {
|
||||
expect(ctx.rows[0].panels[0].scopedVars.apps).to.be(undefined);
|
||||
});
|
||||
|
||||
it('should have new variable value in scopedVars', function() {
|
||||
expect(ctx.rows[0].panels[0].scopedVars.server.value).to.be("se1");
|
||||
});
|
||||
});
|
||||
|
||||
describe('After a second iteration and selected values reduced', function() {
|
||||
beforeEach(function() {
|
||||
ctx.dash.templating.list[0].options[1].selected = false;
|
||||
ctx.dynamicDashboardSrv.process();
|
||||
});
|
||||
|
||||
it('should clean up repeated panel', function() {
|
||||
expect(ctx.rows[0].panels.length).to.be(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('After a second iteration and panel repeat is turned off', function() {
|
||||
beforeEach(function() {
|
||||
ctx.rows[0].panels[0].repeat = null;
|
||||
ctx.dynamicDashboardSrv.process();
|
||||
});
|
||||
|
||||
it('should clean up repeated panel', function() {
|
||||
expect(ctx.rows[0].panels.length).to.be(1);
|
||||
});
|
||||
|
||||
it('should remove scoped vars from reused panel', function() {
|
||||
expect(ctx.rows[0].panels[0].scopedVars).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
dynamicDashScenario('given dashboard with row repeat', function(ctx) {
|
||||
ctx.setup(function(dash) {
|
||||
dash.rows.push({
|
||||
repeat: 'servers',
|
||||
panels: [{id: 2}]
|
||||
});
|
||||
dash.rows.push({panels: []});
|
||||
dash.templating.list.push({
|
||||
name: 'servers',
|
||||
current: {
|
||||
text: 'se1, se2',
|
||||
value: ['se1', 'se2']
|
||||
},
|
||||
options: [
|
||||
{text: 'se1', value: 'se1', selected: true},
|
||||
{text: 'se2', value: 'se2', selected: true},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should repeat row one time', function() {
|
||||
expect(ctx.rows.length).to.be(3);
|
||||
});
|
||||
|
||||
it('should keep panel ids on first row', function() {
|
||||
expect(ctx.rows[0].panels[0].id).to.be(2);
|
||||
});
|
||||
|
||||
it('should keep first row as repeat', function() {
|
||||
expect(ctx.rows[0].repeat).to.be('servers');
|
||||
});
|
||||
|
||||
it('should clear repeat field on repeated row', function() {
|
||||
expect(ctx.rows[1].repeat).to.be(null);
|
||||
});
|
||||
|
||||
it('should add scopedVars to rows', function() {
|
||||
expect(ctx.rows[0].scopedVars.servers.value).to.be('se1');
|
||||
expect(ctx.rows[1].scopedVars.servers.value).to.be('se2');
|
||||
});
|
||||
|
||||
it('should generate a repeartRowId based on repeat row index', function() {
|
||||
expect(ctx.rows[1].repeatRowId).to.be(1);
|
||||
expect(ctx.rows[1].repeatIteration).to.be(ctx.dynamicDashboardSrv.iteration);
|
||||
});
|
||||
|
||||
it('should set scopedVars on row panels', function() {
|
||||
expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1');
|
||||
expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2');
|
||||
});
|
||||
|
||||
describe('After a second iteration', function() {
|
||||
var repeatedRowAfterFirstIteration;
|
||||
|
||||
beforeEach(function() {
|
||||
repeatedRowAfterFirstIteration = ctx.rows[1];
|
||||
ctx.rows[0].height = 500;
|
||||
ctx.dynamicDashboardSrv.process();
|
||||
});
|
||||
|
||||
it('should still only have 2 rows', function() {
|
||||
expect(ctx.rows.length).to.be(3);
|
||||
});
|
||||
|
||||
it.skip('should have updated props from source', function() {
|
||||
expect(ctx.rows[1].height).to.be(500);
|
||||
});
|
||||
|
||||
it('should reuse row instance', function() {
|
||||
expect(ctx.rows[1]).to.be(repeatedRowAfterFirstIteration);
|
||||
});
|
||||
});
|
||||
|
||||
describe('After a second iteration and selected values reduced', function() {
|
||||
beforeEach(function() {
|
||||
ctx.dash.templating.list[0].options[1].selected = false;
|
||||
ctx.dynamicDashboardSrv.process();
|
||||
});
|
||||
|
||||
it('should remove repeated second row', function() {
|
||||
expect(ctx.rows.length).to.be(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
dynamicDashScenario('given dashboard with row repeat and panel repeat', function(ctx) {
|
||||
ctx.setup(function(dash) {
|
||||
dash.rows.push({
|
||||
repeat: 'servers',
|
||||
panels: [{id: 2, repeat: 'metric'}]
|
||||
});
|
||||
dash.templating.list.push({
|
||||
name: 'servers',
|
||||
current: { text: 'se1, se2', value: ['se1', 'se2'] },
|
||||
options: [
|
||||
{text: 'se1', value: 'se1', selected: true},
|
||||
{text: 'se2', value: 'se2', selected: true},
|
||||
]
|
||||
});
|
||||
dash.templating.list.push({
|
||||
name: 'metric',
|
||||
current: { text: 'm1, m2', value: ['m1', 'm2'] },
|
||||
options: [
|
||||
{text: 'm1', value: 'm1', selected: true},
|
||||
{text: 'm2', value: 'm2', selected: true},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should repeat row one time', function() {
|
||||
expect(ctx.rows.length).to.be(2);
|
||||
});
|
||||
|
||||
it('should repeat panel on both rows', function() {
|
||||
expect(ctx.rows[0].panels.length).to.be(2);
|
||||
expect(ctx.rows[1].panels.length).to.be(2);
|
||||
});
|
||||
|
||||
it('should keep panel ids on first row', function() {
|
||||
expect(ctx.rows[0].panels[0].id).to.be(2);
|
||||
});
|
||||
|
||||
it('should mark second row as repeated', function() {
|
||||
expect(ctx.rows[0].repeat).to.be('servers');
|
||||
});
|
||||
|
||||
it('should clear repeat field on repeated row', function() {
|
||||
expect(ctx.rows[1].repeat).to.be(null);
|
||||
});
|
||||
|
||||
it('should generate a repeartRowId based on repeat row index', function() {
|
||||
expect(ctx.rows[1].repeatRowId).to.be(1);
|
||||
});
|
||||
|
||||
it('should set scopedVars on row panels', function() {
|
||||
expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1');
|
||||
expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2');
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -3,14 +3,13 @@ import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import {DashboardExporter} from '../export/exporter';
|
||||
import {DashboardModel} from '../DashboardModel';
|
||||
import {DashboardModel} from '../dashboard_model';
|
||||
|
||||
describe('given dashboard with repeated panels', function() {
|
||||
var dash, exported;
|
||||
|
||||
beforeEach(done => {
|
||||
dash = {
|
||||
rows: [],
|
||||
templating: { list: [] },
|
||||
annotations: { list: [] },
|
||||
};
|
||||
@ -47,25 +46,19 @@ describe('given dashboard with repeated panels', function() {
|
||||
datasource: 'gfdb',
|
||||
});
|
||||
|
||||
dash.rows.push({
|
||||
repeat: 'test',
|
||||
panels: [
|
||||
{id: 2, repeat: 'apps', datasource: 'gfdb', type: 'graph'},
|
||||
{id: 3, repeat: null, repeatPanelId: 2},
|
||||
{
|
||||
id: 4,
|
||||
datasource: '-- Mixed --',
|
||||
targets: [{datasource: 'other'}],
|
||||
},
|
||||
{id: 5, datasource: '$ds'},
|
||||
]
|
||||
});
|
||||
dash.panels = [
|
||||
{id: 6, datasource: 'gfdb', type: 'graph'},
|
||||
{id: 7},
|
||||
{
|
||||
id: 8,
|
||||
datasource: '-- Mixed --',
|
||||
targets: [{datasource: 'other'}],
|
||||
},
|
||||
{id: 9, datasource: '$ds'},
|
||||
];
|
||||
|
||||
dash.rows.push({
|
||||
repeat: null,
|
||||
repeatRowId: 1,
|
||||
panels: [],
|
||||
});
|
||||
dash.panels.push({id: 2, repeat: 'apps', datasource: 'gfdb', type: 'graph'});
|
||||
dash.panels.push({id: 3, repeat: null, repeatPanelId: 2});
|
||||
|
||||
var datasourceSrvStub = {get: sinon.stub()};
|
||||
datasourceSrvStub.get.withArgs('gfdb').returns(Promise.resolve({
|
||||
@ -99,16 +92,8 @@ describe('given dashboard with repeated panels', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('exported dashboard should not contain repeated panels', function() {
|
||||
expect(exported.rows[0].panels.length).to.be(3);
|
||||
});
|
||||
|
||||
it('exported dashboard should not contain repeated rows', function() {
|
||||
expect(exported.rows.length).to.be(1);
|
||||
});
|
||||
|
||||
it('should replace datasource refs', function() {
|
||||
var panel = exported.rows[0].panels[0];
|
||||
var panel = exported.panels[0];
|
||||
expect(panel.datasource).to.be("${DS_GFDB}");
|
||||
});
|
||||
|
||||
|
@ -29,6 +29,7 @@ describe("unsavedChangesSrv", function() {
|
||||
beforeEach(function() {
|
||||
dash = _dashboardSrv.create({
|
||||
refresh: false,
|
||||
panels: [{ test: "asd", legend: { } }],
|
||||
rows: [
|
||||
{
|
||||
panels: [{ test: "asd", legend: { } }]
|
||||
@ -58,23 +59,23 @@ describe("unsavedChangesSrv", function() {
|
||||
expect(tracker.hasChanges()).to.be(false);
|
||||
});
|
||||
|
||||
it('Should ignore row collapse change', function() {
|
||||
it.skip('Should ignore row collapse change', function() {
|
||||
dash.rows[0].collapse = true;
|
||||
expect(tracker.hasChanges()).to.be(false);
|
||||
});
|
||||
|
||||
it('Should ignore panel legend changes', function() {
|
||||
dash.rows[0].panels[0].legend.sortDesc = true;
|
||||
dash.rows[0].panels[0].legend.sort = "avg";
|
||||
dash.panels[0].legend.sortDesc = true;
|
||||
dash.panels[0].legend.sort = "avg";
|
||||
expect(tracker.hasChanges()).to.be(false);
|
||||
});
|
||||
|
||||
it('Should ignore panel repeats', function() {
|
||||
it.skip('Should ignore panel repeats', function() {
|
||||
dash.rows[0].panels.push({repeatPanelId: 10});
|
||||
expect(tracker.hasChanges()).to.be(false);
|
||||
});
|
||||
|
||||
it('Should ignore row repeats', function() {
|
||||
it.skip('Should ignore row repeats', function() {
|
||||
dash.addEmptyRow();
|
||||
dash.rows[1].repeatRowId = 10;
|
||||
expect(tracker.hasChanges()).to.be(false);
|
||||
|
@ -106,6 +106,23 @@ function(angular, _) {
|
||||
return true;
|
||||
});
|
||||
|
||||
dash.panels = _.filter(dash.panels, function(panel) {
|
||||
if (panel.repeatPanelId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// remove scopedVars
|
||||
panel.scopedVars = null;
|
||||
|
||||
// ignore panel legend sort
|
||||
if (panel.legend) {
|
||||
delete panel.legend.sort;
|
||||
delete panel.legend.sortDesc;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// ignore template variable values
|
||||
_.each(dash.templating.list, function(value) {
|
||||
value.current = null;
|
||||
|
@ -18,13 +18,13 @@ const template = `
|
||||
<div class="modal-content text-center">
|
||||
|
||||
<div class="confirm-modal-text">
|
||||
Do you want to save you changes?
|
||||
Do you want to save your changes?
|
||||
</div>
|
||||
|
||||
<div class="confirm-modal-buttons">
|
||||
<button type="button" class="btn btn-inverse" ng-click="ctrl.dismiss()">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="ctrl.discard()">Discard</button>
|
||||
<button type="button" class="btn btn-success" ng-click="ctrl.save()">Save</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="ctrl.discard()">Discard</button>
|
||||
<button type="button" class="btn btn-inverse" ng-click="ctrl.dismiss()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -39,18 +39,8 @@ function (angular, _, $, config) {
|
||||
// dont want url changes like adding orgId to add browser history
|
||||
$location.replace();
|
||||
this.update(this.getQueryStringState());
|
||||
this.expandRowForPanel();
|
||||
}
|
||||
|
||||
DashboardViewState.prototype.expandRowForPanel = function() {
|
||||
if (!this.state.panelId) { return; }
|
||||
|
||||
var panelInfo = this.$scope.dashboard.getPanelInfoById(this.state.panelId);
|
||||
if (panelInfo) {
|
||||
panelInfo.row.collapse = false;
|
||||
}
|
||||
};
|
||||
|
||||
DashboardViewState.prototype.needsSync = function(urlState) {
|
||||
return _.isEqual(this.state, urlState) === false;
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {DashboardModel} from '../dashboard/DashboardModel';
|
||||
import {DashboardModel} from '../dashboard/dashboard_model';
|
||||
import Remarkable from 'remarkable';
|
||||
|
||||
export class MetricsTabCtrl {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import {profiler} from 'app/core/profiler';
|
||||
import {appEvents, profiler} from 'app/core/core';
|
||||
import Remarkable from 'remarkable';
|
||||
import {CELL_HEIGHT, CELL_VMARGIN} from '../dashboard/DashboardModel';
|
||||
import {GRID_CELL_HEIGHT, GRID_CELL_VMARGIN} from 'app/core/constants';
|
||||
|
||||
const TITLE_HEIGHT = 25;
|
||||
const EMPTY_TITLE_HEIGHT = 9;
|
||||
@ -163,7 +163,7 @@ export class PanelCtrl {
|
||||
var fullscreenHeight = Math.floor(docHeight * 0.8);
|
||||
this.containerHeight = this.editMode ? editHeight : fullscreenHeight;
|
||||
} else {
|
||||
this.containerHeight = this.panel.gridPos.h * CELL_HEIGHT + ((this.panel.gridPos.h-1) * CELL_VMARGIN);
|
||||
this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + ((this.panel.gridPos.h-1) * GRID_CELL_VMARGIN);
|
||||
}
|
||||
|
||||
this.height = this.containerHeight - (PANEL_BORDER + PANEL_PADDING + (this.panel.title ? TITLE_HEIGHT : EMPTY_TITLE_HEIGHT));
|
||||
@ -188,13 +188,36 @@ export class PanelCtrl {
|
||||
});
|
||||
}
|
||||
|
||||
removePanel() {
|
||||
removePanel(ask: boolean) {
|
||||
// confirm deletion
|
||||
if (ask !== false) {
|
||||
var text2, confirmText;
|
||||
|
||||
if (this.panel.alert) {
|
||||
text2 = "Panel includes an alert rule, removing panel will also remove alert rule";
|
||||
confirmText = "YES";
|
||||
}
|
||||
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Remove Panel',
|
||||
text: 'Are you sure you want to remove this panel?',
|
||||
text2: text2,
|
||||
icon: 'fa-trash',
|
||||
confirmText: confirmText,
|
||||
yesText: 'Remove',
|
||||
onConfirm: () => {
|
||||
this.removePanel(false);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.dashboard.removePanel(this.panel);
|
||||
}
|
||||
|
||||
editPanelJson() {
|
||||
this.publishAppEvent('show-json-editor', {
|
||||
object: this.panel,
|
||||
object: this.panel.getSaveModel(),
|
||||
updateHandler: this.replacePanel.bind(this)
|
||||
});
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ import * as heatmapPanel from 'app/plugins/panel/heatmap/module';
|
||||
import * as tablePanel from 'app/plugins/panel/table/module';
|
||||
import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
|
||||
import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
|
||||
import * as permissionListPlugin from 'app/plugins/panel/permissionlist/module';
|
||||
|
||||
import * as testDataAppPlugin from 'app/plugins/app/testdata/module';
|
||||
import * as testDataDSPlugin from 'app/plugins/app/testdata/datasource/module';
|
||||
|
||||
@ -35,6 +37,7 @@ const builtInPlugins = {
|
||||
"app/plugins/app/testdata/module": testDataAppPlugin,
|
||||
"app/plugins/app/testdata/datasource/module": testDataDSPlugin,
|
||||
|
||||
"app/plugins/panel/permissionlist/module": permissionListPlugin,
|
||||
"app/plugins/panel/text/module": textPanel,
|
||||
"app/plugins/panel/graph/module": graphPanel,
|
||||
"app/plugins/panel/dashlist/module": dashListPanel,
|
||||
|
@ -81,11 +81,6 @@
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
|
@ -1,23 +1,23 @@
|
||||
|
||||
|
||||
<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-inline">
|
||||
<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"
|
||||
ng-model='current.url' placeholder="{{suggestUrl}}"
|
||||
bs-typeahead="getSuggestUrls" min-length="0"
|
||||
ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input>
|
||||
<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'">
|
||||
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.
|
||||
</span>
|
||||
<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.
|
||||
</span>
|
||||
</info-popover>
|
||||
@ -30,7 +30,7 @@
|
||||
<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>
|
||||
<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
|
||||
</info-popover>
|
||||
</div>
|
||||
@ -38,27 +38,21 @@
|
||||
</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">
|
||||
<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="Optional. Needed for self-signed TLS Certs."
|
||||
checked="current.jsonData.tlsAuthWithCACert" label-class="width-11" switch-class="max-width-6">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form-inline">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -79,7 +73,7 @@
|
||||
</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">
|
||||
<h6>TLS Auth Details</h6>
|
||||
<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>
|
||||
</div>
|
||||
<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 class="gf-form" ng-if="current.secureJsonFields.tlsCACert">
|
||||
@ -100,29 +94,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<label class="gf-form-label width-7">Client Cert</label>
|
||||
<div ng-if="current.jsonData.tlsAuth">
|
||||
<div class="gf-form-inline">
|
||||
<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 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 gf-form--v-stretch">
|
||||
<label class="gf-form-label width-7">Client Key</label>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey">
|
||||
<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>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<label class="gf-form-label width-7">Client Key</label>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -42,8 +42,8 @@
|
||||
</div>
|
||||
<div>
|
||||
<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>If the error persists, seek help on the <a href="#" class="error-link">community site</a>.</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="https://community.grafana.com" target="_blank" class="error-link">community site</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,21 +9,27 @@
|
||||
<span class="gf-form-label width-7">Description</span>
|
||||
<textarea class="gf-form-input width-25" rows="3" ng-model="ctrl.panel.description" placeholder="Panel description, supports markdown & links"></textarea>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label-class="width-7" switch-class="max-width-6" label="Transparent" checked="ctrl.panel.transparent" on-change="ctrl.render()"></gf-form-switch>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">Options</h5>
|
||||
<gf-form-switch class="gf-form" label-class="width-8" switch-class="max-width-6" label="Transparent" checked="ctrl.panel.transparent" on-change="ctrl.render()"></gf-form-switch>
|
||||
<h5 class="section-heading">Repeat</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Repeat Panel</span>
|
||||
<span class="gf-form-label width-9">For each value of</span>
|
||||
<dash-repeat-option model="ctrl.panel"></dash-repeat-option>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Min width</span>
|
||||
<div class="gf-form" ng-show="ctrl.panel.repeat">
|
||||
<span class="gf-form-label width-9">Min width</span>
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.minSpan" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10,11,12]">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.panel.repeat">
|
||||
<span class="gf-form-label width-9">Direction</span>
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.repeatDirection" ng-options="f.value as f.text for f in [{value: 'v', text: 'Vertical'}, {value: 'h', text: 'Horizontal'}]">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<panel-links-editor panel="ctrl.panel"></panel-links-editor>
|
||||
|
@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<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 class="gf-form">
|
||||
|
@ -497,8 +497,8 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
|
||||
show: panel.yaxes[0].show,
|
||||
index: 1,
|
||||
logBase: panel.yaxes[0].logBase || 1,
|
||||
min: panel.yaxes[0].min ? _.toNumber(panel.yaxes[0].min) : null,
|
||||
max: panel.yaxes[0].max ? _.toNumber(panel.yaxes[0].max) : null,
|
||||
min: parseNumber(panel.yaxes[0].min),
|
||||
max: parseNumber(panel.yaxes[0].max),
|
||||
tickDecimals: panel.yaxes[0].decimals
|
||||
};
|
||||
|
||||
@ -510,9 +510,9 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
|
||||
secondY.show = panel.yaxes[1].show;
|
||||
secondY.logBase = panel.yaxes[1].logBase || 1;
|
||||
secondY.position = 'right';
|
||||
secondY.min = panel.yaxes[1].min ? _.toNumber(panel.yaxes[1].min) : null;
|
||||
secondY.max = panel.yaxes[1].max ? _.toNumber(panel.yaxes[1].max) : null;
|
||||
secondY.tickDecimals = panel.yaxes[1].decimals !== null ? _.toNumber(panel.yaxes[1].decimals): null;
|
||||
secondY.min = parseNumber(panel.yaxes[1].min);
|
||||
secondY.max = parseNumber(panel.yaxes[1].max);
|
||||
secondY.tickDecimals = panel.yaxes[1].decimals;
|
||||
options.yaxes.push(secondY);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function parseNumber(value: any) {
|
||||
if (value === null || typeof value === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _.toNumber(value);
|
||||
}
|
||||
|
||||
function applyLogScale(axis, data) {
|
||||
if (axis.logBase === 1) {
|
||||
return;
|
||||
|
@ -21,8 +21,8 @@
|
||||
"transparent": true,
|
||||
"type": "text",
|
||||
"gridPos": {
|
||||
"w": 12,
|
||||
"h": 2,
|
||||
"w": 24,
|
||||
"h": 3,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
@ -42,7 +42,7 @@
|
||||
"transparent": false,
|
||||
"type": "dashlist",
|
||||
"gridPos": {
|
||||
"w": 7,
|
||||
"w": 12,
|
||||
"h": 17,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
@ -57,9 +57,9 @@
|
||||
"transparent": false,
|
||||
"type": "pluginlist",
|
||||
"gridPos": {
|
||||
"w": 5,
|
||||
"w": 12,
|
||||
"h": 17,
|
||||
"x": 7,
|
||||
"x": 12,
|
||||
"y": 6
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ $tight-form-bg: $dark-3;
|
||||
$tight-form-func-bg: #333;
|
||||
$tight-form-func-highlight-bg: #444;
|
||||
|
||||
$modal-backdrop-bg: $dark-3;
|
||||
$modal-backdrop-bg: #3a4754;
|
||||
$code-tag-bg: $gray-1;
|
||||
$code-tag-border: lighten($code-tag-bg, 2%);
|
||||
|
||||
|
@ -206,7 +206,7 @@ $zindex-modal: 1050;
|
||||
//
|
||||
|
||||
$btn-padding-x: 1rem !default;
|
||||
$btn-padding-y: .8rem !default;
|
||||
$btn-padding-y: .7rem !default;
|
||||
$btn-line-height: 1 !default;
|
||||
$btn-font-weight: 500 !default;
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
$gf-form-margin: 1px;
|
||||
$gf-form-margin: 3px;
|
||||
|
||||
.gf-form {
|
||||
margin-bottom: $gf-form-margin;
|
||||
@ -55,6 +55,7 @@ $gf-form-margin: 1px;
|
||||
|
||||
.gf-form-label {
|
||||
padding: $input-padding-y $input-padding-x;
|
||||
margin-right: $gf-form-margin;
|
||||
flex-shrink: 0;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
|
||||
@ -107,6 +108,7 @@ $gf-form-margin: 1px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: $input-padding-y $input-padding-x;
|
||||
margin-right: $gf-form-margin;
|
||||
font-size: $font-size-base;
|
||||
line-height: $input-line-height;
|
||||
color: $input-color;
|
||||
@ -114,8 +116,6 @@ $gf-form-margin: 1px;
|
||||
background-image: none;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid $input-border-color;
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
@include border-radius($input-border-radius-sm);
|
||||
@include box-shadow($input-box-shadow);
|
||||
white-space: nowrap;
|
||||
|
@ -62,40 +62,6 @@
|
||||
background-color: $navbarLinkBackgroundActive;
|
||||
}
|
||||
|
||||
.navbar-page-btn {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
margin: 0;
|
||||
color: darken($link-color, 5%);
|
||||
font-size: $font-size-lg;
|
||||
padding: 1rem 1rem 0.75rem 1rem;
|
||||
min-height:: $navbarHeight;
|
||||
|
||||
.fa-caret-down {
|
||||
font-size: 60%;
|
||||
padding-left: 0.2rem;
|
||||
}
|
||||
|
||||
.icon-gf {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
font-size: 20px;
|
||||
line-height: 8px;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
> img {
|
||||
max-width: 27px;
|
||||
max-height: 27px;
|
||||
}
|
||||
|
||||
&--search {
|
||||
padding: 1rem 1.5rem 0.75rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-page-btn {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
@ -12,6 +12,9 @@
|
||||
.gf-tabs-link.active {
|
||||
background-color: $panel-bg;
|
||||
}
|
||||
.tabbed-view-body {
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +54,6 @@
|
||||
.tabbed-view-body {
|
||||
background-color: $panel-bg;
|
||||
padding: $spacer*2 $spacer;
|
||||
min-height: 250px;
|
||||
|
||||
&--small {
|
||||
min-height: 0px;
|
||||
|
@ -43,7 +43,7 @@
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.error-link {color: $yellow;}
|
||||
.error-link {color: $orange;}
|
||||
|
||||
.error-minus {
|
||||
color: #7eb26d;
|
||||
@ -57,4 +57,4 @@
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.graph-text {margin: 0;}
|
||||
.graph-text {margin: 0;}
|
||||
|
@ -2,7 +2,7 @@ import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import {angularMocks, sinon} from '../lib/common';
|
||||
import {PanelModel} from 'app/features/dashboard/PanelModel';
|
||||
import {PanelModel} from 'app/features/dashboard/panel_model';
|
||||
|
||||
export function ControllerTestContext() {
|
||||
var self = this;
|
||||
|
@ -358,6 +358,8 @@
|
||||
// Remove icon clicked
|
||||
self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
|
||||
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));
|
||||
|
||||
// Only add existing value as tags when using strings as tags
|
||||
|
@ -3,7 +3,7 @@ const merge = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
config = merge(common, {
|
||||
devtool: 'inline-source-map',
|
||||
devtool: 'cheap-module-source-map',
|
||||
externals: {
|
||||
'react/addons': true,
|
||||
'react/lib/ExecutionEnvironment': true,
|
||||
@ -13,10 +13,10 @@ config = merge(common, {
|
||||
fs: 'empty'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.SourceMapDevToolPlugin({
|
||||
filename: null, // if no value is provided the sourcemap is inlined
|
||||
test: /\.(ts|js)($|\?)/i // process .js and .ts files only
|
||||
})
|
||||
// new webpack.SourceMapDevToolPlugin({
|
||||
// filename: null, // if no value is provided the sourcemap is inlined
|
||||
// test: /\.(ts|js)($|\?)/i // process .js and .ts files only
|
||||
// })
|
||||
]
|
||||
});
|
||||
|
||||
|
@ -2,8 +2,8 @@
|
||||
"rules": {
|
||||
"no-string-throw": true,
|
||||
"no-unused-expression": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-unused-variable": true,
|
||||
"no-duplicate-variable": true,
|
||||
"curly": true,
|
||||
"class-name": true,
|
||||
"semicolon": [true, "always", "ignore-bound-class-methods"],
|
||||
|
Loading…
Reference in New Issue
Block a user