Merge branch 'data-source-settings-to-react' of github.com:grafana/grafana into data-source-settings-to-react

This commit is contained in:
Torkel Ödegaard
2018-11-07 11:51:02 -08:00
282 changed files with 8359 additions and 3512 deletions
+56 -20
View File
@@ -206,6 +206,10 @@ jobs:
- run: docker info
- run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
- run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}"
- run: rm packaging/docker/grafana-latest.linux-x64.tar.gz
- run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz
- run: cd packaging/docker && ./build-enterprise.sh "master"
grafana-docker-pr:
docker:
@@ -230,6 +234,9 @@ jobs:
- run: docker info
- run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
- run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}"
- run: rm packaging/docker/grafana-latest.linux-x64.tar.gz
- run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz
- run: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}"
build-enterprise:
docker:
@@ -312,39 +319,52 @@ jobs:
deploy-enterprise-master:
docker:
- image: circleci/python:2.7-stretch
- image: grafana/grafana-ci-deploy:1.0.0
steps:
- attach_workspace:
at: .
- run:
name: install awscli
command: 'sudo pip install awscli'
name: gcp credentials
command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json'
- run:
name: sign in to gcp
command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
- run:
name: deploy to s3
command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/master'
- run:
name: deploy to gcp
command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/master'
- run:
name: Deploy to grafana.com
command: 'cd enterprise-dist && ../scripts/build/release_publisher/release_publisher -apikey ${GRAFANA_COM_API_KEY} -enterprise -from-local'
deploy-enterprise-release:
docker:
- image: circleci/python:2.7-stretch
- image: grafana/grafana-ci-deploy:1.0.0
steps:
- attach_workspace:
at: .
- run:
name: install awscli
command: 'sudo pip install awscli'
- run:
name: deploy to s3
command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/release'
- attach_workspace:
at: .
- run:
name: gcp credentials
command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json'
- run:
name: sign in to gcp
command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
- run:
name: deploy to s3
command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/release'
- run:
name: deploy to gcp
command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/release'
deploy-master:
docker:
- image: circleci/python:2.7-stretch
- image: grafana/grafana-ci-deploy:1.0.0
steps:
- attach_workspace:
at: .
- run:
name: install awscli
command: 'sudo pip install awscli'
- run:
name: deploy to s3
command: |
@@ -354,6 +374,15 @@ jobs:
- run:
name: Trigger Windows build
command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master'
- run:
name: gcp credentials
command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json'
- run:
name: sign in to gcp
command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
- run:
name: deploy to gcp
command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://$GCP_BUCKET_NAME/oss/master'
- run:
name: Publish to Grafana.com
command: |
@@ -362,16 +391,22 @@ jobs:
deploy-release:
docker:
- image: circleci/python:2.7-stretch
- image: grafana/grafana-ci-deploy:1.0.0
steps:
- attach_workspace:
at: .
- run:
name: install awscli
command: 'sudo pip install awscli'
- run:
name: deploy to s3
command: 'aws s3 sync ./dist s3://$BUCKET_NAME/release'
- run:
name: gcp credentials
command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json'
- run:
name: sign in to gcp
command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
- run:
name: deploy to gcp
command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://$GCP_BUCKET_NAME/oss/release'
- run:
name: Deploy to Grafana.com
command: './scripts/build/publish.sh'
@@ -409,6 +444,7 @@ workflows:
- grafana-docker-master:
requires:
- build-all
- build-all-enterprise
- test-backend
- test-frontend
- codespell
+2
View File
@@ -8,6 +8,7 @@ awsconfig
/dist
/public/build
/public/views/index.html
/public/views/error.html
/emails/dist
/public_gen
/public/vendor/npm
@@ -75,3 +76,4 @@ debug.test
/devenv/bulk_alerting_dashboards/*.json
/scripts/build/release_publisher/release_publisher
*.patch
+15 -1
View File
@@ -4,20 +4,34 @@
* **Alerting**: Option to disable OK alert notifications [#12330](https://github.com/grafana/grafana/issues/12330) & [#6696](https://github.com/grafana/grafana/issues/6696), thx [@davewat](https://github.com/davewat)
* **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset)
* **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro)
* **MySQL**: Graphical query builder [#13762](https://github.com/grafana/grafana/issues/13762), thx [svenklemm](https://github.com/svenklemm)
* **MySQL**: Support connecting thru Unix socket for MySQL datasource [#12342](https://github.com/grafana/grafana/issues/12342), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
* **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro)
* **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669)
### Minor
* **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
* **Cloudwatch**: AWS/Connect metrics and dimensions [#13970](https://github.com/grafana/grafana/pull/13970), thx [@zcoffy](https://github.com/zcoffy)
* **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm)
* **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
* **DingDing**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
* **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876)
* **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945)
* **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme)
### Breaking changes
* Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited)
# 5.3.3 (unreleased)
* **Alerting**: Delete alerts when parent folder was deleted [#13322](https://github.com/grafana/grafana/issues/13322)
* **MySQL**: Fix `$__timeFilter()` should respect local time zone [#13769](https://github.com/grafana/grafana/issues/13769)
* **Dashboard**: Fix datasource selection in panel by enter key [#13932](https://github.com/grafana/grafana/issues/13932)
* **Graph**: Fix table legend height when positioned below graph and using Internet Explorer 11 [#13903](https://github.com/grafana/grafana/issues/13903)
# 5.3.2 (2018-10-24)
* **InfluxDB/Graphite/Postgres**: Prevent cross site scripting (XSS) in query editor [#13667](https://github.com/grafana/grafana/issues/13667), thx [@svenklemm](https://github.com/svenklemm)
+5
View File
@@ -9,12 +9,17 @@ module.exports = function (grunt) {
destDir: 'dist',
tempDir: 'tmp',
platform: process.platform.replace('win32', 'windows'),
enterprise: false,
};
if (grunt.option('platform')) {
config.platform = grunt.option('platform');
}
if (grunt.option('enterprise')) {
config.enterprise = true;
}
if (grunt.option('arch')) {
config.arch = grunt.option('arch');
} else {
+8 -2
View File
@@ -5,8 +5,7 @@ all: deps build
deps-go:
go run build.go setup
deps-js:
yarn install --pure-lockfile --no-progress
deps-js: node_modules
deps: deps-js
@@ -43,3 +42,10 @@ test: test-go test-js
run:
./bin/grafana-server
clean:
rm -rf node_modules
rm -rf public/build
node_modules: package.json yarn.lock
yarn install --pure-lockfile --no-progress
+33 -8
View File
@@ -41,8 +41,8 @@ var (
race bool
phjsToRelease string
workingDir string
includeBuildNumber bool = true
buildNumber int = 0
includeBuildId bool = true
buildId string = "0"
binaries []string = []string{"grafana-server", "grafana-cli"}
isDev bool = false
enterprise bool = false
@@ -54,6 +54,8 @@ func main() {
ensureGoPath()
var buildIdRaw string
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
flag.StringVar(&gocc, "cc", "", "CC")
@@ -61,12 +63,14 @@ func main() {
flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH")
flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary")
flag.BoolVar(&race, "race", race, "Use race detector")
flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name")
flag.BoolVar(&includeBuildId, "includeBuildId", includeBuildId, "IncludeBuildId in package name")
flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana")
flag.IntVar(&buildNumber, "buildNumber", 0, "Build number from CI system")
flag.StringVar(&buildIdRaw, "buildId", "0", "Build ID from CI system")
flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps")
flag.Parse()
buildId = shortenBuildId(buildIdRaw)
readVersionFromPackageJson()
if pkgArch == "" {
@@ -197,9 +201,9 @@ func readVersionFromPackageJson() {
}
// add timestamp to iteration
if includeBuildNumber {
if buildNumber != 0 {
linuxPackageIteration = fmt.Sprintf("%d%s", buildNumber, linuxPackageIteration)
if includeBuildId {
if buildId != "0" {
linuxPackageIteration = fmt.Sprintf("%s%s", buildId, linuxPackageIteration)
} else {
linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration)
}
@@ -392,7 +396,7 @@ func grunt(params ...string) {
func gruntBuildArg(task string) []string {
args := []string{task}
if includeBuildNumber {
if includeBuildId {
args = append(args, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration))
} else {
args = append(args, fmt.Sprintf("--pkgVer=%v", version))
@@ -403,6 +407,10 @@ func gruntBuildArg(task string) []string {
if phjsToRelease != "" {
args = append(args, fmt.Sprintf("--phjsToRelease=%v", phjsToRelease))
}
if enterprise {
args = append(args, "--enterprise")
}
args = append(args, fmt.Sprintf("--platform=%v", goos))
return args
@@ -467,6 +475,7 @@ func ldflags() string {
b.WriteString(fmt.Sprintf(" -X main.version=%s", version))
b.WriteString(fmt.Sprintf(" -X main.commit=%s", getGitSha()))
b.WriteString(fmt.Sprintf(" -X main.buildstamp=%d", buildStamp()))
b.WriteString(fmt.Sprintf(" -X main.buildBranch=%s", getGitBranch()))
return b.String()
}
@@ -514,6 +523,14 @@ func setBuildEnv() {
}
}
func getGitBranch() string {
v, err := runError("git", "rev-parse", "--abbrev-ref", "HEAD")
if err != nil {
return "master"
}
return string(v)
}
func getGitSha() string {
v, err := runError("git", "rev-parse", "--short", "HEAD")
if err != nil {
@@ -619,3 +636,11 @@ func shaFile(file string) error {
return out.Close()
}
func shortenBuildId(buildId string) string {
buildId = strings.Replace(buildId, "-", "", -1)
if (len(buildId) < 9) {
return buildId
}
return buildId[0:8]
}
+4
View File
@@ -557,3 +557,7 @@ callback_url =
[panels]
enable_alpha = false
[enterprise]
license_path =
+5
View File
@@ -475,3 +475,8 @@ log_queries =
# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
;server_url =
;callback_url =
[enterprise]
# Path to a valid Grafana Enterprise license.jwt file
;license_path =
+123 -6
View File
@@ -927,6 +927,123 @@
"title": "",
"type": "text"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "gdev-testdata",
"editable": true,
"error": false,
"fill": 0,
"gridPos": {
"h": 7,
"w": 16,
"x": 0,
"y": 44
},
"id": 21,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [
{
"alias": "C-series",
"steppedLine": true
}
],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"alias": "",
"hide": false,
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "1,null,40,null,90,null,null,100,null,null,100,null,null,80,null",
"target": ""
},
{
"alias": "",
"hide": false,
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "20,null40,null,null,50,null,70,null,100,null,10,null,30,null",
"target": ""
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Null between points",
"tooltip": {
"msResolution": false,
"shared": true,
"sort": 0,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"content": "Left is showing null between values for a normal line graph and staircase graph. Orphaned data points should be rendered as points",
"editable": true,
"error": false,
"gridPos": {
"h": 7,
"w": 8,
"x": 16,
"y": 44
},
"id": 22,
"links": [],
"mode": "markdown",
"title": "",
"type": "text"
},
{
"aliasColors": {},
"bars": false,
@@ -939,7 +1056,7 @@
"h": 7,
"w": 24,
"x": 0,
"y": 44
"y": 51
},
"id": 20,
"legend": {
@@ -1024,7 +1141,7 @@
"h": 7,
"w": 12,
"x": 0,
"y": 51
"y": 58
},
"id": 16,
"legend": {
@@ -1127,7 +1244,7 @@
"h": 7,
"w": 12,
"x": 12,
"y": 51
"y": 58
},
"id": 17,
"legend": {
@@ -1266,7 +1383,7 @@
"h": 7,
"w": 12,
"x": 0,
"y": 58
"y": 65
},
"id": 18,
"legend": {
@@ -1370,7 +1487,7 @@
"h": 7,
"w": 12,
"x": 12,
"y": 58
"y": 65
},
"id": 19,
"legend": {
@@ -1554,5 +1671,5 @@
"timezone": "browser",
"title": "Panel Tests - Graph",
"uid": "5SdHCadmz",
"version": 3
"version": 1
}
+108 -2
View File
@@ -404,6 +404,112 @@
"title": "Column style thresholds & units",
"transform": "timeseries_to_columns",
"type": "table"
},
{
"columns": [],
"datasource": "gdev-testdata",
"fontSize": "100%",
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 26
},
"id": 6,
"links": [],
"pageSize": 20,
"scroll": true,
"showHeader": true,
"sort": {
"col": 0,
"desc": true
},
"styles": [
{
"alias": "Time",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"pattern": "Time",
"type": "date"
},
{
"alias": "",
"colorMode": "cell",
"colors": [
"rgba(245, 54, 54, 0.5)",
"rgba(237, 129, 40, 0.5)",
"rgba(50, 172, 45, 0.5)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"link": true,
"linkTargetBlank": true,
"linkTooltip": "",
"linkUrl": "http://www.grafana.com",
"mappingType": 1,
"pattern": "ColorCell",
"thresholds": [
"5",
"10"
],
"type": "number",
"unit": "currencyUSD"
},
{
"alias": "",
"colorMode": "value",
"colors": [
"rgba(245, 54, 54, 0.5)",
"rgba(237, 129, 40, 0.5)",
"rgba(50, 172, 45, 0.5)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"link": true,
"linkUrl": "http://www.grafana.com",
"mappingType": 1,
"pattern": "ColorValue",
"thresholds": [
"5",
"10"
],
"type": "number",
"unit": "Bps"
},
{
"alias": "",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"decimals": 2,
"pattern": "/.*/",
"thresholds": [],
"type": "number",
"unit": "short"
}
],
"targets": [
{
"alias": "ColorValue",
"expr": "",
"format": "table",
"intervalFactor": 1,
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "null,1,20,90,30,5,0,20,10"
},
{
"alias": "ColorCell",
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "null,5,1,2,3,4,5,10,20"
}
],
"title": "Column style thresholds and links",
"transform": "timeseries_to_columns",
"type": "table"
}
],
"refresh": false,
@@ -449,5 +555,5 @@
"timezone": "browser",
"title": "Panel Tests - Table",
"uid": "pttable",
"version": 1
}
"version": 2
}
+1 -1
View File
@@ -158,7 +158,7 @@ Since not all datasources have the same configuration settings we only have the
| timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL & MSSQL | Lowest interval/step value that should be used for this data source |
| esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56) |
| timeField | string | Elasticsearch | Which field that should be used as timestamp |
| interval | string | Elasticsearch | Index date time format |
| interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' |
| authType | string | Cloudwatch | Auth provider. keys/credentials/arn |
| assumeRoleArn | string | Cloudwatch | ARN of Assume Role |
| defaultRegion | string | Cloudwatch | AWS region |
+1 -1
View File
@@ -140,7 +140,7 @@ In DingTalk PC Client:
6. There will be a Webhook URL in the panel, looks like this: https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx. Copy this URL to the grafana Dingtalk setting page and then click "finish".
Dingtalk supports the following "message type": `text`, `link` and `markdown`. Only the `text` message type is supported.
Dingtalk supports the following "message type": `text`, `link` and `markdown`. Only the `link` message type is supported.
### Kafka
+2 -2
View File
@@ -100,12 +100,12 @@ display name, especially if the display name contains spaces or special
characters. Make sure you always use the group or subgroup name as it appears
in the URL of the group or subgroup.
Here's a complete example with `alloed_sign_up` enabled, and access limited to
Here's a complete example with `allow_sign_up` enabled, and access limited to
the `example` and `foo/bar` groups:
```ini
[auth.gitlab]
enabled = false
enabled = true
allow_sign_up = true
client_id = GITLAB_APPLICATION_ID
client_secret = GITLAB_SECRET
@@ -60,7 +60,8 @@ Here is a minimal policy example:
"Effect": "Allow",
"Action": [
"cloudwatch:ListMetrics",
"cloudwatch:GetMetricStatistics"
"cloudwatch:GetMetricStatistics",
"cloudwatch:GetMetricData"
],
"Resource": "*"
},
@@ -73,6 +73,58 @@ Example:
You can use wildcards (`*`) in place of database or table if you want to grant access to more databases and tables.
## Query Editor
> Only available in Grafana v5.4+.
{{< docs-imagebox img="/img/docs/v54/mysql_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v54/mysql_query.gif" >}}
You find the MySQL query editor in the metrics tab in a panel's edit mode. You enter edit mode by clicking the
panel title, then edit.
The query editor has a link named `Generated SQL` that shows up after a query has been executed, while in panel edit mode. Click on it and it will expand and show the raw interpolated SQL string that was executed.
### Select table, time column and metric column (FROM)
When you enter edit mode for the first time or add a new query Grafana will try to prefill the query builder with the first table that has a timestamp column and a numeric column.
In the FROM field, Grafana will suggest tables that are in the configured database. To select a table or view in another database that your database user has access to you can manually enter a fully qualified name (database.table) like `otherDb.metrics`.
The Time column field refers to the name of the column holding your time values. Selecting a value for the Metric column field is optional. If a value is selected, the Metric column field will be used as the series name.
The metric column suggestions will only contain columns with a text datatype (text, tinytext, mediumtext, longtext, varchar, char).
If you want to use a column with a different datatype as metric column you may enter the column name with a cast: `CAST(numericColumn as CHAR)`.
You may also enter arbitrary SQL expressions in the metric column field that evaluate to a text datatype like
`CONCAT(column1, " ", CAST(numericColumn as CHAR))`.
### Columns and Aggregation functions (SELECT)
In the `SELECT` row you can specify what columns and functions you want to use.
In the column field you may write arbitrary expressions instead of a column name like `column1 * column2 / column3`.
If you use aggregate functions you need to group your resultset. The editor will automatically add a `GROUP BY time` if you add an aggregate function.
You may add further value columns by clicking the plus button and selecting `Column` from the menu. Multiple value columns will be plotted as separate series in the graph panel.
### Filter data (WHERE)
To add a filter click the plus icon to the right of the `WHERE` condition. You can remove filters by clicking on
the filter and selecting `Remove`. A filter for the current selected timerange is automatically added to new queries.
### Group By
To group by time or any other columns click the plus icon at the end of the GROUP BY row. The suggestion dropdown will only show text columns of your currently selected table but you may manually enter any column.
You can remove the group by clicking on the item and then selecting `Remove`.
If you add any grouping, all selected columns need to have an aggregate function applied. The query builder will automatically add aggregate functions to all columns without aggregate functions when you add groupings.
#### Gap Filling
Grafana can fill in missing values when you group by time. The time function accepts two arguments. The first argument is the time window that you would like to group by, and the second argument is the value you want Grafana to fill missing items with.
### Text Editor Mode (RAW)
You can switch to the raw query editor mode by clicking the hamburger icon and selecting `Switch editor mode` or by clicking `Edit SQL` below the query.
> If you use the raw query editor, be sure your query at minimum has `ORDER BY time` and a filter on the returned time range.
## Macros
To simplify syntax and to allow for dynamic parts, like date range filters, the query can contain macros.
+3 -3
View File
@@ -18,7 +18,7 @@ Grafana v5.3 brings new features, many enhancements and bug fixes. This article
- [TV mode]({{< relref "#tv-and-kiosk-mode" >}}) is improved and more accessible
- [Alerting]({{< relref "#notification-reminders" >}}) with notification reminders
- [Postgres]({{< relref "#postgres-query-builder" >}}) gets a new query builder!
- [OAuth]({{< relref "#improved-oauth-support-for-gitlab" >}}) support for Gitlab is improved
- [OAuth]({{< relref "#improved-oauth-support-for-gitlab" >}}) support for GitLab is improved
- [Annotations]({{< relref "#annotations" >}}) with template variable filtering
- [Variables]({{< relref "#variables" >}}) with free text support
@@ -69,9 +69,9 @@ Grafana 5.3 comes with a new graphical query builder for Postgres. This brings P
{{< docs-imagebox img="/img/docs/v53/postgres_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v53/postgres_query.gif" >}}
## Improved OAuth Support for Gitlab
## Improved OAuth Support for GitLab
Grafana 5.3 comes with a new OAuth integration for Gitlab that enables configuration to only allow users that are a member of certain Gitlab groups to authenticate. This makes it possible to use Gitlab OAuth with Grafana in a shared environment without giving everyone access to Grafana.
Grafana 5.3 comes with a new OAuth integration for GitLab that enables configuration to only allow users that are a member of certain GitLab groups to authenticate. This makes it possible to use GitLab OAuth with Grafana in a shared environment without giving everyone access to Grafana.
Learn how to enable and configure it in the [documentation](/auth/gitlab/).
## Annotations
+1 -1
View File
@@ -290,7 +290,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
"sendReminder": true,
"frequency": "15m",
"settings": {
"addresses: "carl@grafana.com;dev@grafana.com"
"addresses": "carl@grafana.com;dev@grafana.com"
}
}
```
+2 -1
View File
@@ -47,6 +47,7 @@
"grunt-contrib-copy": "~1.0.0",
"grunt-contrib-cssmin": "~1.0.2",
"grunt-exec": "^1.0.1",
"grunt-newer": "^1.3.0",
"grunt-notify": "^0.4.5",
"grunt-postcss": "^0.8.0",
"grunt-sass": "^2.0.0",
@@ -102,7 +103,7 @@
"build": "grunt build",
"test": "grunt test",
"lint": "tslint -c tslint.json --project tsconfig.json",
"jest": "jest --config jest.config.json --notify --watch",
"jest": "jest --notify --watch",
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
"precommit": "lint-staged && grunt precommit"
},
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
set -e
_grafana_tag=$1
_docker_repo=${2:-grafana/grafana-enterprise}
docker build \
--tag "${_docker_repo}:${_grafana_tag}"\
--no-cache=true \
.
docker push "${_docker_repo}:${_grafana_tag}"
-29
View File
@@ -1,29 +0,0 @@
# New Grafana Release Processes
## Building release packages
1) Update package.json so that it has the right version.
2) Create a git tag for the release: `git tag -a v3.0.4 -m "3.0.4 release"`
3) Push branch & tag to github!
2) Packages from master a built automatically by circle CI for this repo [grafana/grafana-packer](https://github.com/grafana/grafana-packer)
### Non master branch
When building from non master branch create a new branch in repo [grafana/grafana-packer](https://github.com/grafana/grafana-packer)
and configure circle.yml to deploy that branch as well, https://github.com/grafana/grafana-packer/blob/master/circle.yml#L25,
you also need to update https://github.com/grafana/grafana-packer/blob/v3.1.x/deploy.sh#L7.
### Windows build
Sign into ci.appveyor.com and the Grafana project's build history page. Builds for windows take a long time (around 20min)
and fail quite often for random reasons so I usually continue with the release process without a windows build already built.
1) Click on the green build that has the correct version and tag
2) Click on `DEPLOYMENTS`
3) Click on `NEW DEPLOYMENT`
4) Select GrafanaBuildS3
4) Select the build you want to deploy.
The deployment should be quick (just uploads the release zip file to S3)
+4
View File
@@ -134,12 +134,16 @@ func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
OrgId: c.OrgId,
Dashboard: dto.Dashboard,
PanelId: dto.PanelId,
User: c.SignedInUser,
}
if err := bus.Dispatch(&backendCmd); err != nil {
if validationErr, ok := err.(alerting.ValidationError); ok {
return Error(422, validationErr.Error(), nil)
}
if err == m.ErrDataSourceAccessDenied {
return Error(403, "Access denied to datasource", err)
}
return Error(500, "Failed to test rule", err)
}
+5 -45
View File
@@ -1,62 +1,22 @@
package api
import (
"fmt"
"github.com/pkg/errors"
"time"
"github.com/grafana/grafana/pkg/api/pluginproxy"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
)
const HeaderNameNoBackendCache = "X-Grafana-NoCache"
func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) {
userPermissionsQuery := m.GetDataSourcePermissionsForUserQuery{
User: c.SignedInUser,
}
if err := bus.Dispatch(&userPermissionsQuery); err != nil {
if err != bus.ErrHandlerNotFound {
return nil, err
}
} else {
permissionType, exists := userPermissionsQuery.Result[id]
if exists && permissionType != m.DsPermissionQuery {
return nil, errors.New("User not allowed to access datasource")
}
}
nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
cacheKey := fmt.Sprintf("ds-%d", id)
if !nocache {
if cached, found := hs.cache.Get(cacheKey); found {
ds := cached.(*m.DataSource)
if ds.OrgId == c.OrgId {
return ds, nil
}
}
}
query := m.GetDataSourceByIdQuery{Id: id, OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
return nil, err
}
hs.cache.Set(cacheKey, query.Result, time.Second*5)
return query.Result, nil
}
func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
dsId := c.ParamsInt64(":id")
ds, err := hs.getDatasourceFromCache(dsId, c)
ds, err := hs.DatasourceCache.GetDatasource(dsId, c.SignedInUser, c.SkipCache)
if err != nil {
if err == m.ErrDataSourceAccessDenied {
c.JsonApiErr(403, "Access denied to datasource", err)
return
}
c.JsonApiErr(500, "Unable to load datasource meta data", err)
return
}
+1
View File
@@ -14,6 +14,7 @@ type IndexViewData struct {
NewGrafanaVersionExists bool
NewGrafanaVersion string
AppName string
AppNameBodyClass string
}
type PluginCss struct {
+10 -8
View File
@@ -16,7 +16,6 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
gocache "github.com/patrickmn/go-cache"
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/api/live"
@@ -28,6 +27,8 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/cache"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/setting"
@@ -46,19 +47,19 @@ type HTTPServer struct {
macaron *macaron.Macaron
context context.Context
streamManager *live.StreamManager
cache *gocache.Cache
httpSrv *http.Server
RouteRegister routing.RouteRegister `inject:""`
Bus bus.Bus `inject:""`
RenderService rendering.Service `inject:""`
Cfg *setting.Cfg `inject:""`
HooksService *hooks.HooksService `inject:""`
RouteRegister routing.RouteRegister `inject:""`
Bus bus.Bus `inject:""`
RenderService rendering.Service `inject:""`
Cfg *setting.Cfg `inject:""`
HooksService *hooks.HooksService `inject:""`
CacheService *cache.CacheService `inject:""`
DatasourceCache datasources.CacheService `inject:""`
}
func (hs *HTTPServer) Init() error {
hs.log = log.New("http.server")
hs.cache = gocache.New(5*time.Minute, 10*time.Minute)
hs.streamManager = live.NewStreamManager()
hs.macaron = hs.newMacaron()
@@ -231,6 +232,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m.Use(middleware.ValidateHostHeader(setting.Domain))
}
m.Use(middleware.HandleNoCacheHeader())
m.Use(middleware.AddDefaultResponseHeaders())
}
+12
View File
@@ -83,6 +83,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
NewGrafanaVersion: plugins.GrafanaLatestVersion,
NewGrafanaVersionExists: plugins.GrafanaHasUpdate,
AppName: setting.ApplicationName,
AppNameBodyClass: getAppNameBodyClass(setting.ApplicationName),
}
if setting.DisableGravatar {
@@ -377,3 +378,14 @@ func (hs *HTTPServer) NotFoundHandler(c *m.ReqContext) {
c.HTML(404, "index", data)
}
func getAppNameBodyClass(name string) string {
switch name {
case setting.APP_NAME:
return "app-grafana"
case setting.APP_NAME_ENTERPRISE:
return "app-enterprise"
default:
return ""
}
}
+4 -1
View File
@@ -25,8 +25,11 @@ func (hs *HTTPServer) QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) R
return Error(400, "Query missing datasourceId", nil)
}
ds, err := hs.getDatasourceFromCache(datasourceId, c)
ds, err := hs.DatasourceCache.GetDatasource(datasourceId, c.SignedInUser, c.SkipCache)
if err != nil {
if err == m.ErrDataSourceAccessDenied {
return Error(403, "Access denied to datasource", err)
}
return Error(500, "Unable to load datasource meta data", err)
}
+8 -8
View File
@@ -3,6 +3,8 @@ package main
import (
"flag"
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"os/signal"
"runtime"
@@ -11,16 +13,12 @@ import (
"syscall"
"time"
"net/http"
_ "net/http/pprof"
extensions "github.com/grafana/grafana/pkg/extensions"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/setting"
extensions "github.com/grafana/grafana/pkg/extensions"
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/grafana/grafana/pkg/setting"
_ "github.com/grafana/grafana/pkg/tsdb/cloudwatch"
_ "github.com/grafana/grafana/pkg/tsdb/elasticsearch"
_ "github.com/grafana/grafana/pkg/tsdb/graphite"
@@ -35,6 +33,7 @@ import (
var version = "5.0.0"
var commit = "NA"
var buildBranch = "master"
var buildstamp string
var configFile = flag.String("config", "", "path to config file")
@@ -47,7 +46,7 @@ func main() {
profilePort := flag.Int("profile-port", 6060, "Define custom port for profiling")
flag.Parse()
if *v {
fmt.Printf("Version %s (commit: %s)\n", version, commit)
fmt.Printf("Version %s (commit: %s, branch: %s)\n", version, commit, buildBranch)
os.Exit(0)
}
@@ -78,9 +77,10 @@ func main() {
setting.BuildVersion = version
setting.BuildCommit = commit
setting.BuildStamp = buildstampInt64
setting.BuildBranch = buildBranch
setting.IsEnterprise = extensions.IsEnterprise
metrics.M_Grafana_Version.WithLabelValues(version).Set(1)
metrics.SetBuildInformation(version, commit, buildBranch)
server := NewGrafanaServer()
+6 -6
View File
@@ -12,20 +12,20 @@ import (
"time"
"github.com/facebookgo/inject"
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/social"
"golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/services/cache"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social"
// self registering services
_ "github.com/grafana/grafana/pkg/extensions"
_ "github.com/grafana/grafana/pkg/metrics"
@@ -77,6 +77,7 @@ func (g *GrafanaServerImpl) Run() error {
serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
serviceGraph.Provide(&inject.Object{Value: g.cfg})
serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)})
// self registered services
services := registry.GetServices()
@@ -143,7 +144,6 @@ func (g *GrafanaServerImpl) Run() error {
}
sendSystemdNotification("READY=1")
return g.childRoutines.Wait()
}
@@ -159,7 +159,7 @@ func (g *GrafanaServerImpl) loadConfiguration() {
os.Exit(1)
}
g.log.Info("Starting "+setting.ApplicationName, "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
g.log.Info("Starting "+setting.ApplicationName, "version", version, "commit", commit, "branch", buildBranch, "compiled", time.Unix(setting.BuildStamp, 0))
g.cfg.LogConfigSources()
}
+1
View File
@@ -2,6 +2,7 @@ package login
import (
"errors"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
)
+1 -1
View File
@@ -185,7 +185,7 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
if ldapUser.isMemberOf(group.GroupDN) {
extUser.OrgRoles[group.OrgId] = group.OrgRole
if extUser.IsGrafanaAdmin == nil || *extUser.IsGrafanaAdmin == false {
if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin {
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
}
}
+27 -3
View File
@@ -58,7 +58,14 @@ var (
M_StatActive_Users prometheus.Gauge
M_StatTotal_Orgs prometheus.Gauge
M_StatTotal_Playlists prometheus.Gauge
M_Grafana_Version *prometheus.GaugeVec
// M_Grafana_Version is a gauge that contains build info about this binary
//
// Deprecated: use M_Grafana_Build_Version instead.
M_Grafana_Version *prometheus.GaugeVec
// grafanaBuildVersion is a gauge that contains build info about this binary
grafanaBuildVersion *prometheus.GaugeVec
)
func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec {
@@ -293,9 +300,25 @@ func init() {
M_Grafana_Version = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "info",
Help: "Information about the Grafana",
Help: "Information about the Grafana. This metric is deprecated. please use `grafana_build_info`",
Namespace: exporterName,
}, []string{"version"})
grafanaBuildVersion = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "build_info",
Help: "A metric with a constant '1' value labeled by version, revision, branch, and goversion from which Grafana was built.",
Namespace: exporterName,
}, []string{"version", "revision", "branch", "goversion"})
}
// SetBuildInformation sets the build information for this binary
func SetBuildInformation(version, revision, branch string) {
// We export this info twice for backwards compability.
// Once this have been released for some time we should be able to remote `M_Grafana_Version`
// The reason we added a new one is that its common practice in the prometheus community
// to name this metric `*_build_info` so its easy to do aggregation on all programs.
M_Grafana_Version.WithLabelValues(version).Set(1)
grafanaBuildVersion.WithLabelValues(version, revision, branch, runtime.Version()).Set(1)
}
func initMetricVars() {
@@ -334,7 +357,8 @@ func initMetricVars() {
M_StatActive_Users,
M_StatTotal_Orgs,
M_StatTotal_Playlists,
M_Grafana_Version)
M_Grafana_Version,
grafanaBuildVersion)
}
+14
View File
@@ -0,0 +1,14 @@
package middleware
import (
m "github.com/grafana/grafana/pkg/models"
macaron "gopkg.in/macaron.v1"
)
const HeaderNameNoBackendCache = "X-Grafana-NoCache"
func HandleNoCacheHeader() macaron.Handler {
return func(ctx *m.ReqContext) {
ctx.SkipCache = ctx.Req.Header.Get(HeaderNameNoBackendCache) == "true"
}
}
+8 -6
View File
@@ -29,6 +29,7 @@ func GetContextHandler() macaron.Handler {
Session: session.GetSession(),
IsSignedIn: false,
AllowAnonymous: false,
SkipCache: false,
Logger: log.New("context"),
}
@@ -43,12 +44,13 @@ func GetContextHandler() macaron.Handler {
// then init session and look for userId in session
// then look for api key in session (special case for render calls via api)
// then test if anonymous access is enabled
if initContextWithRenderAuth(ctx) ||
initContextWithApiKey(ctx) ||
initContextWithBasicAuth(ctx, orgId) ||
initContextWithAuthProxy(ctx, orgId) ||
initContextWithUserSessionCookie(ctx, orgId) ||
initContextWithAnonymousUser(ctx) {
switch {
case initContextWithRenderAuth(ctx):
case initContextWithApiKey(ctx):
case initContextWithBasicAuth(ctx, orgId):
case initContextWithAuthProxy(ctx, orgId):
case initContextWithUserSessionCookie(ctx, orgId):
case initContextWithAnonymousUser(ctx):
}
ctx.Logger = log.New("context", "userId", ctx.UserId, "orgId", ctx.OrgId, "uname", ctx.Login)
+1
View File
@@ -18,6 +18,7 @@ import (
)
func TestMiddlewareContext(t *testing.T) {
setting.ERR_TEMPLATE_NAME = "error-template"
Convey("Given the grafana middleware", t, func() {
middlewareScenario("middleware should add context to injector", func(sc *scenarioContext) {
+1 -1
View File
@@ -138,7 +138,7 @@ func Recovery() macaron.Handler {
c.JSON(500, resp)
} else {
c.HTML(500, "error")
c.HTML(500, setting.ERR_TEMPLATE_NAME)
}
}
}()
+4
View File
@@ -8,11 +8,14 @@ import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/macaron.v1"
)
func TestRecoveryMiddleware(t *testing.T) {
setting.ERR_TEMPLATE_NAME = "error-template"
Convey("Given an api route that panics", t, func() {
apiURL := "/api/whatever"
recoveryScenario("recovery middleware should return json", apiURL, func(sc *scenarioContext) {
@@ -50,6 +53,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
sc := &scenarioContext{
url: url,
}
viewsPath, _ := filepath.Abs("../../public/views")
sc.m = macaron.New()
+2 -1
View File
@@ -215,13 +215,14 @@ type AlertStateInfoDTO struct {
// "Internal" commands
type UpdateDashboardAlertsCommand struct {
UserId int64
OrgId int64
Dashboard *Dashboard
User *SignedInUser
}
type ValidateDashboardAlertsCommand struct {
UserId int64
OrgId int64
Dashboard *Dashboard
User *SignedInUser
}
+2 -1
View File
@@ -20,6 +20,7 @@ type ReqContext struct {
IsSignedIn bool
IsRenderCall bool
AllowAnonymous bool
SkipCache bool
Logger log.Logger
}
@@ -36,7 +37,7 @@ func (ctx *ReqContext) Handle(status int, title string, err error) {
ctx.Data["AppSubUrl"] = setting.AppSubUrl
ctx.Data["Theme"] = "dark"
ctx.HTML(status, "error")
ctx.HTML(status, setting.ERR_TEMPLATE_NAME)
}
func (ctx *ReqContext) JsonOK(message string) {
-5
View File
@@ -207,11 +207,6 @@ func (p DsPermissionType) String() string {
return names[int(p)]
}
type GetDataSourcePermissionsForUserQuery struct {
User *SignedInUser
Result map[int64]DsPermissionType
}
type DatasourcesPermissionFilterQuery struct {
User *SignedInUser
Datasources []*DataSource
+1
View File
@@ -165,6 +165,7 @@ type SignedInUser struct {
IsAnonymous bool
HelpFlags1 HelpFlags1
LastSeenAt time.Time
Teams []int64
}
func (u *SignedInUser) ShouldUpdateLastSeenAt() bool {
-1
View File
@@ -121,7 +121,6 @@ func (pm *PluginManager) Run(ctx context.Context) error {
pm.checkForUpdates()
case <-ctx.Done():
run = false
break
}
}
+34 -3
View File
@@ -29,11 +29,42 @@ func Register(descriptor *Descriptor) {
}
func GetServices() []*Descriptor {
sort.Slice(services, func(i, j int) bool {
return services[i].InitPriority > services[j].InitPriority
slice := getServicesWithOverrides()
sort.Slice(slice, func(i, j int) bool {
return slice[i].InitPriority > slice[j].InitPriority
})
return services
return slice
}
type OverrideServiceFunc func(descriptor Descriptor) (*Descriptor, bool)
var overrides []OverrideServiceFunc
func RegisterOverride(fn OverrideServiceFunc) {
overrides = append(overrides, fn)
}
func getServicesWithOverrides() []*Descriptor {
slice := []*Descriptor{}
for _, s := range services {
var descriptor *Descriptor
for _, fn := range overrides {
if newDescriptor, override := fn(*s); override {
descriptor = newDescriptor
break
}
}
if descriptor != nil {
slice = append(slice, descriptor)
} else {
slice = append(slice, s)
}
}
return slice
}
// Service interface is the lowest common shape that services
+3 -3
View File
@@ -11,7 +11,7 @@ func init() {
}
func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User)
return extractor.ValidateAlerts()
}
@@ -19,11 +19,11 @@ func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
saveAlerts := m.SaveAlertsCommand{
OrgId: cmd.OrgId,
UserId: cmd.UserId,
UserId: cmd.User.UserId,
DashboardId: cmd.Dashboard.Id,
}
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User)
alerts, err := extractor.GetAlerts()
if err != nil {
@@ -52,6 +52,24 @@ func TestSimpleReducer(t *testing.T) {
So(result, ShouldEqual, float64(1))
})
Convey("median should ignore null values", func() {
reducer := NewSimpleReducer("median")
series := &tsdb.TimeSeries{
Name: "test time serie",
}
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 3))
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(1)), 4))
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(2)), 5))
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(3)), 6))
result := reducer.Reduce(series)
So(result.Valid, ShouldEqual, true)
So(result.Float64, ShouldEqual, float64(2))
})
Convey("avg", func() {
result := testReducer("avg", 1, 2, 3)
So(result, ShouldEqual, float64(2))
+18 -1
View File
@@ -13,14 +13,16 @@ import (
// DashAlertExtractor extracts alerts from the dashboard json
type DashAlertExtractor struct {
User *m.SignedInUser
Dash *m.Dashboard
OrgID int64
log log.Logger
}
// NewDashAlertExtractor returns a new DashAlertExtractor
func NewDashAlertExtractor(dash *m.Dashboard, orgID int64) *DashAlertExtractor {
func NewDashAlertExtractor(dash *m.Dashboard, orgID int64, user *m.SignedInUser) *DashAlertExtractor {
return &DashAlertExtractor{
User: user,
Dash: dash,
OrgID: orgID,
log: log.New("alerting.extractor"),
@@ -149,6 +151,21 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)}
}
dsFilterQuery := m.DatasourcesPermissionFilterQuery{
User: e.User,
Datasources: []*m.DataSource{datasource},
}
if err := bus.Dispatch(&dsFilterQuery); err != nil {
if err != bus.ErrHandlerNotFound {
return nil, err
}
} else {
if len(dsFilterQuery.Result) == 0 {
return nil, m.ErrDataSourceAccessDenied
}
}
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
if interval, err := panel.Get("interval").String(); err == nil {
+8 -8
View File
@@ -69,7 +69,7 @@ func TestAlertRuleExtraction(t *testing.T) {
So(getTarget(dashJson), ShouldEqual, "")
})
extractor := NewDashAlertExtractor(dash, 1)
extractor := NewDashAlertExtractor(dash, 1, nil)
_, _ = extractor.GetAlerts()
Convey("Dashboard json should not be updated after extracting rules", func() {
@@ -83,7 +83,7 @@ func TestAlertRuleExtraction(t *testing.T) {
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1)
extractor := NewDashAlertExtractor(dash, 1, nil)
alerts, err := extractor.GetAlerts()
@@ -146,7 +146,7 @@ func TestAlertRuleExtraction(t *testing.T) {
dashJson, err := simplejson.NewJson(panelWithoutId)
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1)
extractor := NewDashAlertExtractor(dash, 1, nil)
_, err = extractor.GetAlerts()
@@ -162,7 +162,7 @@ func TestAlertRuleExtraction(t *testing.T) {
dashJson, err := simplejson.NewJson(panelWithIdZero)
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1)
extractor := NewDashAlertExtractor(dash, 1, nil)
_, err = extractor.GetAlerts()
@@ -178,7 +178,7 @@ func TestAlertRuleExtraction(t *testing.T) {
dashJson, err := simplejson.NewJson(json)
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1)
extractor := NewDashAlertExtractor(dash, 1, nil)
alerts, err := extractor.GetAlerts()
@@ -198,7 +198,7 @@ func TestAlertRuleExtraction(t *testing.T) {
dashJson, err := simplejson.NewJson(json)
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1)
extractor := NewDashAlertExtractor(dash, 1, nil)
alerts, err := extractor.GetAlerts()
@@ -228,7 +228,7 @@ func TestAlertRuleExtraction(t *testing.T) {
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1)
extractor := NewDashAlertExtractor(dash, 1, nil)
alerts, err := extractor.GetAlerts()
@@ -248,7 +248,7 @@ func TestAlertRuleExtraction(t *testing.T) {
dashJSON, err := simplejson.NewJson(json)
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1)
extractor := NewDashAlertExtractor(dash, 1, nil)
err = extractor.ValidateAlerts()
@@ -57,6 +57,9 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
message := evalContext.Rule.Message
picUrl := evalContext.ImagePublicUrl
title := evalContext.GetNotificationTitle()
if message == "" {
message = title
}
bodyJSON, err := simplejson.NewJson([]byte(`{
"msgtype": "link",
+1 -1
View File
@@ -14,7 +14,7 @@ import (
)
const (
captionLengthLimit = 200
captionLengthLimit = 1024
)
var (
@@ -61,7 +61,7 @@ func TestTelegramNotifier(t *testing.T) {
})
caption := generateImageCaption(evalContext, "http://grafa.url/abcdef", "")
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
So(len(caption), ShouldBeLessThanOrEqualTo, 1024)
So(caption, ShouldContainSubstring, "Some kind of message.")
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
So(caption, ShouldContainSubstring, "http://grafa.url/abcdef")
@@ -78,9 +78,9 @@ func TestTelegramNotifier(t *testing.T) {
})
caption := generateImageCaption(evalContext,
"http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"foo bar")
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
So(len(caption), ShouldBeLessThanOrEqualTo, 1024)
So(caption, ShouldContainSubstring, "Some kind of message.")
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
So(caption, ShouldContainSubstring, "foo bar")
@@ -91,31 +91,31 @@ func TestTelegramNotifier(t *testing.T) {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it.",
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis scelerisque. Nulla ipsum ex, iaculis vitae vehicula sit amet, fermentum eu eros.",
State: m.AlertStateOK,
})
caption := generateImageCaption(evalContext,
"http://grafa.url/foo",
"")
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
So(len(caption), ShouldBeLessThanOrEqualTo, 1024)
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
So(caption, ShouldNotContainSubstring, "http")
So(caption, ShouldContainSubstring, "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise ")
So(caption, ShouldContainSubstring, "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis sceleri")
})
Convey("Metrics should be skipped if they don't fit", func() {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I ",
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis sceleri",
State: m.AlertStateOK,
})
caption := generateImageCaption(evalContext,
"http://grafa.url/foo",
"foo bar long song")
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
So(len(caption), ShouldBeLessThanOrEqualTo, 1024)
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
So(caption, ShouldNotContainSubstring, "http")
So(caption, ShouldNotContainSubstring, "foo bar")
+2 -5
View File
@@ -34,11 +34,8 @@ func NewRuleReader() *DefaultRuleReader {
func (arr *DefaultRuleReader) initReader() {
heartbeat := time.NewTicker(time.Second * 10)
for {
select {
case <-heartbeat.C:
arr.heartbeat()
}
for range heartbeat.C {
arr.heartbeat()
}
}
+2 -1
View File
@@ -13,6 +13,7 @@ type AlertTestCommand struct {
Dashboard *simplejson.Json
PanelId int64
OrgId int64
User *m.SignedInUser
Result *EvalContext
}
@@ -25,7 +26,7 @@ func handleAlertTestCommand(cmd *AlertTestCommand) error {
dash := m.NewDashboardFromJson(cmd.Dashboard)
extractor := NewDashAlertExtractor(dash, cmd.OrgId)
extractor := NewDashAlertExtractor(dash, cmd.OrgId, cmd.User)
alerts, err := extractor.GetAlerts()
if err != nil {
return err
+17
View File
@@ -0,0 +1,17 @@
package cache
import (
"time"
gocache "github.com/patrickmn/go-cache"
)
type CacheService struct {
*gocache.Cache
}
func New(defaultExpiration, cleanupInterval time.Duration) *CacheService {
return &CacheService{
Cache: gocache.New(defaultExpiration, cleanupInterval),
}
}
+2 -1
View File
@@ -90,6 +90,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
validateAlertsCmd := models.ValidateDashboardAlertsCommand{
OrgId: dto.OrgId,
Dashboard: dash,
User: dto.User,
}
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
@@ -159,8 +160,8 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
alertCmd := models.UpdateDashboardAlertsCommand{
OrgId: dto.OrgId,
UserId: dto.User.UserId,
Dashboard: cmd.Result,
User: dto.User,
}
if err := bus.Dispatch(&alertCmd); err != nil {
+53
View File
@@ -0,0 +1,53 @@
package datasources
import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/cache"
)
type CacheService interface {
GetDatasource(datasourceID int64, user *m.SignedInUser, skipCache bool) (*m.DataSource, error)
}
type CacheServiceImpl struct {
Bus bus.Bus `inject:""`
CacheService *cache.CacheService `inject:""`
}
func init() {
registry.Register(&registry.Descriptor{
Name: "DatasourceCacheService",
Instance: &CacheServiceImpl{},
InitPriority: registry.Low,
})
}
func (dc *CacheServiceImpl) Init() error {
return nil
}
func (dc *CacheServiceImpl) GetDatasource(datasourceID int64, user *m.SignedInUser, skipCache bool) (*m.DataSource, error) {
cacheKey := fmt.Sprintf("ds-%d", datasourceID)
if !skipCache {
if cached, found := dc.CacheService.Get(cacheKey); found {
ds := cached.(*m.DataSource)
if ds.OrgId == user.OrgId {
return ds, nil
}
}
}
query := m.GetDataSourceByIdQuery{Id: datasourceID, OrgId: user.OrgId}
if err := dc.Bus.Dispatch(&query); err != nil {
return nil, err
}
dc.CacheService.Set(cacheKey, query.Result, time.Second*5)
return query.Result, nil
}
+18 -4
View File
@@ -327,6 +327,24 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
if dashboard.IsFolder {
deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)")
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
dashIds := []struct {
Id int64
}{}
err := sess.SQL("select id from dashboard where folder_id = ?", dashboard.Id).Find(&dashIds)
if err != nil {
return err
}
for _, id := range dashIds {
if err := deleteAlertDefinition(id.Id, sess); err != nil {
return nil
}
}
}
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
return nil
}
for _, sql := range deletes {
@@ -337,10 +355,6 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
}
}
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
return nil
}
return nil
})
}
+8 -3
View File
@@ -16,6 +16,7 @@ import (
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/cache"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
@@ -47,8 +48,9 @@ func init() {
}
type SqlStore struct {
Cfg *setting.Cfg `inject:""`
Bus bus.Bus `inject:""`
Cfg *setting.Cfg `inject:""`
Bus bus.Bus `inject:""`
CacheService *cache.CacheService `inject:""`
dbCfg DatabaseConfig
engine *xorm.Engine
@@ -148,9 +150,11 @@ func (ss *SqlStore) Init() error {
// Init repo instances
annotations.SetRepository(&SqlAnnotationRepo{})
ss.Bus.SetTransactionManager(ss)
// Register handlers
ss.addUserQueryAndCommandHandlers()
// ensure admin user
if ss.skipEnsureAdmin {
return nil
@@ -322,6 +326,7 @@ func InitTestDB(t *testing.T) *SqlStore {
sqlstore := &SqlStore{}
sqlstore.skipEnsureAdmin = true
sqlstore.Bus = bus.New()
sqlstore.CacheService = cache.New(5*time.Minute, 10*time.Minute)
dbType := migrator.SQLITE
+30 -3
View File
@@ -15,8 +15,9 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func init() {
//bus.AddHandler("sql", CreateUser)
func (ss *SqlStore) addUserQueryAndCommandHandlers() {
ss.Bus.AddHandler(ss.GetSignedInUserWithCache)
bus.AddHandler("sql", GetUserById)
bus.AddHandler("sql", UpdateUser)
bus.AddHandler("sql", ChangeUserPassword)
@@ -25,7 +26,6 @@ func init() {
bus.AddHandler("sql", SetUsingOrg)
bus.AddHandler("sql", UpdateUserLastSeenAt)
bus.AddHandler("sql", GetUserProfile)
bus.AddHandler("sql", GetSignedInUser)
bus.AddHandler("sql", SearchUsers)
bus.AddHandler("sql", GetUserOrgList)
bus.AddHandler("sql", DeleteUser)
@@ -345,6 +345,22 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error {
return err
}
func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) error {
cacheKey := fmt.Sprintf("signed-in-user-%d-%d", query.UserId, query.OrgId)
if cached, found := ss.CacheService.Get(cacheKey); found {
query.Result = cached.(*m.SignedInUser)
return nil
}
err := GetSignedInUser(query)
if err != nil {
return err
}
ss.CacheService.Set(cacheKey, query.Result, time.Second*5)
return nil
}
func GetSignedInUser(query *m.GetSignedInUserQuery) error {
orgId := "u.org_id"
if query.OrgId > 0 {
@@ -389,6 +405,17 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
user.OrgName = "Org missing"
}
getTeamsByUserQuery := &m.GetTeamsByUserQuery{OrgId: user.OrgId, UserId: user.UserId}
err = GetTeamsByUser(getTeamsByUserQuery)
if err != nil {
return err
}
user.Teams = make([]int64, len(getTeamsByUserQuery.Result))
for i, t := range getTeamsByUserQuery.Result {
user.Teams[i] = t.Id
}
query.Result = &user
return err
}
+22 -16
View File
@@ -13,15 +13,12 @@ import (
"regexp"
"runtime"
"strings"
"gopkg.in/ini.v1"
"github.com/go-macaron/session"
"time"
"github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/util"
"gopkg.in/ini.v1"
)
type Scheme string
@@ -34,9 +31,15 @@ const (
)
const (
DEV string = "development"
PROD string = "production"
TEST string = "test"
DEV = "development"
PROD = "production"
TEST = "test"
APP_NAME = "Grafana"
APP_NAME_ENTERPRISE = "Grafana Enterprise"
)
var (
ERR_TEMPLATE_NAME = "error"
)
var (
@@ -49,6 +52,7 @@ var (
// build
BuildVersion string
BuildCommit string
BuildBranch string
BuildStamp int64
IsEnterprise bool
ApplicationName string
@@ -209,12 +213,10 @@ type Cfg struct {
RendererLimitAlerting int
DisableBruteForceLoginProtection bool
TempDataLifetime time.Duration
MetricsEndpointEnabled bool
EnableAlphaPanels bool
TempDataLifetime time.Duration
MetricsEndpointEnabled bool
EnableAlphaPanels bool
EnterpriseLicensePath string
}
type CommandLineArgs struct {
@@ -533,9 +535,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
// Temporary keep global, to make refactor in steps
Raw = cfg.Raw
ApplicationName = "Grafana"
ApplicationName = APP_NAME
if IsEnterprise {
ApplicationName += " Enterprise"
ApplicationName = APP_NAME_ENTERPRISE
}
Env = iniFile.Section("").Key("app_mode").MustString("development")
@@ -715,6 +717,10 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
imageUploadingSection := iniFile.Section("external_image_storage")
ImageUploadProvider = imageUploadingSection.Key("provider").MustString("")
enterprise := iniFile.Section("enterprise")
cfg.EnterpriseLicensePath = enterprise.Key("license_path").MustString(filepath.Join(cfg.DataPath, "license.jwt"))
return nil
}
+13 -2
View File
@@ -35,6 +35,7 @@ type CustomMetricsCache struct {
var customMetricsMetricsMap map[string]map[string]map[string]*CustomMetricsCache
var customMetricsDimensionsMap map[string]map[string]map[string]*CustomMetricsCache
var regionCache sync.Map
func init() {
metricsMap = map[string][]string{
@@ -45,6 +46,7 @@ func init() {
"AWS/Billing": {"EstimatedCharges"},
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
"AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
"AWS/Connect": {"CallsBreachingConcurrencyQuota", "CallBackNotDialableNumber", "CallRecordingUploadError", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MissedCalls", "MisconfiguredPhoneNumbers", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"},
"AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"},
"AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"},
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
@@ -119,6 +121,7 @@ func init() {
"AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"},
"AWS/CloudFront": {"DistributionId", "Region"},
"AWS/CloudSearch": {},
"AWS/Connect": {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"},
"AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
"AWS/DX": {"ConnectionId"},
"AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"},
@@ -233,13 +236,20 @@ func parseMultiSelectValue(input string) []string {
// Whenever this list is updated, frontend list should also be updated.
// Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
dsInfo := e.getDsInfo("default")
profile := dsInfo.Profile
if cache, ok := regionCache.Load(profile); ok {
if cache2, ok2 := cache.([]suggestData); ok2 {
return cache2, nil
}
}
regions := []string{
"ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ap-south-1", "ap-southeast-1", "ap-southeast-2", "ca-central-1",
"eu-central-1", "eu-north-1", "eu-west-1", "eu-west-2", "eu-west-3", "me-south-1", "sa-east-1", "us-east-1", "us-east-2", "us-west-1", "us-west-2",
"cn-north-1", "cn-northwest-1", "us-gov-east-1", "us-gov-west-1", "us-isob-east-1", "us-iso-east-1",
}
err := e.ensureClientSession("us-east-1")
err := e.ensureClientSession("default")
if err != nil {
return nil, err
}
@@ -269,6 +279,7 @@ func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *s
for _, region := range regions {
result = append(result, suggestData{Text: region, Value: region})
}
regionCache.Store(profile, result)
return result, nil
}
+32 -1
View File
@@ -9,20 +9,26 @@ import (
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
"github.com/bmizerany/assert"
"github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
)
type mockedEc2 struct {
ec2iface.EC2API
Resp ec2.DescribeInstancesOutput
Resp ec2.DescribeInstancesOutput
RespRegions ec2.DescribeRegionsOutput
}
func (m mockedEc2) DescribeInstancesPages(in *ec2.DescribeInstancesInput, fn func(*ec2.DescribeInstancesOutput, bool) bool) error {
fn(&m.Resp, true)
return nil
}
func (m mockedEc2) DescribeRegions(in *ec2.DescribeRegionsInput) (*ec2.DescribeRegionsOutput, error) {
return &m.RespRegions, nil
}
func TestCloudWatchMetrics(t *testing.T) {
@@ -82,6 +88,31 @@ func TestCloudWatchMetrics(t *testing.T) {
})
})
Convey("When calling handleGetRegions", t, func() {
executor := &CloudWatchExecutor{
ec2Svc: mockedEc2{RespRegions: ec2.DescribeRegionsOutput{
Regions: []*ec2.Region{
{
RegionName: aws.String("ap-northeast-2"),
},
},
}},
}
jsonData := simplejson.New()
jsonData.Set("defaultRegion", "default")
executor.DataSource = &models.DataSource{
JsonData: jsonData,
SecureJsonData: securejsondata.SecureJsonData{},
}
result, _ := executor.handleGetRegions(context.Background(), simplejson.New(), &tsdb.TsdbQuery{})
Convey("Should return regions", func() {
So(result[0].Text, ShouldEqual, "ap-northeast-1")
So(result[1].Text, ShouldEqual, "ap-northeast-2")
})
})
Convey("When calling handleGetEc2InstanceAttribute", t, func() {
executor := &CloudWatchExecutor{
ec2Svc: mockedEc2{Resp: ec2.DescribeInstancesOutput{
+2 -4
View File
@@ -164,14 +164,12 @@ func formatTimeRange(input string) string {
func fixIntervalFormat(target string) string {
rMinute := regexp.MustCompile(`'(\d+)m'`)
rMin := regexp.MustCompile("m")
target = rMinute.ReplaceAllStringFunc(target, func(m string) string {
return rMin.ReplaceAllString(m, "min")
return strings.Replace(m, "m", "min", -1)
})
rMonth := regexp.MustCompile(`'(\d+)M'`)
rMon := regexp.MustCompile("M")
target = rMonth.ReplaceAllStringFunc(target, func(M string) string {
return rMon.ReplaceAllString(M, "mon")
return strings.Replace(M, "M", "mon", -1)
})
return target
}
+1 -1
View File
@@ -60,7 +60,7 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
return fmt.Sprintf("%s BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", args[0], m.timeRange.GetFromAsSecondsEpoch(), m.timeRange.GetToAsSecondsEpoch()), nil
case "__timeGroup":
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name)
+3 -3
View File
@@ -60,7 +60,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
})
Convey("interpolate __unixEpochFilter function", func() {
@@ -92,7 +92,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
})
Convey("interpolate __unixEpochFilter function", func() {
@@ -112,7 +112,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
})
Convey("interpolate __unixEpochFilter function", func() {
+1 -2
View File
@@ -186,8 +186,7 @@ func reverse(s string) string {
}
func interpolateFilterWildcards(value string) string {
re := regexp.MustCompile("[*]")
matches := len(re.FindAllStringIndex(value, -1))
matches := strings.Count(value, "*")
if matches == 2 && strings.HasSuffix(value, "*") && strings.HasPrefix(value, "*") {
value = strings.Replace(value, "*", "", -1)
value = fmt.Sprintf(`has_substring("%s")`, value)
@@ -0,0 +1,28 @@
import { AppNotification } from 'app/types/';
export enum ActionTypes {
AddAppNotification = 'ADD_APP_NOTIFICATION',
ClearAppNotification = 'CLEAR_APP_NOTIFICATION',
}
interface AddAppNotificationAction {
type: ActionTypes.AddAppNotification;
payload: AppNotification;
}
interface ClearAppNotificationAction {
type: ActionTypes.ClearAppNotification;
payload: number;
}
export type Action = AddAppNotificationAction | ClearAppNotificationAction;
export const clearAppNotification = (appNotificationId: number) => ({
type: ActionTypes.ClearAppNotification,
payload: appNotificationId,
});
export const notifyApp = (appNotification: AppNotification) => ({
type: ActionTypes.AddAppNotification,
payload: appNotification,
});
+2 -1
View File
@@ -1,4 +1,5 @@
import { updateLocation } from './location';
import { updateNavIndex, UpdateNavIndexAction } from './navModel';
import { notifyApp, clearAppNotification } from './appNotification';
export { updateLocation, updateNavIndex, UpdateNavIndexAction };
export { updateLocation, updateNavIndex, UpdateNavIndexAction, notifyApp, clearAppNotification };
+28
View File
@@ -0,0 +1,28 @@
import { ThunkAction } from 'redux-thunk';
import { getBackendSrv } from '../services/backend_srv';
import { DashboardAcl, DashboardSearchHit, StoreState } from '../../types';
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
export type Action = LoadStarredDashboardsAction;
export enum ActionTypes {
LoadStarredDashboards = 'LOAD_STARRED_DASHBOARDS',
}
interface LoadStarredDashboardsAction {
type: ActionTypes.LoadStarredDashboards;
payload: DashboardSearchHit[];
}
const starredDashboardsLoaded = (dashboards: DashboardAcl[]) => ({
type: ActionTypes.LoadStarredDashboards,
payload: dashboards,
});
export function loadStarredDashboards(): ThunkResult<void> {
return async dispatch => {
const starredDashboards = await getBackendSrv().search({ starred: true });
dispatch(starredDashboardsLoaded(starredDashboards));
};
}
+2
View File
@@ -5,10 +5,12 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
import { SearchResult } from './components/search/SearchResult';
import { TagFilter } from './components/TagFilter/TagFilter';
import { SideMenu } from './components/sidemenu/SideMenu';
import AppNotificationList from './components/AppNotifications/AppNotificationList';
export function registerAngularDirectives() {
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
react2AngularDirective('sidemenu', SideMenu, []);
react2AngularDirective('appNotificationsList', AppNotificationList, []);
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
react2AngularDirective('searchResult', SearchResult, []);
@@ -1,15 +1,22 @@
import React from 'react';
import React from 'react';
import Transition from 'react-transition-group/Transition';
const defaultMaxHeight = '200px'; // When animating using max-height we need to use a static value.
interface Style {
transition?: string;
overflow?: string;
}
// When animating using max-height we need to use a static value.
// If this is not enough, pass in <SlideDown maxHeight="....
const defaultMaxHeight = '200px';
const defaultDuration = 200;
const defaultStyle = {
export const defaultStyle: Style = {
transition: `max-height ${defaultDuration}ms ease-in-out`,
overflow: 'hidden',
};
export default ({ children, in: inProp, maxHeight = defaultMaxHeight }) => {
export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = defaultStyle }) => {
// There are 4 main states a Transition can be in:
// ENTERING, ENTERED, EXITING, EXITED
// https://reactcommunity.org/react-transition-group/
@@ -25,7 +32,7 @@ export default ({ children, in: inProp, maxHeight = defaultMaxHeight }) => {
{state => (
<div
style={{
...defaultStyle,
...style,
...transitionStyles[state],
}}
>
@@ -0,0 +1,38 @@
import React, { Component } from 'react';
import { AppNotification } from 'app/types';
interface Props {
appNotification: AppNotification;
onClearNotification: (id) => void;
}
export default class AppNotificationItem extends Component<Props> {
shouldComponentUpdate(nextProps) {
return this.props.appNotification.id !== nextProps.appNotification.id;
}
componentDidMount() {
const { appNotification, onClearNotification } = this.props;
setTimeout(() => {
onClearNotification(appNotification.id);
}, appNotification.timeout);
}
render() {
const { appNotification, onClearNotification } = this.props;
return (
<div className={`alert-${appNotification.severity} alert`}>
<div className="alert-icon">
<i className={appNotification.icon} />
</div>
<div className="alert-body">
<div className="alert-title">{appNotification.title}</div>
<div className="alert-text">{appNotification.text}</div>
</div>
<button type="button" className="alert-close" onClick={() => onClearNotification(appNotification.id)}>
<i className="fa fa fa-remove" />
</button>
</div>
);
}
}
@@ -0,0 +1,60 @@
import React, { PureComponent } from 'react';
import appEvents from 'app/core/app_events';
import AppNotificationItem from './AppNotificationItem';
import { notifyApp, clearAppNotification } from 'app/core/actions';
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
import { AppNotification, StoreState } from 'app/types';
import {
createErrorNotification,
createSuccessNotification,
createWarningNotification,
} from '../../copy/appNotification';
export interface Props {
appNotifications: AppNotification[];
notifyApp: typeof notifyApp;
clearAppNotification: typeof clearAppNotification;
}
export class AppNotificationList extends PureComponent<Props> {
componentDidMount() {
const { notifyApp } = this.props;
appEvents.on('alert-warning', options => notifyApp(createWarningNotification(options[0], options[1])));
appEvents.on('alert-success', options => notifyApp(createSuccessNotification(options[0], options[1])));
appEvents.on('alert-error', options => notifyApp(createErrorNotification(options[0], options[1])));
}
onClearAppNotification = id => {
this.props.clearAppNotification(id);
};
render() {
const { appNotifications } = this.props;
return (
<div>
{appNotifications.map((appNotification, index) => {
return (
<AppNotificationItem
key={`${appNotification.id}-${index}`}
appNotification={appNotification}
onClearNotification={id => this.onClearAppNotification(id)}
/>
);
})}
</div>
);
}
}
const mapStateToProps = (state: StoreState) => ({
appNotifications: state.appNotifications.appNotifications,
});
const mapDispatchToProps = {
notifyApp,
clearAppNotification,
};
export default connectWithStore(AppNotificationList, mapStateToProps, mapDispatchToProps);
@@ -7,6 +7,7 @@ const model = {
buttonIcon: 'ga css class',
buttonLink: 'http://url/to/destination',
buttonTitle: 'Click me',
onClick: jest.fn(),
proTip: 'This is a tip',
proTipLink: 'http://url/to/tip/destination',
proTipLinkTitle: 'Learn more',
@@ -11,6 +11,7 @@ class EmptyListCTA extends Component<Props, any> {
buttonIcon,
buttonLink,
buttonTitle,
onClick,
proTip,
proTipLink,
proTipLinkTitle,
@@ -19,7 +20,7 @@ class EmptyListCTA extends Component<Props, any> {
return (
<div className="empty-list-cta">
<div className="empty-list-cta__title">{title}</div>
<a href={buttonLink} className="empty-list-cta__button btn btn-xlarge btn-success">
<a onClick={onClick} href={buttonLink} className="empty-list-cta__button btn btn-xlarge btn-success">
<i className={buttonIcon} />
{buttonTitle}
</a>
@@ -12,6 +12,7 @@ exports[`EmptyListCTA renders correctly 1`] = `
<a
className="empty-list-cta__button btn btn-xlarge btn-success"
href="http://url/to/destination"
onClick={[MockFunction]}
>
<i
className="ga css class"
@@ -1,7 +0,0 @@
import React, { SFC } from 'react';
interface Props {}
const InfoPopover: SFC<Props> = props => {
return <div />;
};
export default InfoPopover;
@@ -5,11 +5,12 @@ interface Props {
tooltip?: string;
for?: string;
children: ReactNode;
width?: number;
}
export const Label: SFC<Props> = props => {
return (
<span className="gf-form-label width-10">
<span className={`gf-form-label width-${props.width ? props.width : '10'}`}>
<span>{props.children}</span>
{props.tooltip && (
<Tooltip className="gf-form-help-icon--right-normal" placement="auto" content={props.tooltip}>
@@ -0,0 +1,46 @@
import React, { SFC } from 'react';
import Select from 'react-select';
import DescriptionOption from './DescriptionOption';
import ResetStyles from './ResetStyles';
interface Props {
className?: string;
defaultValue: any;
getOptionLabel: (item: any) => string;
getOptionValue: (item: any) => string;
onSelected: (item: any) => {} | void;
options: any[];
placeholder?: string;
width: number;
}
const SimplePicker: SFC<Props> = ({
className,
defaultValue,
getOptionLabel,
getOptionValue,
onSelected,
options,
placeholder,
width,
}) => {
return (
<Select
classNamePrefix={`gf-form-select-box`}
className={`width-${width} gf-form-input gf-form-input--form-dropdown ${className || ''}`}
components={{
Option: DescriptionOption,
}}
defaultValue={defaultValue}
getOptionLabel={getOptionLabel}
getOptionValue={getOptionValue}
isSearchable={false}
onChange={onSelected}
options={options}
placeholder={placeholder || 'Choose'}
styles={ResetStyles}
/>
);
};
export default SimplePicker;
@@ -13,14 +13,13 @@ export interface State {
id: any;
}
export class FormSwitch extends PureComponent<Props, State> {
export class Switch extends PureComponent<Props, State> {
state = {
id: _.uniqueId(),
};
internalOnChange = event => {
event.stopPropagation();
this.props.onChange(event);
};
@@ -45,5 +44,3 @@ export class FormSwitch extends PureComponent<Props, State> {
);
}
}
export default FormSwitch;
+16 -25
View File
@@ -1,37 +1,28 @@
import React from 'react';
import React, { PureComponent } from 'react';
import withTooltip from './withTooltip';
import { Target } from 'react-popper';
interface TooltipProps {
interface Props {
tooltipSetState: (prevState: object) => void;
}
class Tooltip extends React.Component<TooltipProps, any> {
constructor(props) {
super(props);
this.showTooltip = this.showTooltip.bind(this);
this.hideTooltip = this.hideTooltip.bind(this);
}
showTooltip() {
class Tooltip extends PureComponent<Props> {
showTooltip = () => {
const { tooltipSetState } = this.props;
tooltipSetState(prevState => {
return {
...prevState,
show: true,
};
});
}
hideTooltip() {
tooltipSetState(prevState => ({
...prevState,
show: true,
}));
};
hideTooltip = () => {
const { tooltipSetState } = this.props;
tooltipSetState(prevState => {
return {
...prevState,
show: false,
};
});
}
tooltipSetState(prevState => ({
...prevState,
show: false,
}));
};
render() {
return (
@@ -1,53 +1,84 @@
import React from 'react';
import { ColorPickerPopover } from './ColorPickerPopover';
import { react2AngularDirective } from 'app/core/utils/react2angular';
import ReactDOM from 'react-dom';
import Drop from 'tether-drop';
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
export interface Props {
series: any;
onColorChange: (color: string) => void;
onToggleAxis: () => void;
export interface SeriesColorPickerProps {
color: string;
yaxis?: number;
optionalClass?: string;
onColorChange: (newColor: string) => void;
onToggleAxis?: () => void;
}
export class SeriesColorPicker extends React.Component<Props, any> {
export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
pickerElem: any;
colorPickerDrop: any;
static defaultProps = {
optionalClass: '',
yaxis: undefined,
onToggleAxis: () => {},
};
constructor(props) {
super(props);
this.onColorChange = this.onColorChange.bind(this);
this.onToggleAxis = this.onToggleAxis.bind(this);
}
onColorChange(color) {
this.props.onColorChange(color);
componentWillUnmount() {
this.destroyDrop();
}
onToggleAxis() {
this.props.onToggleAxis();
}
onClickToOpen = () => {
if (this.colorPickerDrop) {
this.destroyDrop();
}
renderAxisSelection() {
const leftButtonClass = this.props.series.yaxis === 1 ? 'btn-success' : 'btn-inverse';
const rightButtonClass = this.props.series.yaxis === 2 ? 'btn-success' : 'btn-inverse';
return (
<div className="p-b-1">
<label className="small p-r-1">Y Axis:</label>
<button onClick={this.onToggleAxis} className={'btn btn-small ' + leftButtonClass}>
Left
</button>
<button onClick={this.onToggleAxis} className={'btn btn-small ' + rightButtonClass}>
Right
</button>
</div>
const { color, yaxis, onColorChange, onToggleAxis } = this.props;
const dropContent = (
<SeriesColorPickerPopover color={color} yaxis={yaxis} onColorChange={onColorChange} onToggleAxis={onToggleAxis} />
);
const dropContentElem = document.createElement('div');
ReactDOM.render(dropContent, dropContentElem);
const drop = new Drop({
target: this.pickerElem,
content: dropContentElem,
position: 'top center',
classes: 'drop-popover',
openOn: 'hover',
hoverCloseDelay: 200,
remove: true,
tetherOptions: {
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
},
});
drop.on('close', this.closeColorPicker.bind(this));
this.colorPickerDrop = drop;
this.colorPickerDrop.open();
};
closeColorPicker() {
setTimeout(() => {
this.destroyDrop();
}, 100);
}
destroyDrop() {
if (this.colorPickerDrop && this.colorPickerDrop.tether) {
this.colorPickerDrop.destroy();
this.colorPickerDrop = null;
}
}
render() {
const { optionalClass, children } = this.props;
return (
<div className="graph-legend-popover">
{this.props.series.yaxis && this.renderAxisSelection()}
<ColorPickerPopover color={this.props.series.color} onColorSelect={this.onColorChange} />
<div className={optionalClass} ref={e => (this.pickerElem = e)} onClick={this.onClickToOpen}>
{children}
</div>
);
}
}
react2AngularDirective('seriesColorPicker', SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']);
@@ -0,0 +1,70 @@
import React from 'react';
import { ColorPickerPopover } from './ColorPickerPopover';
import { react2AngularDirective } from 'app/core/utils/react2angular';
export interface SeriesColorPickerPopoverProps {
color: string;
yaxis?: number;
onColorChange: (color: string) => void;
onToggleAxis?: () => void;
}
export class SeriesColorPickerPopover extends React.PureComponent<SeriesColorPickerPopoverProps, any> {
render() {
return (
<div className="graph-legend-popover">
{this.props.yaxis && <AxisSelector yaxis={this.props.yaxis} onToggleAxis={this.props.onToggleAxis} />}
<ColorPickerPopover color={this.props.color} onColorSelect={this.props.onColorChange} />
</div>
);
}
}
interface AxisSelectorProps {
yaxis: number;
onToggleAxis: () => void;
}
interface AxisSelectorState {
yaxis: number;
}
export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
constructor(props) {
super(props);
this.state = {
yaxis: this.props.yaxis,
};
this.onToggleAxis = this.onToggleAxis.bind(this);
}
onToggleAxis() {
this.setState({
yaxis: this.state.yaxis === 2 ? 1 : 2,
});
this.props.onToggleAxis();
}
render() {
const leftButtonClass = this.state.yaxis === 1 ? 'btn-success' : 'btn-inverse';
const rightButtonClass = this.state.yaxis === 2 ? 'btn-success' : 'btn-inverse';
return (
<div className="p-b-1">
<label className="small p-r-1">Y Axis:</label>
<button onClick={this.onToggleAxis} className={'btn btn-small ' + leftButtonClass}>
Left
</button>
<button onClick={this.onToggleAxis} className={'btn btn-small ' + rightButtonClass}>
Right
</button>
</div>
);
}
}
react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [
'series',
'onColorChange',
'onToggleAxis',
]);
@@ -88,7 +88,7 @@ export class FormDropdownCtrl {
if (evt.keyCode === 13) {
setTimeout(() => {
this.inputElement.blur();
}, 100);
}, 300);
}
});
+46
View File
@@ -0,0 +1,46 @@
import { AppNotification, AppNotificationSeverity, AppNotificationTimeout } from 'app/types';
const defaultSuccessNotification: AppNotification = {
title: '',
text: '',
severity: AppNotificationSeverity.Success,
icon: 'fa fa-check',
timeout: AppNotificationTimeout.Success,
};
const defaultWarningNotification: AppNotification = {
title: '',
text: '',
severity: AppNotificationSeverity.Warning,
icon: 'fa fa-exclamation',
timeout: AppNotificationTimeout.Warning,
};
const defaultErrorNotification: AppNotification = {
title: '',
text: '',
severity: AppNotificationSeverity.Error,
icon: 'fa fa-exclamation-triangle',
timeout: AppNotificationTimeout.Error,
};
export const createSuccessNotification = (title: string, text?: string): AppNotification => ({
...defaultSuccessNotification,
title: title,
text: text,
id: Date.now(),
});
export const createErrorNotification = (title: string, text?: string): AppNotification => ({
...defaultErrorNotification,
title: title,
text: text,
id: Date.now(),
});
export const createWarningNotification = (title: string, text?: string): AppNotification => ({
...defaultWarningNotification,
title: title,
text: text,
id: Date.now(),
});
+1 -1
View File
@@ -14,7 +14,7 @@ import './components/jsontree/jsontree';
import './components/code_editor/code_editor';
import './utils/outline';
import './components/colorpicker/ColorPicker';
import './components/colorpicker/SeriesColorPicker';
import './components/colorpicker/SeriesColorPickerPopover';
import './components/colorpicker/spectrum_picker';
import './services/search_srv';
import './services/ng_react';
+14
View File
@@ -1,3 +1,5 @@
import _ from 'lodash';
export enum LogLevel {
crit = 'crit',
warn = 'warn',
@@ -27,3 +29,15 @@ export interface LogRow {
export interface LogsModel {
rows: LogRow[];
}
export function mergeStreams(streams: LogsModel[], limit?: number): LogsModel {
const combinedEntries = streams.reduce((acc, stream) => {
return [...acc, ...stream.rows];
}, []);
const sortedEntries = _.chain(combinedEntries)
.sortBy('timestamp')
.reverse()
.slice(0, limit || combinedEntries.length)
.value();
return { rows: sortedEntries };
}
@@ -0,0 +1,51 @@
import { appNotificationsReducer } from './appNotification';
import { ActionTypes } from '../actions/appNotification';
import { AppNotificationSeverity, AppNotificationTimeout } from 'app/types/';
describe('clear alert', () => {
it('should filter alert', () => {
const id1 = 1540301236048;
const id2 = 1540301248293;
const initialState = {
appNotifications: [
{
id: id1,
severity: AppNotificationSeverity.Success,
icon: 'success',
title: 'test',
text: 'test alert',
timeout: AppNotificationTimeout.Success,
},
{
id: id2,
severity: AppNotificationSeverity.Warning,
icon: 'warning',
title: 'test2',
text: 'test alert fail 2',
timeout: AppNotificationTimeout.Warning,
},
],
};
const result = appNotificationsReducer(initialState, {
type: ActionTypes.ClearAppNotification,
payload: id2,
});
const expectedResult = {
appNotifications: [
{
id: id1,
severity: AppNotificationSeverity.Success,
icon: 'success',
title: 'test',
text: 'test alert',
timeout: AppNotificationTimeout.Success,
},
],
};
expect(result).toEqual(expectedResult);
});
});
@@ -0,0 +1,19 @@
import { AppNotification, AppNotificationsState } from 'app/types/';
import { Action, ActionTypes } from '../actions/appNotification';
export const initialState: AppNotificationsState = {
appNotifications: [] as AppNotification[],
};
export const appNotificationsReducer = (state = initialState, action: Action): AppNotificationsState => {
switch (action.type) {
case ActionTypes.AddAppNotification:
return { ...state, appNotifications: state.appNotifications.concat([action.payload]) };
case ActionTypes.ClearAppNotification:
return {
...state,
appNotifications: state.appNotifications.filter(appNotification => appNotification.id !== action.payload),
};
}
return state;
};
+4
View File
@@ -1,7 +1,11 @@
import { navIndexReducer as navIndex } from './navModel';
import { locationReducer as location } from './location';
import { appNotificationsReducer as appNotifications } from './appNotification';
import { userReducer as user } from './user';
export default {
navIndex,
location,
appNotifications,
user,
};
+15
View File
@@ -0,0 +1,15 @@
import { DashboardSearchHit, UserState } from '../../types';
import { Action, ActionTypes } from '../actions/user';
const initialState: UserState = {
starredDashboards: [] as DashboardSearchHit[],
};
export const userReducer = (state: UserState = initialState, action: Action): UserState => {
switch (action.type) {
case ActionTypes.LoadStarredDashboards:
return { ...state, starredDashboards: action.payload };
}
return state;
};
@@ -4,6 +4,7 @@ import _ from 'lodash';
export interface AngularComponent {
destroy();
digest();
}
export class AngularLoader {
@@ -24,6 +25,9 @@ export class AngularLoader {
scope.$destroy();
compiledElem.remove();
},
digest: () => {
scope.$digest();
},
};
}
}
+4 -92
View File
@@ -1,100 +1,12 @@
import angular from 'angular';
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
export class AlertSrv {
list: any[];
constructor() {}
/** @ngInject */
constructor(private $timeout, private $rootScope) {
this.list = [];
}
init() {
this.$rootScope.onAppEvent(
'alert-error',
(e, alert) => {
this.set(alert[0], alert[1], 'error', 12000);
},
this.$rootScope
);
this.$rootScope.onAppEvent(
'alert-warning',
(e, alert) => {
this.set(alert[0], alert[1], 'warning', 5000);
},
this.$rootScope
);
this.$rootScope.onAppEvent(
'alert-success',
(e, alert) => {
this.set(alert[0], alert[1], 'success', 3000);
},
this.$rootScope
);
appEvents.on('alert-warning', options => this.set(options[0], options[1], 'warning', 5000));
appEvents.on('alert-success', options => this.set(options[0], options[1], 'success', 3000));
appEvents.on('alert-error', options => this.set(options[0], options[1], 'error', 7000));
}
getIconForSeverity(severity) {
switch (severity) {
case 'success':
return 'fa fa-check';
case 'error':
return 'fa fa-exclamation-triangle';
default:
return 'fa fa-exclamation';
}
}
set(title, text, severity, timeout) {
if (_.isObject(text)) {
console.log('alert error', text);
if (text.statusText) {
text = `HTTP Error (${text.status}) ${text.statusText}`;
}
}
const newAlert = {
title: title || '',
text: text || '',
severity: severity || 'info',
icon: this.getIconForSeverity(severity),
};
const newAlertJson = angular.toJson(newAlert);
// remove same alert if it already exists
_.remove(this.list, value => {
return angular.toJson(value) === newAlertJson;
});
this.list.push(newAlert);
if (timeout > 0) {
this.$timeout(() => {
this.list = _.without(this.list, newAlert);
}, timeout);
}
if (!this.$rootScope.$$phase) {
this.$rootScope.$digest();
}
return newAlert;
}
clear(alert) {
this.list = _.without(this.list, alert);
}
clearAll() {
this.list = [];
set() {
console.log('old depricated alert srv being used');
}
}
// this is just added to not break old plugins that might be using it
coreModule.service('alertSrv', AlertSrv);
+7 -6
View File
@@ -9,7 +9,7 @@ export class BackendSrv {
private noBackendCache: boolean;
/** @ngInject */
constructor(private $http, private alertSrv, private $q, private $timeout, private contextSrv) {}
constructor(private $http, private $q, private $timeout, private contextSrv) {}
get(url, params?) {
return this.request({ method: 'GET', url: url, params: params });
@@ -49,14 +49,14 @@ export class BackendSrv {
}
if (err.status === 422) {
this.alertSrv.set('Validation failed', data.message, 'warning', 4000);
appEvents.emit('alert-warning', ['Validation failed', data.message]);
throw data;
}
data.severity = 'error';
let severity = 'error';
if (err.status < 500) {
data.severity = 'warning';
severity = 'warning';
}
if (data.message) {
@@ -66,7 +66,8 @@ export class BackendSrv {
description = message;
message = 'Error';
}
this.alertSrv.set(message, description, data.severity, 10000);
appEvents.emit('alert-' + severity, [message, description]);
}
throw data;
@@ -93,7 +94,7 @@ export class BackendSrv {
if (options.method !== 'GET') {
if (results && results.data.message) {
if (options.showSuccessAlert !== false) {
this.alertSrv.set(results.data.message, '', 'success', 3000);
appEvents.emit('alert-success', [results.data.message]);
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import { store } from 'app/store/configureStore';
import { store } from 'app/store/store';
import locationUtil from 'app/core/utils/location_util';
import { updateLocation } from 'app/core/actions';
+1 -1
View File
@@ -9,7 +9,7 @@ describe('backend_srv', () => {
return Promise.resolve({});
};
const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {});
const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {});
describe('when handling errors', () => {
it('should return the http status code', async () => {
+4
View File
@@ -83,6 +83,10 @@ function areRowsMatching(columns, row, otherRow) {
export function mergeTablesIntoModel(dst?: TableModel, ...tables: TableModel[]): TableModel {
const model = dst || new TableModel();
if (arguments.length === 1) {
return model;
}
// Single query returns data columns and rows as is
if (arguments.length === 2) {
model.columns = [...tables[0].columns];
@@ -0,0 +1,11 @@
import React from 'react';
import { connect } from 'react-redux';
import { store } from '../../store/store';
export function connectWithStore(WrappedComponent, ...args) {
const ConnectedWrappedComponent = connect(...args)(WrappedComponent);
return props => {
return <ConnectedWrappedComponent {...props} store={store} />;
};
}
+4 -1
View File
@@ -1,5 +1,8 @@
import _ from 'lodash';
import moment from 'moment';
import { RawTimeRange } from 'app/types/series';
import * as dateMath from './datemath';
const spans = {
@@ -129,7 +132,7 @@ export function describeTextRange(expr: any) {
return opt;
}
export function describeTimeRange(range) {
export function describeTimeRange(range: RawTimeRange): string {
const option = rangeIndex[range.from.toString() + ' to ' + range.to.toString()];
if (option) {
return option.display;
+2 -2
View File
@@ -166,7 +166,7 @@ export class AlertTabCtrl {
alert.noDataState = alert.noDataState || config.alertingNoDataOrNullValues;
alert.executionErrorState = alert.executionErrorState || config.alertingErrorOrTimeout;
alert.frequency = alert.frequency || '60s';
alert.frequency = alert.frequency || '1m';
alert.handler = alert.handler || 1;
alert.notifications = alert.notifications || [];
@@ -217,7 +217,7 @@ export class AlertTabCtrl {
buildDefaultCondition() {
return {
type: 'query',
query: { params: ['A', '5m', 'now'] },
query: { params: ['A', '15m', 'now'] },
reducer: { type: 'avg', params: [] },
evaluator: { type: 'gt', params: [null] },
operator: { type: 'and' },
+3
View File
@@ -9,3 +9,6 @@ import './admin';
import './alerting/NotificationsEditCtrl';
import './alerting/NotificationsListCtrl';
import './manage-dashboards';
import './teams/CreateTeamCtrl';
import './profile/ProfileCtrl';
import './profile/ChangePasswordCtrl';
@@ -1,25 +1,32 @@
import './editor_ctrl';
// Libaries
import angular from 'angular';
import _ from 'lodash';
// Components
import './editor_ctrl';
import coreModule from 'app/core/core_module';
// Utils & Services
import { makeRegions, dedupAnnotations } from './events_processing';
// Types
import { DashboardModel } from '../dashboard/dashboard_model';
export class AnnotationsSrv {
globalAnnotationsPromise: any;
alertStatesPromise: any;
datasourcePromises: any;
/** @ngInject */
constructor(private $rootScope, private $q, private datasourceSrv, private backendSrv, private timeSrv) {
$rootScope.onAppEvent('refresh', this.clearCache.bind(this), $rootScope);
$rootScope.onAppEvent('dashboard-initialized', this.clearCache.bind(this), $rootScope);
}
constructor(private $rootScope, private $q, private datasourceSrv, private backendSrv, private timeSrv) {}
clearCache() {
this.globalAnnotationsPromise = null;
this.alertStatesPromise = null;
this.datasourcePromises = null;
init(dashboard: DashboardModel) {
// clear promises on refresh events
dashboard.on('refresh', () => {
this.globalAnnotationsPromise = null;
this.alertStatesPromise = null;
this.datasourcePromises = null;
});
}
getAnnotations(options) {

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