Compare commits

..

41 Commits

Author SHA1 Message Date
Arve Knudsen
f77a692de5 Release 6.3.7
Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
2019-11-22 15:02:03 +01:00
Marcus Efraimsson
d433e21275 CloudWatch: Fix high CPU load (#20579)
* Cache decrypted securejsondata
* Models: Add datasource cache tests
2019-11-22 15:02:03 +01:00
Chiang Fong Lee
ede3adc7d4 Build: Fix Dockerfile glibc version error (#19394)
Change the base image for the golang build stage from 
golang:1.12.9 (Debian 'Buster' 10) to golang:1.12.9-stretch 
(Debian 'Stretch' 9), so that the glibc version in golang build 
stage matches the glibc version in the final container based 
on ubuntu:18.04. Otherwise, the built binaries cannot execute 
in the final container.
2019-09-25 13:24:21 +02:00
Sofia Papagiannaki
fdd211758e SQL: Rewrite statistics query (#19178)
* Rewrite statistics query

(cherry picked from commit 56f5106717)
2019-09-23 11:14:34 +03:00
Sofia Papagiannaki
5d6512a7a2 Datas to Data 2019-09-23 11:14:34 +03:00
Sofia Papagiannaki
b467557614 release 6.3.6 2019-09-23 11:14:34 +03:00
Marcus Efraimsson
2148a9ff6e Metrics: Adds setting for turning off total stats metrics (#19142)
Don't update total stats metrics if reporting is disabled.
New setting disable_total_stats for turning off update 
of total stats (stat_totals_*) metrics.

Ref #19137

(cherry picked from commit 80592e3361)
2019-09-23 11:14:34 +03:00
kay delaney
dc6219d8e0 Explore: Fixes error when switching from prometheus to loki data sources (#18599)
Closes #18594
Closes #18596

(cherry picked from commit bf82e6cded)
2019-09-23 11:14:34 +03:00
Marcus Olsson
67bad726f1 Build: Update ua-parser/uap-go (#18788)
(cherry picked from commit 5bb15cf3e2)
2019-09-02 11:34:49 +02:00
Marcus Efraimsson
3f8624bffb Build: Use the latest build container which has go 1.12.9 (#18807)
(cherry picked from commit e111232324)
2019-09-02 11:34:49 +02:00
Marcus Olsson
7f1db70213 Build: Upgrade to go 1.12.9 (#18638)
* Build: Upgrade to go 1.12.9

* Build: Use default-mysql-client for debian buster

The go base image has been updated to use Debian Buster instead of
Stretch, which seems to have done away with mysql-client in favor of
default-mysql-client.

* Build: Update Dockerfile to use go 1.12.9

Fixes #18592

(cherry picked from commit 299a0e20f4)
2019-09-02 11:34:49 +02:00
kay delaney
b2d86c76c6 Editor: Fixes issue where only entire lines were being copied (#18806)
* Editor: Fixes issue where only entire lines were being copied
Closes #18768

* Simplifies onCopy handler and factors out logic for easier testing
Also adds tests to verify behaviour

(cherry picked from commit d6fb48c0ff)
2019-09-02 11:34:49 +02:00
kay delaney
8c168a6b83 Prometheus: Changes brace-insertion behavior to be less annoying (#18698)
* Changes brace-insertion behavior to be less annoying

* Removes use of braces plugin

* Revert "Removes use of braces plugin"

This reverts commit 4cf4a6073b.

(cherry picked from commit 3aa3a45372)
2019-09-02 11:34:49 +02:00
Torkel Ödegaard
f02d6c7be2 Updated version to v6.3.5 2019-09-02 11:34:49 +02:00
Torkel Ödegaard
496d0323bd Singlestat: Backport singlestat fix, fixes #18753 2019-09-02 11:34:49 +02:00
Torkel Ödegaard
f455f02318 DashboardMigrator: Fixed issue migrating incomplete panel link models (#18786)
(cherry picked from commit 65a6eda93b)
2019-09-02 11:34:49 +02:00
Hugo Häggmark
1e58fdaffd Explore: Fixes query field layout in splitted view for Safari browsers (#18654)
Fixes: #18436
(cherry picked from commit 6eb13ae555)
2019-09-02 11:34:49 +02:00
Hugo Häggmark
c27fd346d2 Prometheus: Prevents panel editor crash when switching to Prometheus datasource (#18616)
* Fix: Fixes panel editor crash when switching to Promehteus
Fixes: #18600

* Refactor: Adds tests

(cherry picked from commit d7ccf98b1b)
2019-09-02 11:34:49 +02:00
Oleg Gaidarenko
59fa8cc82e LDAP: multildap + ldap integration (#18588)
It seems `ldap` module introduced new error type of which
multildap module didn't know about.

This broke the multildap login logic

Fixes #18491
Ref #18587

(cherry picked from commit 02af966964)
2019-09-02 11:34:49 +02:00
Marcus Efraimsson
a557646484 Release v6.3.4 2019-08-19 16:28:51 +02:00
Marcus Efraimsson
be2e2330f5 Snapshot: Require authentication for snapshot api 2019-08-19 14:27:33 +02:00
Marcus Efraimsson
84d0a71b25 Release v6.3.3 2019-08-15 11:08:12 +02:00
Torkel Ödegaard
e0ee72a2ff Graph: Fixed issue clicking on series line icon (#18563)
(cherry picked from commit 8e92eecc19)
2019-08-15 11:08:12 +02:00
kay delaney
881c229ee3 Explore/Prometheus: More consistently allows for multi-line queries (#18362)
* Explore/Prometheus: More consistently allows for multi-line queries
Allows a user to hit shift+enter to create a new line in the query field, even
when the autocomplete suggestions are displayed.
Also fixes an issue where a new line was inserted when selecting a suggestion
Closes #18341

* Fixes behavior where query wasn't running on pressing Enter
Also adds test to verify this behavior

(cherry picked from commit d66601a5f5)
2019-08-15 11:08:12 +02:00
Ryan McKinley
9d97f48374 TimeSeries: assume values are all numbers (#18540)
* assume number for TimeSeries types

* use const

(cherry picked from commit 0ba07720df)
2019-08-15 11:08:12 +02:00
Dominik Prokop
39f00259f3 Annotations: Fix failing annotation query when time series query is cancelled (#18532)
(cherry picked from commit 993e5636d6)
2019-08-15 11:08:12 +02:00
David
84022650cb Explore: Fix loading error for empty queries (#18488)
* Explore: Fix loading error for empty queries

* Explore: Render tests for QueryField

(cherry picked from commit b3d2cc3e2f)
2019-08-15 11:08:12 +02:00
Hugo Häggmark
e368080dea Fix: Fixes stripping of $d in Explore urls (#18480)
Fixes: #18455
(cherry picked from commit 445f1dabcc)
2019-08-15 11:08:12 +02:00
Dominik Prokop
a02c2b21d2 DataLinks: respect timezone when displaying datapoint's timestamp in graph context menu (#18461)
(cherry picked from commit 81c42fc912)
2019-08-15 11:08:12 +02:00
Sofia Papagiannaki
3a58974314 Backend: Do not set SameSite cookie attribute if cookie_samesite is none (#18462)
* Do not set SameSite login_error cookie attribute if cookie_samesite is none

* Do not set SameSite grafana_session cookie attribute if cookie_samesite is none

* Update middleware tests

(cherry picked from commit 4e29357d15)
2019-08-15 11:08:12 +02:00
Dominik Prokop
5954cb7220 DataLinks: Apply scoped variables correctly (#18454)
(cherry picked from commit b6ec06eeb4)
2019-08-15 11:08:12 +02:00
Dominik Prokop
f24ef80e52 DataLinks: Use datapoint timestamp correctly when interpolating variables (#18459)
(cherry picked from commit 20d0c07359)
2019-08-15 11:08:12 +02:00
gotjosh
917b278e45 Fix: Avoid glob of single-value array variables (#18420)
* Fix: Avoid glob of single-value array variables

Based on our current implementation of templates, when multi-select
variables are part of a dashboard query the default/fallback formatting option is `glob`.

Some data sources do not support glob (e.g. metrics.{a}.* instead of
metrics.a.*) for single variable queries. This behaviour breaks dashboards.

This commit introduces an alternative formatting option where globing is avoided if it's there is only one value as part of the query variable.

This means, queries previously formatted as `query=metrics.{a}.*.*`, are
now formatted as `query=metrics.a.*.*`. However, queries formatted as
`query=metrics.{a,b}.*.*` continue to be as is.
(cherry picked from commit b424e12a5a)
2019-08-15 11:08:12 +02:00
Torkel Ödegaard
483246016b Updated version to 6.3.2 2019-08-07 11:49:41 +02:00
Torkel Ödegaard
43fe057baa Gauge/BarGauge: Rewrite of how migrations are applied (#18375)
(cherry picked from commit 541981c341)
2019-08-07 11:48:26 +02:00
Torkel Ödegaard
f2fffadcd6 Panels: Fixed crashing dashboards with panel links (#18430)
* ReactPanels: Fixed panel header tooltip rendering crash

* Added unit test

* Improved test

(cherry picked from commit c55578d303)
2019-08-07 10:50:57 +02:00
Torkel Ödegaard
de06c1c1b8 Updated version to 6.3.1 2019-08-07 10:49:26 +02:00
Sofia Papagiannaki
830da0fda0 release 6.3.0 2019-08-06 15:49:28 +03:00
Tobias Skarhed
78fff0161a PanelLinks: Fix render issue when there is no panel description (#18408)
Make empty string if there is no panel description

(cherry picked from commit 1f9bce7f9f)
2019-08-06 15:49:28 +03:00
Sofia Papagiannaki
06d4641a8f Do not set SameSite for OAuth cookie if cookie_samesite is None (#18392)
(cherry picked from commit 269c1fb107)
2019-08-06 15:49:28 +03:00
Torkel Ödegaard
e232629917 FieldDisplay: Return field defaults when there are no data (#18357)
(cherry picked from commit 202c136238)
2019-08-06 15:49:28 +03:00
77 changed files with 922 additions and 583 deletions

View File

@@ -19,7 +19,7 @@ version: 2
jobs:
mysql-integration-test:
docker:
- image: circleci/golang:1.12.6
- image: circleci/golang:1.12.9
- image: circleci/mysql:5.6-ram
environment:
MYSQL_ROOT_PASSWORD: rootpass
@@ -30,7 +30,7 @@ jobs:
steps:
- checkout
- run: sudo apt update
- run: sudo apt install -y mysql-client
- run: sudo apt install -y default-mysql-client
- run: dockerize -wait tcp://127.0.0.1:3306 -timeout 120s
- run: cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h 127.0.0.1 -P 3306 -u root -prootpass
- run:
@@ -39,7 +39,7 @@ jobs:
postgres-integration-test:
docker:
- image: circleci/golang:1.12.6
- image: circleci/golang:1.12.9
- image: circleci/postgres:9.3-ram
environment:
POSTGRES_USER: grafanatest
@@ -58,7 +58,7 @@ jobs:
cache-server-test:
docker:
- image: circleci/golang:1.12.6
- image: circleci/golang:1.12.9
- image: circleci/redis:4-alpine
- image: memcached
working_directory: /go/src/github.com/grafana/grafana
@@ -144,7 +144,7 @@ jobs:
lint-go:
docker:
- image: circleci/golang:1.12.6
- image: circleci/golang:1.12.9
environment:
# we need CGO because of go-sqlite3
CGO_ENABLED: 1
@@ -185,7 +185,7 @@ jobs:
test-backend:
docker:
- image: circleci/golang:1.12.6
- image: circleci/golang:1.12.9
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -195,7 +195,7 @@ jobs:
build-all:
docker:
- image: grafana/build-container:1.2.7
- image: grafana/build-container:1.2.8
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -239,7 +239,7 @@ jobs:
build:
docker:
- image: grafana/build-container:1.2.7
- image: grafana/build-container:1.2.8
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -265,7 +265,7 @@ jobs:
build-fast-backend:
docker:
- image: grafana/build-container:1.2.7
- image: grafana/build-container:1.2.8
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -282,7 +282,7 @@ jobs:
build-fast-frontend:
docker:
- image: grafana/build-container:1.2.7
- image: grafana/build-container:1.2.8
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -306,7 +306,7 @@ jobs:
build-fast-package:
docker:
- image: grafana/build-container:1.2.7
- image: grafana/build-container:1.2.8
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -333,7 +333,7 @@ jobs:
build-fast-save:
docker:
- image: grafana/build-container:1.2.7
- image: grafana/build-container:1.2.8
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -419,7 +419,7 @@ jobs:
build-enterprise:
docker:
- image: grafana/build-container:1.2.7
- image: grafana/build-container:1.2.8
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -451,7 +451,7 @@ jobs:
build-all-enterprise:
docker:
- image: grafana/build-container:1.2.7
- image: grafana/build-container:1.2.8
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout

View File

@@ -1,5 +1,5 @@
# Golang build container
FROM golang:1.12.4
FROM golang:1.12.9-stretch
WORKDIR $GOPATH/src/github.com/grafana/grafana

View File

@@ -214,6 +214,10 @@ external_enabled = true
external_snapshot_url = https://snapshots-origin.raintank.io
external_snapshot_name = Publish to snapshot.raintank.io
# Set to true to enable this Grafana instance act as an external snapshot server and allow unauthenticated requests for
# creating and deleting snapshots.
public_mode = false
# remove expired snapshot
snapshot_remove_expired = true
@@ -588,8 +592,10 @@ enabled = true
#################################### Internal Grafana Metrics ############
# Metrics available at HTTP API Url /metrics
[metrics]
enabled = true
interval_seconds = 10
enabled = true
interval_seconds = 10
# Disable total stats (stat_totals_*) metrics to be generated
disable_total_stats = false
#If both are set, basic auth will be required for the metrics endpoint.
basic_auth_username =

View File

@@ -209,6 +209,10 @@
;external_snapshot_url = https://snapshots-origin.raintank.io
;external_snapshot_name = Publish to snapshot.raintank.io
# Set to true to enable this Grafana instance act as an external snapshot server and allow unauthenticated requests for
# creating and deleting snapshots.
;public_mode = false
# remove expired snapshot
;snapshot_remove_expired = true
@@ -520,6 +524,8 @@
[metrics]
# Disable / Enable internal metrics
;enabled = true
# Disable total stats (stat_totals_*) metrics to be generated
;disable_total_stats = false
# Publish interval
;interval_seconds = 10

View File

@@ -44,7 +44,7 @@
"nullPointMode": "null",
"options-gauge": {
"baseColor": "#299c46",
"decimals": "2",
"decimals": 2,
"maxValue": 100,
"minValue": 0,
"options": {
@@ -111,7 +111,7 @@
"nullPointMode": "null",
"options-gauge": {
"baseColor": "#299c46",
"decimals": "",
"decimals": null,
"maxValue": 100,
"minValue": 0,
"options": {
@@ -178,7 +178,7 @@
"nullPointMode": "null",
"options-gauge": {
"baseColor": "#299c46",
"decimals": "",
"decimals": null,
"maxValue": 100,
"minValue": 0,
"options": {

View File

@@ -56,7 +56,7 @@ More information [here](https://community.grafana.com/t/using-grafanas-query-ins
This option is now renamed (and moved to Options sub section above your queries):
![image|519x120](upload://ySjHOVpavV6yk9LHQxL9nq2HIsT.png)
Datas source selection & options & help are now above your metric queries.
Data source selection & options & help are now above your metric queries.
![image|690x179](upload://5kNDxKgMz1BycOKgG3iWYLsEVXv.png)
### Minor Changes

View File

@@ -533,6 +533,9 @@ If set configures the username to use for basic authentication on the metrics en
### basic_auth_password
If set configures the password to use for basic authentication on the metrics endpoint.
### disable_total_stats
If set to `true`, then total stats generation (`stat_totals_*` metrics) is disabled. The default is `false`.
### interval_seconds
Flush/Write interval when sending metrics to external TSDB. Defaults to 10s.

4
go.mod
View File

@@ -52,7 +52,7 @@ require (
github.com/onsi/gomega v1.5.0 // indirect
github.com/opentracing/opentracing-go v1.1.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.8.1
github.com/pkg/errors v0.8.1 // indirect
github.com/prometheus/client_golang v0.9.2
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90
github.com/prometheus/common v0.2.0
@@ -64,7 +64,7 @@ require (
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a
github.com/stretchr/testify v1.3.0
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf
github.com/ua-parser/uap-go v0.0.0-20190303233514-1004ccd816b3
github.com/ua-parser/uap-go v0.0.0-20190826212731-daf92ba38329
github.com/uber-go/atomic v1.3.2 // indirect
github.com/uber/jaeger-client-go v2.16.0+incompatible
github.com/uber/jaeger-lib v2.0.0+incompatible // indirect

4
go.sum
View File

@@ -202,8 +202,8 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA=
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
github.com/ua-parser/uap-go v0.0.0-20190303233514-1004ccd816b3 h1:E7xa7Zur8hLPvw+03gAeQ9esrglfV389j2PcwhiGf/I=
github.com/ua-parser/uap-go v0.0.0-20190303233514-1004ccd816b3/go.mod h1:OBcG9bn7sHtXgarhUEb3OfCnNsgtGnkVf41ilSZ3K3E=
github.com/ua-parser/uap-go v0.0.0-20190826212731-daf92ba38329 h1:VBsKFh4W1JEMz3eLCmM9zOJKZdDkP5W4b3Y4hc7SbZc=
github.com/ua-parser/uap-go v0.0.0-20190826212731-daf92ba38329/go.mod h1:OBcG9bn7sHtXgarhUEb3OfCnNsgtGnkVf41ilSZ3K3E=
github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo=
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
github.com/uber/jaeger-client-go v2.16.0+incompatible h1:Q2Pp6v3QYiocMxomCaJuwQGFt7E53bPYqEgug/AoBtY=

View File

@@ -5,7 +5,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
"version": "6.3.0-beta4",
"version": "6.3.7",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"

View File

@@ -104,7 +104,7 @@ export const fieldReducers = new Registry<FieldReducerInfo>(() => [
name: 'Last (not null)',
description: 'Last non-null value',
standard: true,
alias: 'current',
aliasIds: ['current'],
reduce: calculateLastNotNull,
},
{
@@ -124,14 +124,14 @@ export const fieldReducers = new Registry<FieldReducerInfo>(() => [
},
{ id: ReducerID.min, name: 'Min', description: 'Minimum Value', standard: true },
{ id: ReducerID.max, name: 'Max', description: 'Maximum Value', standard: true },
{ id: ReducerID.mean, name: 'Mean', description: 'Average Value', standard: true, alias: 'avg' },
{ id: ReducerID.mean, name: 'Mean', description: 'Average Value', standard: true, aliasIds: ['avg'] },
{
id: ReducerID.sum,
name: 'Total',
description: 'The sum of all values',
emptyInputResult: 0,
standard: true,
alias: 'total',
aliasIds: ['total'],
},
{
id: ReducerID.count,

View File

@@ -29,6 +29,15 @@ describe('toDataFrame', () => {
expect(series.fields[0].name).toEqual('Value');
});
it('assumes TimeSeries values are numbers', () => {
const input1 = {
target: 'time',
datapoints: [[100, 1], [200, 2]],
};
const data = toDataFrame(input1);
expect(data.fields[0].type).toBe(FieldType.number);
});
it('keeps dataFrame unchanged', () => {
const input = {
fields: [{ text: 'A' }, { text: 'B' }, { text: 'C' }],

View File

@@ -29,6 +29,7 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
fields: [
{
name: timeSeries.target || 'Value',
type: FieldType.number,
unit: timeSeries.unit,
},
{

View File

@@ -77,7 +77,7 @@ export class CustomScrollbar extends Component<Props> {
{...passedProps}
className={cx(
css`
visibility: ${hideTrack ? 'none' : 'visible'};
visibility: ${hideTrack ? 'hidden' : 'visible'};
`,
track
)}

View File

@@ -0,0 +1,39 @@
import { sharedSingleStatMigrationCheck } from './SingleStatBaseOptions';
describe('sharedSingleStatMigrationCheck', () => {
it('from old valueOptions model without pluginVersion', () => {
const panel = {
options: {
valueOptions: {
unit: 'watt',
stat: 'last',
decimals: 5,
},
minValue: 10,
maxValue: 100,
valueMappings: [{ type: 1, value: '1', text: 'OK' }],
thresholds: [
{
color: 'green',
index: 0,
value: null,
},
{
color: 'orange',
index: 1,
value: 40,
},
{
color: 'red',
index: 2,
value: 80,
},
],
},
title: 'Usage',
type: 'bargauge',
};
expect(sharedSingleStatMigrationCheck(panel as any)).toMatchSnapshot();
});
});

View File

@@ -3,7 +3,7 @@ import omit from 'lodash/omit';
import { VizOrientation, PanelModel } from '../../types/panel';
import { FieldDisplayOptions } from '../../utils/fieldDisplay';
import { Field, fieldReducers, Threshold, sortThresholds } from '@grafana/data';
import { fieldReducers, Threshold, sortThresholds } from '@grafana/data';
export interface SingleStatBaseOptions {
fieldOptions: FieldDisplayOptions;
@@ -25,54 +25,86 @@ export const sharedSingleStatOptionsCheck = (
return options;
};
export const sharedSingleStatMigrationCheck = (panel: PanelModel<SingleStatBaseOptions>) => {
export function sharedSingleStatMigrationCheck(panel: PanelModel<SingleStatBaseOptions>) {
if (!panel.options) {
// This happens on the first load or when migrating from angular
return {};
}
// This migration aims to keep the most recent changes up-to-date
// Plugins should explicitly migrate for known version changes and only use this
// as a backup
const old = panel.options as any;
if (old.valueOptions) {
const { valueOptions } = old;
const previousVersion = parseFloat(panel.pluginVersion || '6.1');
let options = panel.options as any;
const fieldOptions = (old.fieldOptions = {} as FieldDisplayOptions);
if (previousVersion < 6.2) {
options = migrateFromValueOptions(options);
}
const field = (fieldOptions.defaults = {} as Field);
field.mappings = old.valueMappings;
field.thresholds = migrateOldThresholds(old.thresholds);
field.unit = valueOptions.unit;
field.decimals = valueOptions.decimals;
if (previousVersion < 6.3) {
options = moveThresholdsAndMappingsToField(options);
}
// Make sure the stats have a valid name
if (valueOptions.stat) {
const reducer = fieldReducers.get(valueOptions.stat);
if (reducer) {
fieldOptions.calcs = [reducer.id];
}
}
return options as SingleStatBaseOptions;
}
field.min = old.minValue;
field.max = old.maxValue;
export function moveThresholdsAndMappingsToField(old: any) {
const { fieldOptions } = old;
// remove old props
return omit(old, 'valueMappings', 'thresholds', 'valueOptions', 'minValue', 'maxValue');
} else if (old.fieldOptions) {
// Move mappins & thresholds to field defautls (6.4+)
const { mappings, thresholds, ...fieldOptions } = old.fieldOptions;
fieldOptions.defaults = {
mappings,
thresholds: migrateOldThresholds(thresholds),
...fieldOptions.defaults,
};
old.fieldOptions = fieldOptions;
if (!fieldOptions) {
return old;
}
return panel.options;
};
const { mappings, thresholds, ...rest } = old.fieldOptions;
return {
...old,
fieldOptions: {
...rest,
defaults: {
...fieldOptions.defaults,
mappings,
thresholds: migrateOldThresholds(thresholds),
},
},
};
}
/*
* Moves valueMappings and thresholds from root to new fieldOptions object
* Renames valueOptions to to defaults and moves it under fieldOptions
*/
export function migrateFromValueOptions(old: any) {
const { valueOptions } = old;
if (!valueOptions) {
return old;
}
const fieldOptions: any = {};
const fieldDefaults: any = {};
fieldOptions.mappings = old.valueMappings;
fieldOptions.thresholds = old.thresholds;
fieldOptions.defaults = fieldDefaults;
fieldDefaults.unit = valueOptions.unit;
fieldDefaults.decimals = valueOptions.decimals;
// Make sure the stats have a valid name
if (valueOptions.stat) {
const reducer = fieldReducers.get(valueOptions.stat);
if (reducer) {
fieldOptions.calcs = [reducer.id];
}
}
fieldDefaults.min = old.minValue;
fieldDefaults.max = old.maxValue;
const newOptions = {
...old,
fieldOptions,
};
return omit(newOptions, 'valueMappings', 'thresholds', 'valueOptions', 'minValue', 'maxValue');
}
export function migrateOldThresholds(thresholds?: any[]): Threshold[] | undefined {
if (!thresholds || !thresholds.length) {

View File

@@ -0,0 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sharedSingleStatMigrationCheck from old valueOptions model without pluginVersion 1`] = `
Object {
"fieldOptions": Object {
"calcs": Array [
"last",
],
"defaults": Object {
"decimals": 5,
"mappings": Array [
Object {
"text": "OK",
"type": 1,
"value": "1",
},
],
"max": 100,
"min": 10,
"thresholds": Array [
Object {
"color": "green",
"value": -Infinity,
},
Object {
"color": "orange",
"value": 40,
},
Object {
"color": "red",
"value": 80,
},
],
"unit": "watt",
},
},
}
`;

View File

@@ -1,6 +1,6 @@
export { DeleteButton } from './DeleteButton/DeleteButton';
export { Tooltip } from './Tooltip/Tooltip';
export { PopperController } from './Tooltip/PopperController';
export { PopperController, PopperContent } from './Tooltip/PopperController';
export { Popper } from './Tooltip/Popper';
export { Portal } from './Portal/Portal';
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';

View File

@@ -135,4 +135,30 @@ describe('FieldDisplay', () => {
expect(field.thresholds!.length).toEqual(2);
expect(field.thresholds![0].value).toBe(-Infinity);
});
it('Should return field thresholds when there is no data', () => {
const options: GetFieldDisplayValuesOptions = {
data: [
{
name: 'No data',
fields: [],
rows: [],
},
],
replaceVariables: (value: string) => {
return value;
},
fieldOptions: {
calcs: [],
override: {},
defaults: {
thresholds: [{ color: '#F2495C', value: 50 }],
},
},
theme: getTheme(GrafanaThemeType.Dark),
};
const display = getFieldDisplayValues(options);
expect(display[0].field.thresholds!.length).toEqual(1);
});
});

View File

@@ -182,7 +182,10 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
if (values.length === 0) {
values.push({
field: { name: 'No Data' },
field: {
...defaults,
name: 'No Data',
},
display: {
numeric: 0,
text: 'No data',
@@ -244,6 +247,7 @@ type PartialField = Partial<Field>;
export function getFieldProperties(...props: PartialField[]): Field {
let field = props[0] as Field;
for (let i = 1; i < props.length; i++) {
field = applyFieldProperties(field, props[i]);
}

View File

@@ -15,6 +15,7 @@ func (hs *HTTPServer) registerRoutes() {
reqEditorRole := middleware.ReqEditorRole
reqOrgAdmin := middleware.ReqOrgAdmin
reqCanAccessTeams := middleware.AdminOrFeatureEnabled(hs.Cfg.EditorsCanAdmin)
reqSnapshotPublicModeOrSignedIn := middleware.SnapshotPublicModeOrSignedIn()
redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL()
redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL()
quota := middleware.Quota(hs.QuotaService)
@@ -104,13 +105,6 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/dashboard/snapshot/*", hs.Index)
r.Get("/dashboard/snapshots/", reqSignedIn, hs.Index)
// api for dashboard snapshots
r.Post("/api/snapshots/", bind(models.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
r.Get("/api/snapshot/shared-options/", GetSharingOptions)
r.Get("/api/snapshots/:key", GetDashboardSnapshot)
r.Get("/api/snapshots-delete/:deleteKey", Wrap(DeleteDashboardSnapshotByDeleteKey))
r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot))
// api renew session based on cookie
r.Get("/api/login/ping", quota("session"), Wrap(hs.LoginAPIPing))
@@ -413,4 +407,11 @@ func (hs *HTTPServer) registerRoutes() {
// streams
//r.Post("/api/streams/push", reqSignedIn, bind(dtos.StreamMessage{}), liveConn.PushToStream)
// Snapshots
r.Post("/api/snapshots/", reqSnapshotPublicModeOrSignedIn, bind(models.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
r.Get("/api/snapshot/shared-options/", reqSignedIn, GetSharingOptions)
r.Get("/api/snapshots/:key", GetDashboardSnapshot)
r.Get("/api/snapshots-delete/:deleteKey", reqSnapshotPublicModeOrSignedIn, Wrap(DeleteDashboardSnapshotByDeleteKey))
r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot))
}

View File

@@ -199,15 +199,18 @@ func (hs *HTTPServer) trySetEncryptedCookie(ctx *models.ReqContext, cookieName s
return err
}
http.SetCookie(ctx.Resp, &http.Cookie{
cookie := http.Cookie{
Name: cookieName,
MaxAge: 60,
Value: hex.EncodeToString(encryptedError),
HttpOnly: true,
Path: setting.AppSubUrl + "/",
Secure: hs.Cfg.CookieSecure,
SameSite: hs.Cfg.CookieSameSite,
})
}
if hs.Cfg.CookieSameSite != http.SameSiteDefaultMode {
cookie.SameSite = hs.Cfg.CookieSameSite
}
http.SetCookie(ctx.Resp, &cookie)
return nil
}

View File

@@ -60,7 +60,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *m.ReqContext) {
if code == "" {
state := GenStateString()
hashedState := hashStatecode(state, setting.OAuthService.OAuthInfos[name].ClientSecret)
hs.writeCookie(ctx.Resp, OauthStateCookieName, hashedState, 60, http.SameSiteLaxMode)
hs.writeCookie(ctx.Resp, OauthStateCookieName, hashedState, 60, hs.Cfg.CookieSameSite)
if setting.OAuthService.OAuthInfos[name].HostedDomain == "" {
ctx.Redirect(connect.AuthCodeURL(state, oauth2.AccessTypeOnline))
} else {
@@ -73,7 +73,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *m.ReqContext) {
// delete cookie
ctx.Resp.Header().Del("Set-Cookie")
hs.deleteCookie(ctx.Resp, OauthStateCookieName, http.SameSiteLaxMode)
hs.deleteCookie(ctx.Resp, OauthStateCookieName, hs.Cfg.CookieSameSite)
if cookieState == "" {
ctx.Handle(500, "login.OAuthLogin(missing saved state)", nil)
@@ -218,15 +218,18 @@ func (hs *HTTPServer) deleteCookie(w http.ResponseWriter, name string, sameSite
}
func (hs *HTTPServer) writeCookie(w http.ResponseWriter, name string, value string, maxAge int, sameSite http.SameSite) {
http.SetCookie(w, &http.Cookie{
cookie := http.Cookie{
Name: name,
MaxAge: maxAge,
Value: value,
HttpOnly: true,
Path: setting.AppSubUrl + "/",
Secure: hs.Cfg.CookieSecure,
SameSite: sameSite,
})
}
if sameSite != http.SameSiteDefaultMode {
cookie.SameSite = sameSite
}
http.SetCookie(w, &cookie)
}
func hashStatecode(code, seed string) string {

View File

@@ -495,6 +495,7 @@ func TestDSRouteRule(t *testing.T) {
createAuthTest(m.DS_ES, AUTHTYPE_BASIC, AUTHCHECK_HEADER, true),
}
for _, test := range tests {
m.ClearDSDecryptionCache()
runDatasourceAuthTest(test)
}
})

View File

@@ -155,6 +155,10 @@ func (uss *UsageStatsService) sendUsageStats(oauthProviders map[string]bool) {
}
func (uss *UsageStatsService) updateTotalStats() {
if !uss.Cfg.MetricsEndpointEnabled || uss.Cfg.MetricsEndpointDisableTotalStats {
return
}
statsQuery := models.GetSystemStatsQuery{}
if err := uss.Bus.Dispatch(&statsQuery); err != nil {
metricsLogger.Error("Failed to get system stats", "error", err)

View File

@@ -264,6 +264,49 @@ func TestMetrics(t *testing.T) {
ts.Close()
})
})
Convey("Test update total stats", t, func() {
uss := &UsageStatsService{
Bus: bus.New(),
Cfg: setting.NewCfg(),
}
uss.Cfg.MetricsEndpointEnabled = true
uss.Cfg.MetricsEndpointDisableTotalStats = false
getSystemStatsWasCalled := false
uss.Bus.AddHandler(func(query *models.GetSystemStatsQuery) error {
query.Result = &models.SystemStats{}
getSystemStatsWasCalled = true
return nil
})
Convey("should not update stats when metrics is disabled and total stats is disabled", func() {
uss.Cfg.MetricsEndpointEnabled = false
uss.Cfg.MetricsEndpointDisableTotalStats = true
uss.updateTotalStats()
So(getSystemStatsWasCalled, ShouldBeFalse)
})
Convey("should not update stats when metrics is disabled and total stats enabled", func() {
uss.Cfg.MetricsEndpointEnabled = false
uss.Cfg.MetricsEndpointDisableTotalStats = false
uss.updateTotalStats()
So(getSystemStatsWasCalled, ShouldBeFalse)
})
Convey("should not update stats when metrics is enabled and total stats disabled", func() {
uss.Cfg.MetricsEndpointEnabled = true
uss.Cfg.MetricsEndpointDisableTotalStats = true
uss.updateTotalStats()
So(getSystemStatsWasCalled, ShouldBeFalse)
})
Convey("should update stats when metrics is enabled and total stats enabled", func() {
uss.Cfg.MetricsEndpointEnabled = true
uss.Cfg.MetricsEndpointDisableTotalStats = false
uss.updateTotalStats()
So(getSystemStatsWasCalled, ShouldBeTrue)
})
})
}
func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {

View File

@@ -103,3 +103,16 @@ func AdminOrFeatureEnabled(enabled bool) macaron.Handler {
}
}
}
func SnapshotPublicModeOrSignedIn() macaron.Handler {
return func(c *m.ReqContext) {
if setting.SnapshotPublicMode {
return
}
_, err := c.Invoke(ReqSignedIn)
if err != nil {
c.JsonApiErr(500, "Failed to invoke required signed in middleware", err)
}
}
}

View File

@@ -3,6 +3,8 @@ package middleware
import (
"testing"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
@@ -31,5 +33,19 @@ func TestMiddlewareAuth(t *testing.T) {
})
})
Convey("snapshot public mode or signed in", func() {
middlewareScenario(t, "Snapshot public mode disabled and unauthenticated request should return 401", func(sc *scenarioContext) {
sc.m.Get("/api/snapshot", SnapshotPublicModeOrSignedIn(), sc.defaultHandler)
sc.fakeReq("GET", "/api/snapshot").exec()
So(sc.resp.Code, ShouldEqual, 401)
})
middlewareScenario(t, "Snapshot public mode enabled and unauthenticated request should return 200", func(sc *scenarioContext) {
setting.SnapshotPublicMode = true
sc.m.Get("/api/snapshot", SnapshotPublicModeOrSignedIn(), sc.defaultHandler)
sc.fakeReq("GET", "/api/snapshot").exec()
So(sc.resp.Code, ShouldEqual, 200)
})
})
})
}

View File

@@ -267,7 +267,9 @@ func WriteSessionCookie(ctx *models.ReqContext, value string, maxLifetimeDays in
Path: setting.AppSubUrl + "/",
Secure: setting.CookieSecure,
MaxAge: maxAge,
SameSite: setting.CookieSameSite,
}
if setting.CookieSameSite != http.SameSiteDefaultMode {
cookie.SameSite = setting.CookieSameSite
}
http.SetCookie(ctx.Resp, &cookie)

View File

@@ -306,28 +306,38 @@ func TestMiddlewareContext(t *testing.T) {
maxAgeHours := (time.Duration(setting.LoginMaxLifetimeDays) * 24 * time.Hour)
maxAge := (maxAgeHours + time.Hour).Seconds()
expectedCookie := &http.Cookie{
Name: setting.LoginCookieName,
Value: "rotated",
Path: setting.AppSubUrl + "/",
HttpOnly: true,
MaxAge: int(maxAge),
Secure: setting.CookieSecure,
SameSite: setting.CookieSameSite,
sameSitePolicies := []http.SameSite{
http.SameSiteDefaultMode,
http.SameSiteLaxMode,
http.SameSiteStrictMode,
}
for _, sameSitePolicy := range sameSitePolicies {
setting.CookieSameSite = sameSitePolicy
expectedCookie := &http.Cookie{
Name: setting.LoginCookieName,
Value: "rotated",
Path: setting.AppSubUrl + "/",
HttpOnly: true,
MaxAge: int(maxAge),
Secure: setting.CookieSecure,
}
if sameSitePolicy != http.SameSiteDefaultMode {
expectedCookie.SameSite = sameSitePolicy
}
sc.fakeReq("GET", "/").exec()
sc.fakeReq("GET", "/").exec()
Convey("should init context with user info", func() {
So(sc.context.IsSignedIn, ShouldBeTrue)
So(sc.context.UserId, ShouldEqual, 12)
So(sc.context.UserToken.UserId, ShouldEqual, 12)
So(sc.context.UserToken.UnhashedToken, ShouldEqual, "rotated")
})
Convey(fmt.Sprintf("Should init context with user info and setting.SameSite=%v", sameSitePolicy), func() {
So(sc.context.IsSignedIn, ShouldBeTrue)
So(sc.context.UserId, ShouldEqual, 12)
So(sc.context.UserToken.UserId, ShouldEqual, 12)
So(sc.context.UserToken.UnhashedToken, ShouldEqual, "rotated")
})
Convey("should set cookie", func() {
So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, expectedCookie.String())
})
Convey(fmt.Sprintf("Should set cookie with setting.SameSite=%v", sameSitePolicy), func() {
So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, expectedCookie.String())
})
}
})
middlewareScenario(t, "Invalid/expired auth token in cookie", func(sc *scenarioContext) {

View File

@@ -76,7 +76,7 @@ func (ds *DataSource) DecryptedPassword() string {
// decryptedValue returns decrypted value from secureJsonData
func (ds *DataSource) decryptedValue(field string, fallback string) string {
if value, ok := ds.SecureJsonData.DecryptedValue(field); ok {
if value, ok := ds.DecryptedValue(field); ok {
return value
}
return fallback

View File

@@ -110,3 +110,49 @@ func (ds *DataSource) GetTLSConfig() (*tls.Config, error) {
return tlsConfig, nil
}
type cachedDecryptedJSON struct {
updated time.Time
json map[string]string
}
type secureJSONDecryptionCache struct {
cache map[int64]cachedDecryptedJSON
sync.Mutex
}
var dsDecryptionCache = secureJSONDecryptionCache{
cache: make(map[int64]cachedDecryptedJSON),
}
// DecryptedValues returns cached decrypted values from secureJsonData.
func (ds *DataSource) DecryptedValues() map[string]string {
dsDecryptionCache.Lock()
defer dsDecryptionCache.Unlock()
if item, present := dsDecryptionCache.cache[ds.Id]; present && ds.Updated.Equal(item.updated) {
return item.json
}
json := ds.SecureJsonData.Decrypt()
dsDecryptionCache.cache[ds.Id] = cachedDecryptedJSON{
updated: ds.Updated,
json: json,
}
return json
}
// DecryptedValue returns cached decrypted value from cached secureJsonData.
func (ds *DataSource) DecryptedValue(key string) (string, bool) {
value, exists := ds.DecryptedValues()[key]
return value, exists
}
// ClearDSDecryptionCache clears the datasource decryption cache.
func ClearDSDecryptionCache() {
dsDecryptionCache.Lock()
defer dsDecryptionCache.Unlock()
dsDecryptionCache.cache = make(map[int64]cachedDecryptedJSON)
}

View File

@@ -6,15 +6,16 @@ import (
. "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
//nolint:goconst
func TestDataSourceCache(t *testing.T) {
func TestDataSourceProxyCache(t *testing.T) {
Convey("When caching a datasource proxy", t, func() {
clearCache()
clearDSProxyCache()
ds := DataSource{
Id: 1,
Url: "http://k8s:8001",
@@ -36,13 +37,13 @@ func TestDataSourceCache(t *testing.T) {
Convey("Should have no TLS client certificate configured", func() {
So(len(t1.TLSClientConfig.Certificates), ShouldEqual, 0)
})
Convey("Should have no user-supplied TLS CA onfigured", func() {
Convey("Should have no user-supplied TLS CA configured", func() {
So(t1.TLSClientConfig.RootCAs, ShouldBeNil)
})
})
Convey("When caching a datasource proxy then updating it", t, func() {
clearCache()
clearDSProxyCache()
setting.SecretKey = "password"
json := simplejson.New()
@@ -84,7 +85,7 @@ func TestDataSourceCache(t *testing.T) {
})
Convey("When caching a datasource proxy with TLS client authentication enabled", t, func() {
clearCache()
clearDSProxyCache()
setting.SecretKey = "password"
json := simplejson.New()
@@ -118,7 +119,7 @@ func TestDataSourceCache(t *testing.T) {
})
Convey("When caching a datasource proxy with a user-supplied TLS CA", t, func() {
clearCache()
clearDSProxyCache()
setting.SecretKey = "password"
json := simplejson.New()
@@ -147,7 +148,7 @@ func TestDataSourceCache(t *testing.T) {
})
Convey("When caching a datasource proxy when user skips TLS verification", t, func() {
clearCache()
clearDSProxyCache()
json := simplejson.New()
json.Set("tlsSkipVerify", true)
@@ -168,7 +169,64 @@ func TestDataSourceCache(t *testing.T) {
})
}
func clearCache() {
func TestDataSourceDecryptionCache(t *testing.T) {
Convey("When datasource hasn't been updated, encrypted JSON should be fetched from cache", t, func() {
ClearDSDecryptionCache()
ds := DataSource{
Id: 1,
Type: DS_INFLUXDB_08,
JsonData: simplejson.New(),
User: "user",
SecureJsonData: securejsondata.GetEncryptedJsonData(map[string]string{
"password": "password",
}),
}
// Populate cache
password, ok := ds.DecryptedValue("password")
So(password, ShouldEqual, "password")
So(ok, ShouldBeTrue)
ds.SecureJsonData = securejsondata.GetEncryptedJsonData(map[string]string{
"password": "",
})
password, ok = ds.DecryptedValue("password")
So(password, ShouldEqual, "password")
So(ok, ShouldBeTrue)
})
Convey("When datasource is updated, encrypted JSON should not be fetched from cache", t, func() {
ClearDSDecryptionCache()
ds := DataSource{
Id: 1,
Type: DS_INFLUXDB_08,
JsonData: simplejson.New(),
User: "user",
SecureJsonData: securejsondata.GetEncryptedJsonData(map[string]string{
"password": "password",
}),
}
// Populate cache
password, ok := ds.DecryptedValue("password")
So(password, ShouldEqual, "password")
So(ok, ShouldBeTrue)
ds.SecureJsonData = securejsondata.GetEncryptedJsonData(map[string]string{
"password": "",
})
ds.Updated = time.Now()
password, ok = ds.DecryptedValue("password")
So(password, ShouldEqual, "")
So(ok, ShouldBeTrue)
})
}
func clearDSProxyCache() {
ptc.Lock()
defer ptc.Unlock()

View File

@@ -19,6 +19,9 @@ var newLDAP = ldap.New
// ErrInvalidCredentials is returned if username and password do not match
var ErrInvalidCredentials = ldap.ErrInvalidCredentials
// ErrCouldNotFindUser is returned when username hasn't been found (not username+password)
var ErrCouldNotFindUser = ldap.ErrCouldNotFindUser
// ErrNoLDAPServers is returned when there is no LDAP servers specified
var ErrNoLDAPServers = errors.New("No LDAP servers are configured")
@@ -76,7 +79,7 @@ func (multiples *MultiLDAP) Login(query *models.LoginUserQuery) (
}
// Continue if we couldn't find the user
if err == ErrInvalidCredentials {
if err == ErrCouldNotFindUser {
continue
}

View File

@@ -82,10 +82,10 @@ func TestMultiLDAP(t *testing.T) {
teardown()
})
Convey("Should still call a second error for invalid cred error", func() {
Convey("Should still call a second error for invalid not found error", func() {
mock := setup()
mock.loginErrReturn = ErrInvalidCredentials
mock.loginErrReturn = ErrCouldNotFindUser
multi := New([]*ldap.ServerConfig{
{}, {},

View File

@@ -96,22 +96,13 @@ func roleCounterSQL(role, alias string) string {
return `
(
SELECT COUNT(*)
FROM ` + dialect.Quote("user") + ` as u
WHERE
(SELECT COUNT(*)
FROM org_user
WHERE org_user.user_id=u.id
AND org_user.role='` + role + `')>0
FROM ` + dialect.Quote("user") + ` as u, org_user
WHERE ( org_user.user_id=u.id AND org_user.role='` + role + `' )
) as ` + alias + `,
(
SELECT COUNT(*)
FROM ` + dialect.Quote("user") + ` as u
WHERE
(SELECT COUNT(*)
FROM org_user
WHERE org_user.user_id=u.id
AND org_user.role='` + role + `')>0
AND u.last_seen_at>?
FROM ` + dialect.Quote("user") + ` as u, org_user
WHERE u.last_seen_at>? AND ( org_user.user_id=u.id AND org_user.role='` + role + `' )
) as active_` + alias
}

View File

@@ -107,6 +107,7 @@ var (
ExternalSnapshotName string
ExternalEnabled bool
SnapShotRemoveExpired bool
SnapshotPublicMode bool
// Dashboard history
DashboardVersionsToKeep int
@@ -241,6 +242,7 @@ type Cfg struct {
MetricsEndpointEnabled bool
MetricsEndpointBasicAuthUsername string
MetricsEndpointBasicAuthPassword string
MetricsEndpointDisableTotalStats bool
PluginsEnableAlpha bool
PluginsAppsSkipVerifyTLS bool
DisableSanitizeHtml bool
@@ -728,6 +730,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
}
ExternalEnabled = snapshots.Key("external_enabled").MustBool(true)
SnapShotRemoveExpired = snapshots.Key("snapshot_remove_expired").MustBool(true)
SnapshotPublicMode = snapshots.Key("public_mode").MustBool(false)
// read dashboard settings
dashboards := iniFile.Section("dashboards")
@@ -897,6 +900,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
if err != nil {
return err
}
cfg.MetricsEndpointDisableTotalStats = iniFile.Section("metrics").Key("disable_total_stats").MustBool(false)
analytics := iniFile.Section("analytics")
ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true)

View File

@@ -147,16 +147,9 @@ func (e *CloudWatchExecutor) getDsInfo(region string) *DatasourceInfo {
authType := e.DataSource.JsonData.Get("authType").MustString()
assumeRoleArn := e.DataSource.JsonData.Get("assumeRoleArn").MustString()
accessKey := ""
secretKey := ""
for key, value := range e.DataSource.SecureJsonData.Decrypt() {
if key == "accessKey" {
accessKey = value
}
if key == "secretKey" {
secretKey = value
}
}
decrypted := e.DataSource.DecryptedValues()
accessKey := decrypted["accessKey"]
secretKey := decrypted["secretKey"]
datasourceInfo := &DatasourceInfo{
Region: region,

View File

@@ -77,6 +77,7 @@ export function registerAngularDirectives() {
'items',
['onClose', { watchDepth: 'reference', wrapApply: true }],
['getContextMenuSource', { watchDepth: 'reference', wrapApply: true }],
['formatSourceDate', { watchDepth: 'reference', wrapApply: true }],
]);
// We keep the drilldown terminology here because of as using data-* directive

View File

@@ -152,8 +152,6 @@ export class PanelChrome extends PureComponent<Props, State> {
onRefresh = () => {
const { panel, isInView, width } = this.props;
console.log('onRefresh', panel.id);
if (!isInView) {
console.log('Refresh when panel is visible', panel.id);
this.setState({ refreshWhenInView: true });

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { shallow } from 'enzyme';
import { PanelHeaderCorner } from './PanelHeaderCorner';
import { PanelModel } from '../../state';
describe('Render', () => {
it('should render component', () => {
const panel = new PanelModel({});
const links: any[] = [
{
url: 'asd',
title: 'asd',
},
];
const wrapper = shallow(<PanelHeaderCorner panel={panel} links={links} />);
const instance = wrapper.instance() as PanelHeaderCorner;
expect(instance.getInfoContent()).toBeDefined();
});
});

View File

@@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { renderMarkdown } from '@grafana/data';
import { Tooltip, ScopedVars } from '@grafana/ui';
import { Tooltip, ScopedVars, PopperContent } from '@grafana/ui';
import { DataLink } from '@grafana/data';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
@@ -44,7 +44,7 @@ export class PanelHeaderCorner extends Component<Props> {
getInfoContent = (): JSX.Element => {
const { panel } = this.props;
const markdown = panel.description;
const markdown = panel.description || '';
const linkSrv = new LinkSrv(templateSrv, this.timeSrv);
const interpolatedMarkdown = templateSrv.replace(markdown, panel.scopedVars);
const markedInterpolatedMarkdown = renderMarkdown(interpolatedMarkdown);
@@ -71,7 +71,7 @@ export class PanelHeaderCorner extends Component<Props> {
);
};
renderCornerType(infoMode: InfoMode, content: string | JSX.Element) {
renderCornerType(infoMode: InfoMode, content: PopperContent<any>) {
const theme = infoMode === InfoMode.Error ? 'error' : 'info';
return (
<Tooltip content={content} placement="top-start" theme={theme}>
@@ -95,7 +95,7 @@ export class PanelHeaderCorner extends Component<Props> {
}
if (infoMode === InfoMode.Info || infoMode === InfoMode.Links) {
return this.renderCornerType(infoMode, this.getInfoContent());
return this.renderCornerType(infoMode, this.getInfoContent);
}
return null;

View File

@@ -74,11 +74,15 @@ export class QueriesTab extends PureComponent<Props, State> {
// Updates the response with information from the stream
panelDataObserver = {
next: (data: PanelData) => {
const { panel } = this.props;
if (data.state === LoadingState.Error) {
panel.events.emit('data-error', data.error);
} else if (data.state === LoadingState.Done) {
panel.events.emit('data-received', data.legacy);
try {
const { panel } = this.props;
if (data.state === LoadingState.Error) {
panel.events.emit('data-error', data.error);
} else if (data.state === LoadingState.Done) {
panel.events.emit('data-received', data.legacy);
}
} catch (err) {
console.log('Panel.events handler error', err);
}
this.setState({ data });
},

View File

@@ -415,6 +415,10 @@ describe('DashboardModel', () => {
dashUri: '',
title: 'test',
},
{
type: 'dashboard',
keepTime: true,
},
],
},
],

View File

@@ -643,6 +643,11 @@ function upgradePanelLink(link: any): DataLink {
url = `/dashboard/${link.dashUri}`;
}
// some models are incomplete and have no dashboard or dashUri
if (!url) {
url = '/';
}
if (link.keepTime) {
url = appendQueryToUrl(url, `$${DataLinkBuiltInVars.keepTime}`);
}

View File

@@ -247,8 +247,6 @@ export class PanelModel {
pluginLoaded(plugin: PanelPlugin) {
this.plugin = plugin;
this.applyPluginOptionDefaults(plugin);
if (plugin.panel && plugin.onPanelMigration) {
const version = getPluginVersion(plugin);
if (version !== this.pluginVersion) {
@@ -256,6 +254,8 @@ export class PanelModel {
this.pluginVersion = version;
}
}
this.applyPluginOptionDefaults(plugin);
}
changePlugin(newPlugin: PanelPlugin) {

View File

@@ -1,72 +0,0 @@
import PlaceholdersBuffer from './PlaceholdersBuffer';
describe('PlaceholdersBuffer', () => {
it('does nothing if no placeholders are defined', () => {
const text = 'metric';
const buffer = new PlaceholdersBuffer(text);
expect(buffer.hasPlaceholders()).toBe(false);
expect(buffer.toString()).toBe(text);
expect(buffer.getNextMoveOffset()).toBe(0);
});
it('respects the traversal order of placeholders', () => {
const text = 'sum($2 offset $1) by ($3)';
const buffer = new PlaceholdersBuffer(text);
expect(buffer.hasPlaceholders()).toBe(true);
expect(buffer.toString()).toBe('sum( offset ) by ()');
expect(buffer.getNextMoveOffset()).toBe(12);
buffer.setNextPlaceholderValue('1h');
expect(buffer.hasPlaceholders()).toBe(true);
expect(buffer.toString()).toBe('sum( offset 1h) by ()');
expect(buffer.getNextMoveOffset()).toBe(-10);
buffer.setNextPlaceholderValue('metric');
expect(buffer.hasPlaceholders()).toBe(true);
expect(buffer.toString()).toBe('sum(metric offset 1h) by ()');
expect(buffer.getNextMoveOffset()).toBe(16);
buffer.setNextPlaceholderValue('label');
expect(buffer.hasPlaceholders()).toBe(false);
expect(buffer.toString()).toBe('sum(metric offset 1h) by (label)');
expect(buffer.getNextMoveOffset()).toBe(0);
});
it('respects the traversal order of adjacent placeholders', () => {
const text = '$1$3$2$4';
const buffer = new PlaceholdersBuffer(text);
expect(buffer.hasPlaceholders()).toBe(true);
expect(buffer.toString()).toBe('');
expect(buffer.getNextMoveOffset()).toBe(0);
buffer.setNextPlaceholderValue('1');
expect(buffer.hasPlaceholders()).toBe(true);
expect(buffer.toString()).toBe('1');
expect(buffer.getNextMoveOffset()).toBe(0);
buffer.setNextPlaceholderValue('2');
expect(buffer.hasPlaceholders()).toBe(true);
expect(buffer.toString()).toBe('12');
expect(buffer.getNextMoveOffset()).toBe(-1);
buffer.setNextPlaceholderValue('3');
expect(buffer.hasPlaceholders()).toBe(true);
expect(buffer.toString()).toBe('132');
expect(buffer.getNextMoveOffset()).toBe(1);
buffer.setNextPlaceholderValue('4');
expect(buffer.hasPlaceholders()).toBe(false);
expect(buffer.toString()).toBe('1324');
expect(buffer.getNextMoveOffset()).toBe(0);
});
});

View File

@@ -1,112 +0,0 @@
/**
* Provides a stateful means of managing placeholders in text.
*
* Placeholders are numbers prefixed with the `$` character (e.g. `$1`).
* Each number value represents the order in which a placeholder should
* receive focus if multiple placeholders exist.
*
* Example scenario given `sum($3 offset $1) by($2)`:
* 1. `sum( offset |) by()`
* 2. `sum( offset 1h) by(|)`
* 3. `sum(| offset 1h) by (label)`
*/
export default class PlaceholdersBuffer {
private nextMoveOffset: number;
private orders: number[];
private parts: string[];
constructor(text: string) {
const result = this.parse(text);
const nextPlaceholderIndex = result.orders.length ? result.orders[0] : 0;
this.nextMoveOffset = this.getOffsetBetween(result.parts, 0, nextPlaceholderIndex);
this.orders = result.orders;
this.parts = result.parts;
}
clearPlaceholders() {
this.nextMoveOffset = 0;
this.orders = [];
}
getNextMoveOffset(): number {
return this.nextMoveOffset;
}
hasPlaceholders(): boolean {
return this.orders.length > 0;
}
setNextPlaceholderValue(value: string) {
if (this.orders.length === 0) {
return;
}
const currentPlaceholderIndex = this.orders[0];
this.parts[currentPlaceholderIndex] = value;
this.orders = this.orders.slice(1);
if (this.orders.length === 0) {
this.nextMoveOffset = 0;
return;
}
const nextPlaceholderIndex = this.orders[0];
// Case should never happen but handle it gracefully in case
if (currentPlaceholderIndex === nextPlaceholderIndex) {
this.nextMoveOffset = 0;
return;
}
const backwardMove = currentPlaceholderIndex > nextPlaceholderIndex;
const indices = backwardMove
? { start: nextPlaceholderIndex + 1, end: currentPlaceholderIndex + 1 }
: { start: currentPlaceholderIndex + 1, end: nextPlaceholderIndex };
this.nextMoveOffset = (backwardMove ? -1 : 1) * this.getOffsetBetween(this.parts, indices.start, indices.end);
}
toString(): string {
return this.parts.join('');
}
private getOffsetBetween(parts: string[], startIndex: number, endIndex: number) {
return parts.slice(startIndex, endIndex).reduce((offset, part) => offset + part.length, 0);
}
private parse(text: string): ParseResult {
const placeholderRegExp = /\$(\d+)/g;
const parts = [];
const orders = [];
let textOffset = 0;
while (true) {
const match = placeholderRegExp.exec(text);
if (!match) {
break;
}
const part = text.slice(textOffset, match.index);
parts.push(part);
// Accounts for placeholders at text boundaries
if (part !== '') {
parts.push('');
}
const order = parseInt(match[1], 10);
orders.push({ index: parts.length - 1, order });
textOffset += part.length + match.length;
}
// Ensures string serialization still works if no placeholders were parsed
// and also accounts for the remainder of text with placeholders
parts.push(text.slice(textOffset));
return {
// Placeholder values do not necessarily appear sequentially so sort the
// indices to traverse in priority order
orders: orders.sort((o1, o2) => o1.order - o2.order).map(o => o.index),
parts,
};
}
}
type ParseResult = {
/**
* Indices to placeholder items in `parts` in traversal order.
*/
orders: number[];
/**
* Parts comprising the original text with placeholders occupying distinct items.
*/
parts: string[];
};

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { shallow } from 'enzyme';
import { QueryField } from './QueryField';
describe('<QueryField />', () => {
it('should render with null initial value', () => {
const wrapper = shallow(<QueryField initialQuery={null} />);
expect(wrapper.find('div').exists()).toBeTruthy();
});
it('should render with empty initial value', () => {
const wrapper = shallow(<QueryField initialQuery="" />);
expect(wrapper.find('div').exists()).toBeTruthy();
});
it('should render with initial value', () => {
const wrapper = shallow(<QueryField initialQuery="my query" />);
expect(wrapper.find('div').exists()).toBeTruthy();
});
it('should execute query when enter is pressed and there are no suggestions visible', () => {
const wrapper = shallow(<QueryField initialQuery="my query" />);
const instance = wrapper.instance() as QueryField;
instance.executeOnChangeAndRunQueries = jest.fn();
const handleEnterAndTabKeySpy = jest.spyOn(instance, 'handleEnterAndTabKey');
instance.onKeyDown({ key: 'Enter', preventDefault: () => {} } as KeyboardEvent, {});
expect(handleEnterAndTabKeySpy).toBeCalled();
expect(instance.executeOnChangeAndRunQueries).toBeCalled();
});
it('should copy selected text', () => {
const wrapper = shallow(<QueryField initialQuery="" />);
const instance = wrapper.instance() as QueryField;
const textBlocks = ['ignore this text. copy this text'];
const copiedText = instance.getCopiedText(textBlocks, 18, 32);
expect(copiedText).toBe('copy this text');
});
it('should copy selected text across 2 lines', () => {
const wrapper = shallow(<QueryField initialQuery="" />);
const instance = wrapper.instance() as QueryField;
const textBlocks = ['ignore this text. start copying here', 'lorem ipsum. stop copying here. lorem ipsum'];
const copiedText = instance.getCopiedText(textBlocks, 18, 30);
expect(copiedText).toBe('start copying here\nlorem ipsum. stop copying here');
});
it('should copy selected text across > 2 lines', () => {
const wrapper = shallow(<QueryField initialQuery="" />);
const instance = wrapper.instance() as QueryField;
const textBlocks = [
'ignore this text. start copying here',
'lorem ipsum doler sit amet',
'lorem ipsum. stop copying here. lorem ipsum',
];
const copiedText = instance.getCopiedText(textBlocks, 18, 30);
expect(copiedText).toBe('start copying here\nlorem ipsum doler sit amet\nlorem ipsum. stop copying here');
});
});

View File

@@ -2,7 +2,7 @@ import _ from 'lodash';
import React, { Context } from 'react';
import ReactDOM from 'react-dom';
// @ts-ignore
import { Change, Value } from 'slate';
import { Change, Value, Block } from 'slate';
// @ts-ignore
import { Editor } from 'slate-react';
// @ts-ignore
@@ -16,7 +16,6 @@ import NewlinePlugin from './slate-plugins/newline';
import { TypeaheadWithTheme } from './Typeahead';
import { makeFragment, makeValue } from '@grafana/ui';
import PlaceholdersBuffer from './PlaceholdersBuffer';
export const TYPEAHEAD_DEBOUNCE = 100;
export const HIGHLIGHT_WAIT = 500;
@@ -74,7 +73,6 @@ export interface TypeaheadInput {
*/
export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldState> {
menuEl: HTMLElement | null;
placeholdersBuffer: PlaceholdersBuffer;
plugins: any[];
resetTimer: any;
mounted: boolean;
@@ -83,7 +81,6 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
constructor(props: QueryFieldProps, context: Context<any>) {
super(props, context);
this.placeholdersBuffer = new PlaceholdersBuffer(props.initialQuery || '');
this.updateHighlightsTimer = _.debounce(this.updateLogsHighlights, HIGHLIGHT_WAIT);
// Base plugins
@@ -95,7 +92,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
typeaheadIndex: 0,
typeaheadPrefix: '',
typeaheadText: '',
value: makeValue(this.placeholdersBuffer.toString(), props.syntax),
value: makeValue(props.initialQuery || '', props.syntax),
lastExecutedValue: null,
};
}
@@ -118,8 +115,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
if (initialQuery !== prevProps.initialQuery) {
// and we have a version that differs
if (initialQuery !== Plain.serialize(value)) {
this.placeholdersBuffer = new PlaceholdersBuffer(initialQuery || '');
this.setState({ value: makeValue(this.placeholdersBuffer.toString(), syntax) });
this.setState({ value: makeValue(initialQuery || '', syntax) });
}
}
@@ -136,9 +132,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
.change()
.insertText(' ')
.deleteBackward();
if (this.placeholdersBuffer.hasPlaceholders()) {
change.move(this.placeholdersBuffer.getNextMoveOffset()).focus();
}
this.onChange(change, true);
}
}
@@ -313,33 +307,23 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
handleEnterAndTabKey = (event: KeyboardEvent, change: Change) => {
const { typeaheadIndex, suggestions } = this.state;
if (this.menuEl) {
// Dont blur input
event.preventDefault();
if (!suggestions || suggestions.length === 0) {
return undefined;
}
event.preventDefault();
const suggestion = getSuggestionByIndex(suggestions, typeaheadIndex);
const nextChange = this.applyTypeahead(change, suggestion);
const insertTextOperation = nextChange.operations.find((operation: any) => operation.type === 'insert_text');
if (insertTextOperation) {
const suggestionText = insertTextOperation.text;
this.placeholdersBuffer.setNextPlaceholderValue(suggestionText);
if (this.placeholdersBuffer.hasPlaceholders()) {
nextChange.move(this.placeholdersBuffer.getNextMoveOffset()).focus();
}
}
return true;
} else if (!event.shiftKey) {
// Run queries if Shift is not pressed, otherwise pass through
if (event.shiftKey) {
// pass through if shift is pressed
return undefined;
} else if (!this.menuEl) {
this.executeOnChangeAndRunQueries();
return true;
} else if (!suggestions || suggestions.length === 0) {
return undefined;
}
return undefined;
const suggestion = getSuggestionByIndex(suggestions, typeaheadIndex);
const nextChange = this.applyTypeahead(change, suggestion);
const insertTextOperation = nextChange.operations.find((operation: any) => operation.type === 'insert_text');
return insertTextOperation ? true : undefined;
};
onKeyDown = (event: KeyboardEvent, change: Change) => {
@@ -415,8 +399,6 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
// If we dont wait here, menu clicks wont work because the menu
// will be gone.
this.resetTimer = setTimeout(this.resetTypeahead, 100);
// Disrupting placeholder entry wipes all remaining placeholders needing input
this.placeholdersBuffer.clearPlaceholders();
if (previousValue !== currentValue) {
this.executeOnChangeAndRunQueries();
@@ -432,6 +414,10 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
updateMenu = () => {
const { suggestions } = this.state;
const menu = this.menuEl;
// Exit for unit tests
if (!window.getSelection) {
return;
}
const selection = window.getSelection();
const node = selection.anchorNode;
@@ -490,10 +476,47 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
);
};
handlePaste = (event: ClipboardEvent, change: Editor) => {
getCopiedText(textBlocks: string[], startOffset: number, endOffset: number) {
if (!textBlocks.length) {
return undefined;
}
const excludingLastLineLength = textBlocks.slice(0, -1).join('').length + textBlocks.length - 1;
return textBlocks.join('\n').slice(startOffset, excludingLastLineLength + endOffset);
}
handleCopy = (event: ClipboardEvent, change: Change) => {
event.preventDefault();
const { document, selection, startOffset, endOffset } = change.value;
const selectedBlocks = document.getBlocksAtRangeAsArray(selection).map((block: Block) => block.text);
const copiedText = this.getCopiedText(selectedBlocks, startOffset, endOffset);
if (copiedText) {
event.clipboardData.setData('Text', copiedText);
}
return true;
};
handlePaste = (event: ClipboardEvent, change: Change) => {
event.preventDefault();
const pastedValue = event.clipboardData.getData('Text');
const newValue = change.value.change().insertText(pastedValue);
this.onChange(newValue);
const lines = pastedValue.split('\n');
if (lines.length) {
change.insertText(lines[0]);
for (const line of lines.slice(1)) {
change.splitBlock().insertText(line);
}
}
return true;
};
handleCut = (event: ClipboardEvent, change: Change) => {
this.handleCopy(event, change);
change.deleteAtRange(change.value.selection);
return true;
};

View File

@@ -25,7 +25,7 @@ export class Wrapper extends Component<WrapperProps> {
return (
<div className="page-scrollbar-wrapper">
<CustomScrollbar autoHeightMin={'100%'} className="custom-scrollbar--page">
<CustomScrollbar autoHeightMin={'100%'} autoHeightMax={''} className="custom-scrollbar--page">
<div className="explore-wrapper">
<ErrorBoundary>
<Explore exploreId={ExploreId.left} />

View File

@@ -18,42 +18,6 @@ describe('braces', () => {
expect(Plain.serialize(change.value)).toEqual('()');
});
it('adds closing braces around a value', () => {
const change = Plain.deserialize('foo').change();
const event = new window.KeyboardEvent('keydown', { key: '(' });
handler(event, change);
expect(Plain.serialize(change.value)).toEqual('(foo)');
});
it('adds closing braces around the following value only', () => {
const change = Plain.deserialize('foo bar ugh').change();
let event;
event = new window.KeyboardEvent('keydown', { key: '(' });
handler(event, change);
expect(Plain.serialize(change.value)).toEqual('(foo) bar ugh');
// Wrap bar
change.move(5);
event = new window.KeyboardEvent('keydown', { key: '(' });
handler(event, change);
expect(Plain.serialize(change.value)).toEqual('(foo) (bar) ugh');
// Create empty parens after (bar)
change.move(4);
event = new window.KeyboardEvent('keydown', { key: '(' });
handler(event, change);
expect(Plain.serialize(change.value)).toEqual('(foo) (bar)() ugh');
});
it('adds closing braces outside a selector', () => {
const change = Plain.deserialize('sumrate(metric{namespace="dev", cluster="c1"}[2m])').change();
let event;
change.move(3);
event = new window.KeyboardEvent('keydown', { key: '(' });
handler(event, change);
expect(Plain.serialize(change.value)).toEqual('sum(rate(metric{namespace="dev", cluster="c1"}[2m]))');
});
it('removes closing brace when opening brace is removed', () => {
const change = Plain.deserialize('time()').change();
let event;

View File

@@ -1,45 +1,42 @@
const BRACES = {
// @ts-ignore
import { Change } from 'slate';
const BRACES: any = {
'[': ']',
'{': '}',
'(': ')',
};
const NON_SELECTOR_SPACE_REGEXP = / (?![^}]+})/;
export default function BracesPlugin() {
return {
onKeyDown(event, change) {
onKeyDown(event: KeyboardEvent, change: Change) {
const { value } = change;
if (!value.isCollapsed) {
return undefined;
}
switch (event.key) {
case '(':
case '{':
case '[': {
event.preventDefault();
// Insert matching braces
change
.insertText(`${event.key}${BRACES[event.key]}`)
.move(-1)
.focus();
return true;
}
case '(': {
event.preventDefault();
const text = value.anchorText.text;
const offset = value.anchorOffset;
const delimiterIndex = text.slice(offset).search(NON_SELECTOR_SPACE_REGEXP);
const length = delimiterIndex > -1 ? delimiterIndex + offset : text.length;
const forward = length - offset;
// Insert matching braces
change
.insertText(event.key)
.move(forward)
.insertText(BRACES[event.key])
.move(-1 - forward)
.focus();
const { startOffset, startKey, endOffset, endKey, focusOffset } = value.selection;
const text: string = value.focusText.text;
// If text is selected, wrap selected text in parens
if (value.isExpanded) {
change
.insertTextByKey(startKey, startOffset, event.key)
.insertTextByKey(endKey, endOffset + 1, BRACES[event.key])
.moveEnd(-1);
} else if (
focusOffset === text.length ||
text[focusOffset] === ' ' ||
Object.values(BRACES).includes(text[focusOffset])
) {
change.insertText(`${event.key}${BRACES[event.key]}`).move(-1);
} else {
change.insertText(event.key);
}
return true;
}

View File

@@ -152,9 +152,7 @@ class MetricsPanelCtrl extends PanelCtrl {
// Make the results look like they came directly from a <6.2 datasource request
// NOTE: any object other than 'data' is no longer supported supported
this.handleQueryResult({
data: data.legacy,
});
this.handleQueryResult({ data: data.legacy });
} else {
this.handleDataFrame(data.series);
}
@@ -219,7 +217,12 @@ class MetricsPanelCtrl extends PanelCtrl {
if (this.dashboard && this.dashboard.snapshot) {
this.panel.snapshotData = data;
}
// Subclasses that asked for DataFrame will override
try {
this.events.emit('data-frames-received', data);
} catch (err) {
this.processDataError(err);
}
}
handleQueryResult(result: DataQueryResponse) {
@@ -234,7 +237,11 @@ class MetricsPanelCtrl extends PanelCtrl {
result = { data: [] };
}
this.events.emit('data-received', result.data);
try {
this.events.emit('data-received', result.data);
} catch (err) {
this.processDataError(err);
}
}
getAdditionalMenuItems() {

View File

@@ -248,10 +248,10 @@ export class PanelCtrl {
}
getInfoContent(options: { mode: string }) {
let markdown = this.panel.description;
let markdown = this.panel.description || '';
if (options.mode === 'tooltip') {
markdown = this.error || this.panel.description;
markdown = this.error || this.panel.description || '';
}
const linkSrv: LinkSrv = this.$injector.get('linkSrv');

View File

@@ -131,7 +131,7 @@ export class LinkSrv implements LinkService {
if (dataPoint) {
info.href = this.templateSrv.replace(
info.href,
this.getDataPointVars(dataPoint.seriesName, dateTime(dataPoint[0]))
this.getDataPointVars(dataPoint.seriesName, dateTime(dataPoint.datapoint[0]))
);
}

View File

@@ -11,7 +11,7 @@ jest.mock('angular', () => {
const dataPointMock = {
seriesName: 'A-series',
datapoint: [1000000000, 1],
datapoint: [1000000001, 1],
};
describe('linkSrv', () => {
@@ -119,7 +119,7 @@ describe('linkSrv', () => {
{},
dataPointMock
).href
).toEqual('/d/1?time=1000000000');
).toEqual('/d/1?time=1000000001');
});
});
});

View File

@@ -112,6 +112,23 @@ describe('templateSrv', () => {
expect(target).toBe('this.{value1,value2}.filters');
});
describe('when the globbed variable only has one value', () => {
beforeEach(() => {
initTemplateSrv([
{
type: 'query',
name: 'test',
current: { value: ['value1'] },
},
]);
});
it('should not glob the value', () => {
const target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).toBe('this.value1.filters');
});
});
it('should replace ${test} with globbed value', () => {
const target = _templateSrv.replace('this.${test}.filters', {}, 'glob');
expect(target).toBe('this.{value1,value2}.filters');

View File

@@ -172,7 +172,7 @@ export class TemplateSrv {
return this.encodeURIComponentStrict(value);
}
default: {
if (_.isArray(value)) {
if (_.isArray(value) && value.length > 1) {
return '{' + value.join(',') + '}';
}
return value;

View File

@@ -2,7 +2,7 @@ import { GraphiteDatasource } from '../datasource';
import _ from 'lodash';
// @ts-ignore
import $q from 'q';
import { TemplateSrvStub } from 'test/specs/helpers';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { dateTime } from '@grafana/data';
describe('graphiteDatasource', () => {
@@ -10,7 +10,7 @@ describe('graphiteDatasource', () => {
backendSrv: {},
$q,
// @ts-ignore
templateSrv: new TemplateSrvStub(),
templateSrv: new TemplateSrv(),
instanceSettings: { url: 'url', name: 'graphiteProd', jsonData: {} },
};
@@ -218,6 +218,38 @@ describe('graphiteDatasource', () => {
});
expect(results.length).toBe(2);
});
describe('when formatting targets', () => {
it('does not attempt to glob for one variable', () => {
ctx.ds.templateSrv.init([
{
type: 'query',
name: 'metric',
current: { value: ['b'] },
},
]);
const results = ctx.ds.buildGraphiteParams({
targets: [{ target: 'my.$metric.*' }],
});
expect(results).toStrictEqual(['target=my.b.*', 'format=json']);
});
it('globs for more than one variable', () => {
ctx.ds.templateSrv.init([
{
type: 'query',
name: 'metric',
current: { value: ['a', 'b'] },
},
]);
const results = ctx.ds.buildGraphiteParams({
targets: [{ target: 'my.[[metric]].*' }],
});
expect(results).toStrictEqual(['target=my.%7Ba%2Cb%7D.*', 'format=json']);
});
});
});
describe('querying for template variables', () => {
@@ -308,7 +340,13 @@ describe('graphiteDatasource', () => {
});
it('/metrics/find should be POST', () => {
ctx.templateSrv.setGrafanaVariable('foo', 'bar');
ctx.ds.templateSrv.init([
{
type: 'query',
name: 'foo',
current: { value: ['bar'] },
},
]);
ctx.ds.metricFindQuery('[[foo]]').then((data: any) => {
results = data;
});
@@ -327,7 +365,7 @@ function accessScenario(name: string, url: string, fn: any) {
backendSrv: {},
$q,
// @ts-ignore
templateSrv: new TemplateSrvStub(),
templateSrv: new TemplateSrv(),
instanceSettings: { url: 'url', name: 'graphiteProd', jsonData: {} },
};

View File

@@ -41,6 +41,7 @@ export interface PromDataQueryResponse {
result?: DataQueryResponseData[];
};
};
cancelled?: boolean;
}
export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions> {
@@ -528,6 +529,9 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
const eventList: AnnotationEvent[] = [];
tagKeys = tagKeys.split(',');
if (results.cancelled) {
return [];
}
_.each(results.data.data.result, series => {
const tags = _.chain(series.metric)
.filter((v, k) => {

View File

@@ -0,0 +1,44 @@
import { getQueryHints } from './query_hints';
describe('getQueryHints', () => {
describe('when called without datapoints in series', () => {
it('then it should use rows instead and return correct hint', () => {
const series = [
{
fields: [
{
name: 'Some Name',
},
],
rows: [[1], [2]],
},
];
const result = getQueryHints('up', series);
expect(result).toEqual([
{
fix: { action: { query: 'up', type: 'ADD_RATE' }, label: 'Fix by adding rate().' },
label: 'Time series is monotonically increasing.',
type: 'APPLY_RATE',
},
]);
});
});
describe('when called without datapoints and rows in series', () => {
it('then it should use an empty array and return null', () => {
const series = [
{
fields: [
{
name: 'Some Name',
},
],
},
];
const result = getQueryHints('up', series);
expect(result).toEqual(null);
});
});
});

View File

@@ -29,7 +29,7 @@ export function getQueryHints(query: string, series?: any[], datasource?: any):
// Check for monotonicity on series (table results are being ignored here)
if (series && series.length > 0) {
series.forEach(s => {
const datapoints: number[][] = s.datapoints;
const datapoints: number[][] = s.datapoints || s.rows || [];
if (query.indexOf('rate(') === -1 && datapoints.length > 1) {
let increasing = false;
const nonNullData = datapoints.filter(dp => dp[0] !== null);

View File

@@ -667,6 +667,19 @@ describe('PrometheusDatasource', () => {
},
};
describe('when time series query is cancelled', () => {
it('should return empty results', async () => {
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve({ cancelled: true }));
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
await ctx.ds.annotationQuery(options).then((data: any) => {
results = data;
});
expect(results).toEqual([]);
});
});
describe('not use useValueForTime', () => {
beforeEach(async () => {
options.annotation.useValueForTime = false;

View File

@@ -40,18 +40,7 @@ describe('BarGauge Panel Migrations', () => {
orientation: 'vertical',
},
pluginVersion: '6.2.0',
targets: [
{
refId: 'A',
scenarioId: 'random_walk',
},
{
refId: 'B',
scenarioId: 'random_walk',
},
],
timeFrom: null,
timeShift: null,
targets: [],
title: 'Usage',
type: 'bargauge',
} as PanelModel;

View File

@@ -1,36 +1,7 @@
import { PanelModel } from '@grafana/ui';
import {
sharedSingleStatMigrationCheck,
migrateOldThresholds,
} from '@grafana/ui/src/components/SingleStatShared/SingleStatBaseOptions';
import { sharedSingleStatMigrationCheck } from '@grafana/ui/src/components/SingleStatShared/SingleStatBaseOptions';
import { BarGaugeOptions } from './types';
export const barGaugePanelMigrationCheck = (panel: PanelModel<BarGaugeOptions>): Partial<BarGaugeOptions> => {
if (!panel.options) {
// This happens on the first load or when migrating from angular
return {};
}
// Move thresholds to field
const previousVersion = panel.pluginVersion || '';
if (previousVersion.startsWith('6.2') || previousVersion.startsWith('6.3')) {
console.log('TRANSFORM', panel.options);
const old = panel.options as any;
const { fieldOptions } = old;
if (fieldOptions) {
const { mappings, thresholds, ...rest } = fieldOptions;
rest.defaults = {
mappings,
thresholds: migrateOldThresholds(thresholds),
...rest.defaults,
};
return {
...old,
fieldOptions: rest,
};
}
}
// Default to the standard migration path
return sharedSingleStatMigrationCheck(panel);
};

View File

@@ -1,60 +1,7 @@
import { Field, fieldReducers } from '@grafana/data';
import { PanelModel, FieldDisplayOptions } from '@grafana/ui';
import { PanelModel } from '@grafana/ui';
import { GaugeOptions } from './types';
import {
sharedSingleStatMigrationCheck,
migrateOldThresholds,
} from '@grafana/ui/src/components/SingleStatShared/SingleStatBaseOptions';
import { sharedSingleStatMigrationCheck } from '@grafana/ui/src/components/SingleStatShared/SingleStatBaseOptions';
export const gaugePanelMigrationCheck = (panel: PanelModel<GaugeOptions>): Partial<GaugeOptions> => {
if (!panel.options) {
// This happens on the first load or when migrating from angular
return {};
}
const previousVersion = panel.pluginVersion || '';
if (!previousVersion || previousVersion.startsWith('6.1')) {
const old = panel.options as any;
const { valueOptions } = old;
const options = {} as GaugeOptions;
options.showThresholdLabels = old.showThresholdLabels;
options.showThresholdMarkers = old.showThresholdMarkers;
options.orientation = old.orientation;
const fieldOptions = (options.fieldOptions = {} as FieldDisplayOptions);
const field = (fieldOptions.defaults = {} as Field);
field.mappings = old.valueMappings;
field.thresholds = migrateOldThresholds(old.thresholds);
field.unit = valueOptions.unit;
field.decimals = valueOptions.decimals;
// Make sure the stats have a valid name
if (valueOptions.stat) {
fieldOptions.calcs = [fieldReducers.get(valueOptions.stat).id];
}
field.min = old.minValue;
field.max = old.maxValue;
return options;
} else if (previousVersion.startsWith('6.2') || previousVersion.startsWith('6.3')) {
const old = panel.options as any;
const { fieldOptions } = old;
if (fieldOptions) {
const { mappings, thresholds, ...rest } = fieldOptions;
rest.default = {
mappings,
thresholds: migrateOldThresholds(thresholds),
...rest.defaults,
};
return {
...old.options,
fieldOptions: rest,
};
}
}
// Default to the standard migration path
return sharedSingleStatMigrationCheck(panel);
};

View File

@@ -1,14 +1,19 @@
import React, { useContext } from 'react';
import { FlotDataPoint } from './GraphContextMenuCtrl';
import { ContextMenu, ContextMenuProps, SeriesIcon, ThemeContext } from '@grafana/ui';
import { dateTime } from '@grafana/data';
import { DateTimeInput } from '@grafana/data';
import { css } from 'emotion';
type GraphContextMenuProps = ContextMenuProps & {
getContextMenuSource: () => FlotDataPoint | null;
formatSourceDate: (date: DateTimeInput, format?: string) => string;
};
export const GraphContextMenu: React.FC<GraphContextMenuProps> = ({ getContextMenuSource, ...otherProps }) => {
export const GraphContextMenu: React.FC<GraphContextMenuProps> = ({
getContextMenuSource,
formatSourceDate,
...otherProps
}) => {
const theme = useContext(ThemeContext);
const source = getContextMenuSource();
@@ -27,7 +32,7 @@ export const GraphContextMenu: React.FC<GraphContextMenuProps> = ({ getContextMe
font-size: ${theme.typography.size.sm};
`}
>
<strong>{dateTime(source.datapoint[0]).format(timeFormat)}</strong>
<strong>{formatSourceDate(source.datapoint[0], timeFormat)}</strong>
<div>
<SeriesIcon color={source.series.color} />
<span

View File

@@ -65,6 +65,7 @@ export class GraphContextMenuCtrl {
setSource = (source: FlotDataPoint | null) => {
this.source = source;
};
getSource = () => {
return this.source;
};

View File

@@ -199,7 +199,7 @@ class GraphElement {
{
items: [
...dataLinks.map<ContextMenuItem>(link => {
const linkUiModel = this.linkSrv.getDataLinkUIModel(link, this.panel.scopedVariables, {
const linkUiModel = this.linkSrv.getDataLinkUIModel(link, this.panel.scopedVars, {
seriesName: item.series.alias,
datapoint: item.datapoint,
});

View File

@@ -11,7 +11,7 @@ import { DataProcessor } from './data_processor';
import { axesEditorComponent } from './axes_editor';
import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2';
import { DataFrame, DataLink } from '@grafana/data';
import { DataFrame, DataLink, DateTimeInput } from '@grafana/data';
import { getColorFromHexRgbOrName, LegacyResponseData, VariableSuggestion } from '@grafana/ui';
import { getProcessedDataFrame } from 'app/features/dashboard/state/PanelQueryState';
import { PanelQueryRunnerFormat } from 'app/features/dashboard/state/PanelQueryRunner';
@@ -148,6 +148,7 @@ class GraphCtrl extends MetricsPanelCtrl {
this.contextMenuCtrl = new GraphContextMenuCtrl($scope);
this.events.on('render', this.onRender.bind(this));
this.events.on('data-frames-received', this.onDataFramesReceived.bind(this));
this.events.on('data-received', this.onDataReceived.bind(this));
this.events.on('data-error', this.onDataError.bind(this));
this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this));
@@ -210,13 +211,11 @@ class GraphCtrl extends MetricsPanelCtrl {
// This should only be called from the snapshot callback
onDataReceived(dataList: LegacyResponseData[]) {
this.handleDataFrame(getProcessedDataFrame(dataList));
this.onDataFramesReceived(getProcessedDataFrame(dataList));
}
// Directly support DataFrame skipping event callbacks
handleDataFrame(data: DataFrame[]) {
super.handleDataFrame(data);
onDataFramesReceived(data: DataFrame[]) {
this.dataList = data;
this.seriesList = this.processor.getSeriesList({
dataList: this.dataList,
@@ -340,6 +339,10 @@ class GraphCtrl extends MetricsPanelCtrl {
onContextMenuClose = () => {
this.contextMenuCtrl.toggleMenu();
};
formatDate = (date: DateTimeInput, format?: string) => {
return this.dashboard.formatDate.apply(this.dashboard, [date, format]);
};
}
export { GraphCtrl, GraphCtrl as PanelCtrl };

View File

@@ -11,6 +11,7 @@ const template = `
items="ctrl.contextMenuCtrl.menuItems"
onClose="ctrl.onContextMenuClose"
getContextMenuSource="ctrl.contextMenuCtrl.getSource"
formatSourceDate="ctrl.formatDate"
x="ctrl.contextMenuCtrl.position.x"
y="ctrl.contextMenuCtrl.position.y"
></graph-context-menu>

View File

@@ -71,7 +71,7 @@ RUN apt-get update && \
# base image to crossbuild grafana
FROM ubuntu:14.04
ENV GOVERSION=1.12.6 \
ENV GOVERSION=1.12.9 \
PATH=/usr/local/go/bin:$PATH \
GOPATH=/go \
NODEVERSION=10.14.2

View File

@@ -1,6 +1,6 @@
#!/bin/bash
_version="1.2.7"
_version="1.2.8"
_tag="grafana/build-container:${_version}"
docker build -t $_tag .

View File

@@ -16,7 +16,9 @@ type RegexesDefinitions struct {
UA []*uaParser `yaml:"user_agent_parsers"`
OS []*osParser `yaml:"os_parsers"`
Device []*deviceParser `yaml:"device_parsers"`
_ [4]byte // padding for alignment
sync.RWMutex
}
type UserAgentSorter []*uaParser
@@ -32,6 +34,7 @@ type uaParser struct {
V1Replacement string `yaml:"v1_replacement"`
V2Replacement string `yaml:"v2_replacement"`
V3Replacement string `yaml:"v3_replacement"`
_ [4]byte // padding for alignment
MatchesCount uint64
}
@@ -64,6 +67,7 @@ type osParser struct {
V2Replacement string `yaml:"os_v2_replacement"`
V3Replacement string `yaml:"os_v3_replacement"`
V4Replacement string `yaml:"os_v4_replacement"`
_ [4]byte // padding for alignment
MatchesCount uint64
}
@@ -97,6 +101,7 @@ type deviceParser struct {
DeviceReplacement string `yaml:"device_replacement"`
BrandReplacement string `yaml:"brand_replacement"`
ModelReplacement string `yaml:"model_replacement"`
_ [4]byte // padding for alignment
MatchesCount uint64
}

View File

@@ -24,7 +24,7 @@ var definitionYaml = []byte(`user_agent_parsers:
family_replacement: 'PingdomBot'
- regex: '(PingdomTMS)/(\d+)\.(\d+)\.(\d+)'
family_replacement: 'PingdomBot'
- regex: ' (PTST)/(\d+)\.(\d+)$'
- regex: ' (PTST)/(\d+)(?:\.(\d+)|)$'
family_replacement: 'WebPageTest.org bot'
- regex: 'X11; (Datanyze); Linux'
- regex: '(NewRelicPinger)/(\d+)\.(\d+)'
@@ -54,24 +54,26 @@ var definitionYaml = []byte(`user_agent_parsers:
family_replacement: 'Pinterestbot'
- regex: '(CSimpleSpider|Cityreview Robot|CrawlDaddy|CrawlFire|Finderbots|Index crawler|Job Roboter|KiwiStatus Spider|Lijit Crawler|QuerySeekerSpider|ScollSpider|Trends Crawler|USyd-NLP-Spider|SiteCat Webbot|BotName\/\$BotVersion|123metaspider-Bot|1470\.net crawler|50\.nu|8bo Crawler Bot|Aboundex|Accoona-[A-z]{1,30}-Agent|AdsBot-Google(?:-[a-z]{1,30}|)|altavista|AppEngine-Google|archive.{0,30}\.org_bot|archiver|Ask Jeeves|[Bb]ai[Dd]u[Ss]pider(?:-[A-Za-z]{1,30})(?:-[A-Za-z]{1,30}|)|bingbot|BingPreview|blitzbot|BlogBridge|Bloglovin|BoardReader Blog Indexer|BoardReader Favicon Fetcher|boitho.com-dc|BotSeer|BUbiNG|\b\w{0,30}favicon\w{0,30}\b|\bYeti(?:-[a-z]{1,30}|)|Catchpoint(?: bot|)|[Cc]harlotte|Checklinks|clumboot|Comodo HTTP\(S\) Crawler|Comodo-Webinspector-Crawler|ConveraCrawler|CRAWL-E|CrawlConvera|Daumoa(?:-feedfetcher|)|Feed Seeker Bot|Feedbin|findlinks|Flamingo_SearchEngine|FollowSite Bot|furlbot|Genieo|gigabot|GomezAgent|gonzo1|(?:[a-zA-Z]{1,30}-|)Googlebot(?:-[a-zA-Z]{1,30}|)|Google SketchUp|grub-client|gsa-crawler|heritrix|HiddenMarket|holmes|HooWWWer|htdig|ia_archiver|ICC-Crawler|Icarus6j|ichiro(?:/mobile|)|IconSurf|IlTrovatore(?:-Setaccio|)|InfuzApp|Innovazion Crawler|InternetArchive|IP2[a-z]{1,30}Bot|jbot\b|KaloogaBot|Kraken|Kurzor|larbin|LEIA|LesnikBot|Linguee Bot|LinkAider|LinkedInBot|Lite Bot|Llaut|lycos|Mail\.RU_Bot|masscan|masidani_bot|Mediapartners-Google|Microsoft .{0,30} Bot|mogimogi|mozDex|MJ12bot|msnbot(?:-media {0,2}|)|msrbot|Mtps Feed Aggregation System|netresearch|Netvibes|NewsGator[^/]{0,30}|^NING|Nutch[^/]{0,30}|Nymesis|ObjectsSearch|Orbiter|OOZBOT|PagePeeker|PagesInventory|PaxleFramework|Peeplo Screenshot Bot|PlantyNet_WebRobot|Pompos|Qwantify|Read%20Later|Reaper|RedCarpet|Retreiver|Riddler|Rival IQ|scooter|Scrapy|Scrubby|searchsight|seekbot|semanticdiscovery|SemrushBot|Simpy|SimplePie|SEOstats|SimpleRSS|SiteCon|Slackbot-LinkExpanding|Slack-ImgProxy|Slurp|snappy|Speedy Spider|Squrl Java|Stringer|TheUsefulbot|ThumbShotsBot|Thumbshots\.ru|Tiny Tiny RSS|TwitterBot|WhatsApp|URL2PNG|Vagabondo|VoilaBot|^vortex|Votay bot|^voyager|WASALive.Bot|Web-sniffer|WebThumb|WeSEE:[A-z]{1,30}|WhatWeb|WIRE|WordPress|Wotbox|www\.almaden\.ibm\.com|Xenu(?:.s|) Link Sleuth|Xerka [A-z]{1,30}Bot|yacy(?:bot|)|YahooSeeker|Yahoo! Slurp|Yandex\w{1,30}|YodaoBot(?:-[A-z]{1,30}|)|YottaaMonitor|Yowedo|^Zao|^Zao-Crawler|ZeBot_www\.ze\.bz|ZooShot|ZyBorg)(?:[ /]v?(\d+)(?:\.(\d+)(?:\.(\d+)|)|)|)'
- regex: '\b(Boto3?|JetS3t|aws-(?:cli|sdk-(?:cpp|go|java|nodejs|ruby2?))|s3fs)/(\d+)\.(\d+)(?:\.(\d+)|)'
- regex: '(?:\/[A-Za-z0-9\.]+|) {0,5}([A-Za-z0-9 \-_\!\[\]:]{0,50}(?:[Aa]rchiver|[Ii]ndexer|[Ss]craper|[Bb]ot|[Ss]pider|[Cc]rawl[a-z]{0,50}))[/ ](\d+)(?:\.(\d+)(?:\.(\d+)|)|)'
- regex: '((?:[A-Za-z][A-Za-z0-9 -]{0,50}|)[^C][^Uu][Bb]ot)\b(?:(?:[ /]| v)(\d+)(?:\.(\d+)|)(?:\.(\d+)|)|)'
- regex: '((?:[A-z0-9]{1,50}|[A-z\-]{1,50} ?|)(?: the |)(?:[Ss][Pp][Ii][Dd][Ee][Rr]|[Ss]crape|[Cc][Rr][Aa][Ww][Ll])[A-z0-9]{0,50})(?:(?:[ /]| v)(\d+)(?:\.(\d+)|)(?:\.(\d+)|)|)'
- regex: '(HbbTV)/(\d+)\.(\d+)\.(\d+) \('
- regex: '(Chimera|SeaMonkey|Camino|Waterfox)/(\d+)\.(\d+)\.?([ab]?\d+[a-z]*|)'
- regex: '\[(FBAN/MessengerForiOS|FB_IAB/MESSENGER);FBAV/(\d+)(?:\.(\d+)(?:\.(\d+)|)|)'
family_replacement: 'Facebook Messenger'
- regex: '\[FB.*;(FBAV)/(\d+)(?:\.(\d+)|)(?:\.(\d+)|)'
family_replacement: 'Facebook'
- regex: '\[FB.*;'
family_replacement: 'Facebook'
- regex: '(?:\/[A-Za-z0-9\.]+|) {0,5}([A-Za-z0-9 \-_\!\[\]:]{0,50}(?:[Aa]rchiver|[Ii]ndexer|[Ss]craper|[Bb]ot|[Ss]pider|[Cc]rawl[a-z]{0,50}))[/ ](\d+)(?:\.(\d+)(?:\.(\d+)|)|)'
- regex: '((?:[A-Za-z][A-Za-z0-9 -]{0,50}|)[^C][^Uu][Bb]ot)\b(?:(?:[ /]| v)(\d+)(?:\.(\d+)|)(?:\.(\d+)|)|)'
- regex: '((?:[A-z0-9]{1,50}|[A-z\-]{1,50} ?|)(?: the |)(?:[Ss][Pp][Ii][Dd][Ee][Rr]|[Ss]crape|[Cc][Rr][Aa][Ww][Ll])[A-z0-9]{0,50})(?:(?:[ /]| v)(\d+)(?:\.(\d+)|)(?:\.(\d+)|)|)'
- regex: '(HbbTV)/(\d+)\.(\d+)\.(\d+) \('
- regex: '(Chimera|SeaMonkey|Camino|Waterfox)/(\d+)\.(\d+)\.?([ab]?\d+[a-z]*|)'
- regex: '(SailfishBrowser)/(\d+)\.(\d+)(?:\.(\d+)|)'
family_replacement: 'Sailfish Browser'
- regex: '\[(Pinterest)/[^\]]+\]'
- regex: '(Pinterest)(?: for Android(?: Tablet|)|)/(\d+)(?:\.(\d+)|)(?:\.(\d+)|)'
- regex: 'Mozilla.*Mobile.*(Instagram).(\d+)\.(\d+)\.(\d+)'
- regex: 'Mozilla.*Mobile.*(Flipboard).(\d+)\.(\d+)\.(\d+)'
- regex: 'Mozilla.*Mobile.*(Flipboard-Briefing).(\d+)\.(\d+)\.(\d+)'
- regex: 'Mozilla.*Mobile.*(Onefootball)\/Android.(\d+)\.(\d+)\.(\d+)'
- regex: '(Snapchat)\/(\d+)\.(\d+)\.(\d+).(\d+)'
- regex: '(Snapchat)\/(\d+)\.(\d+)\.(\d+)\.(\d+)'
- regex: '(Firefox)/(\d+)\.(\d+) Basilisk/(\d+)'
family_replacement: 'Basilisk'
- regex: '(PaleMoon)/(\d+)\.(\d+)(?:\.(\d+)|)'
@@ -232,6 +234,7 @@ var definitionYaml = []byte(`user_agent_parsers:
family_replacement: 'Whale'
- regex: '(Whale)/(\d+)\.(\d+)\.(\d+)'
family_replacement: 'Whale'
- regex: '(Ghost)/(\d+)\.(\d+)\.(\d+)'
- regex: '(Slack_SSB)/(\d+)\.(\d+)\.(\d+)'
family_replacement: 'Slack Desktop Client'
- regex: '(HipChat)/?(\d+|)'
@@ -249,6 +252,7 @@ var definitionYaml = []byte(`user_agent_parsers:
- regex: 'Microsoft Outlook (?:Mail )?16\.\d+\.\d+'
family_replacement: 'Outlook'
v1_replacement: '2016'
- regex: 'Microsoft Office (Word) 2014'
- regex: 'Outlook-Express\/7\.0.*'
family_replacement: 'Windows Live Mail'
- regex: '(Airmail) (\d+)\.(\d+)(?:\.(\d+)|)'
@@ -261,7 +265,8 @@ var definitionYaml = []byte(`user_agent_parsers:
- regex: '(Lotus-Notes)/(\d+)\.(\d+)(?:\.(\d+)|)'
family_replacement: 'Lotus Notes'
- regex: '(Vivaldi)/(\d+)\.(\d+)\.(\d+)'
- regex: '(Edge)/(\d+)(?:\.(\d+)|)'
- regex: '(Edge?)/(\d+)(?:\.(\d+)|)(?:\.(\d+)|)(?:\.(\d+)|)'
family_replacement: 'Edge'
- regex: '(brave)/(\d+)\.(\d+)\.(\d+) Chrome'
family_replacement: 'Brave'
- regex: '(Chrome)/(\d+)\.(\d+)\.(\d+)[\d.]* Iron[^/]'
@@ -379,6 +384,8 @@ var definitionYaml = []byte(`user_agent_parsers:
family_replacement: 'Mobile Safari'
- regex: '(iPod|iPhone|iPad)'
family_replacement: 'Mobile Safari UI/WKWebView'
- regex: '(Watch)(\d+),(\d+)'
family_replacement: 'Apple $1 App'
- regex: '(Outlook-iOS)/\d+\.\d+\.prod\.iphone \((\d+)\.(\d+)\.(\d+)\)'
- regex: '(AvantGo) (\d+).(\d+)'
- regex: '(OneBrowser)/(\d+).(\d+)'
@@ -527,6 +534,9 @@ os_parsers:
os_replacement: 'Windows Phone'
- regex: '(Windows ?Mobile)'
os_replacement: 'Windows Mobile'
- regex: '(Windows 10)'
os_replacement: 'Windows'
os_v1_replacement: '10'
- regex: '(Windows (?:NT 5\.2|NT 5\.1))'
os_replacement: 'Windows'
os_v1_replacement: 'XP'
@@ -559,9 +569,6 @@ os_parsers:
- regex: '(Windows NT 10\.0)'
os_replacement: 'Windows'
os_v1_replacement: '10'
- regex: '(Windows 10)'
os_replacement: 'Windows'
os_v1_replacement: '10'
- regex: '(Windows NT 5\.0)'
os_replacement: 'Windows'
os_v1_replacement: '2000'
@@ -784,6 +791,8 @@ os_parsers:
- regex: '\b(iOS[ /]|iOS; |iPhone(?:/| v|[ _]OS[/,]|; | OS : |\d,\d/|\d,\d; )|iPad/)(\d{1,2})[_\.](\d{1,2})(?:[_\.](\d+)|)'
os_replacement: 'iOS'
- regex: '\((iOS);'
- regex: '(watchOS)/(\d+)\.(\d+)(?:\.(\d+)|)'
os_replacement: 'WatchOS'
- regex: 'Outlook-(iOS)/\d+\.\d+\.prod\.iphone'
- regex: '(iPod|iPhone|iPad)'
os_replacement: 'iOS'
@@ -868,7 +877,7 @@ os_parsers:
- regex: '(hpw|web)OS/(\d+)\.(\d+)(?:\.(\d+)|)'
os_replacement: 'webOS'
- regex: '(VRE);'
- regex: '(Fedora|Red Hat|PCLinuxOS|Puppy|Ubuntu|Kindle|Bada|Lubuntu|BackTrack|Slackware|(?:Free|Open|Net|\b)BSD)[/ ](\d+)\.(\d+)(?:\.(\d+)|)(?:\.(\d+)|)'
- regex: '(Fedora|Red Hat|PCLinuxOS|Puppy|Ubuntu|Kindle|Bada|Sailfish|Lubuntu|BackTrack|Slackware|(?:Free|Open|Net|\b)BSD)[/ ](\d+)\.(\d+)(?:\.(\d+)|)(?:\.(\d+)|)'
- regex: '(Linux)[ /](\d+)\.(\d+)(?:\.(\d+)|).*gentoo'
os_replacement: 'Gentoo'
- regex: '\((Bada);'
@@ -895,7 +904,7 @@ device_parsers:
device_replacement: 'Spider'
brand_replacement: 'Spider'
model_replacement: 'Feature Phone'
- regex: ' PTST/\d+\.\d+$'
- regex: ' PTST/\d+(?:\.)?\d+$'
device_replacement: 'Spider'
brand_replacement: 'Spider'
- regex: 'X11; Datanyze; Linux'
@@ -1539,6 +1548,10 @@ device_parsers:
device_replacement: 'Huawei $1'
brand_replacement: 'Huawei'
model_replacement: '$1'
- regex: '; *((?:[A-Z]{3})\-L[A-Za0-9]{2})[\)]'
device_replacement: 'Huawei $1'
brand_replacement: 'Huawei'
model_replacement: '$1'
- regex: '; *HTC[ _]([^;]+); Windows Phone'
device_replacement: 'HTC $1'
brand_replacement: 'HTC'
@@ -2135,7 +2148,7 @@ device_parsers:
device_replacement: 'OnePlus $1'
brand_replacement: 'OnePlus'
model_replacement: '$1'
- regex: '; (ONEPLUS [a-zA-Z]\d+) Build/'
- regex: '; (ONEPLUS [a-zA-Z]\d+)(?: Build/|)'
device_replacement: 'OnePlus $1'
brand_replacement: 'OnePlus'
model_replacement: '$1'
@@ -2324,6 +2337,10 @@ device_parsers:
device_replacement: 'Samsung $1'
brand_replacement: 'Samsung'
model_replacement: '$1'
- regex: '; *((?:SC)\-[A-Za-z0-9 ]+)(/?[^ ]*|)\)'
device_replacement: 'Samsung $1'
brand_replacement: 'Samsung'
model_replacement: '$1'
- regex: ' ((?:SCH)\-[A-Za-z0-9 ]+)(/?[^ ]*|) Build'
device_replacement: 'Samsung $1'
brand_replacement: 'Samsung'
@@ -2332,6 +2349,10 @@ device_parsers:
device_replacement: 'Samsung $1'
brand_replacement: 'Samsung'
model_replacement: '$1'
- regex: '; *((?:SCH|SGH|SHV|SHW|SPH|SC|SM)\-[A-Za-z0-9]{5,6})[\)]'
device_replacement: 'Samsung $1'
brand_replacement: 'Samsung'
model_replacement: '$1'
- regex: '; *(SH\-?\d\d[^;/]+|SBM\d[^;/]+) Build'
device_replacement: '$1'
brand_replacement: 'Sharp'
@@ -2667,6 +2688,18 @@ device_parsers:
device_replacement: 'XiaoMi $1'
brand_replacement: 'XiaoMi'
model_replacement: '$1'
- regex: '; *((Mi|MI|HM|MI-ONE|Redmi)[ -](NOTE |Note |)[^;/\)]*)'
device_replacement: 'XiaoMi $1'
brand_replacement: 'XiaoMi'
model_replacement: '$1'
- regex: '; *(MIX) (Build|MIUI)/'
device_replacement: 'XiaoMi $1'
brand_replacement: 'XiaoMi'
model_replacement: '$1'
- regex: '; *((MIX) ([^;/]*)) (Build|MIUI)/'
device_replacement: 'XiaoMi $1'
brand_replacement: 'XiaoMi'
model_replacement: '$1'
- regex: '; *XOLO[ _]([^;/]*tab.*) Build'
regex_flag: 'i'
device_replacement: 'Xolo $1'
@@ -2978,6 +3011,10 @@ device_parsers:
device_replacement: '$1'
brand_replacement: 'Apple'
model_replacement: '$1'
- regex: '(Watch)(\d+,\d+)'
device_replacement: 'Apple $1'
brand_replacement: 'Apple'
model_replacement: 'Apple $1 $2'
- regex: '(Apple Watch)(?:;| Simulator;)'
device_replacement: '$1'
brand_replacement: 'Apple'
@@ -3026,6 +3063,10 @@ device_parsers:
device_replacement: 'Asus $1'
brand_replacement: 'Asus'
model_replacement: '$1'
- regex: '(?:ASUS)_([A-Za-z0-9\-]+)'
device_replacement: 'Asus $1'
brand_replacement: 'Asus'
model_replacement: '$1'
- regex: '\bBIRD[ \-\.]([A-Za-z0-9]+)'
device_replacement: 'Bird $1'
brand_replacement: 'Bird'
@@ -3058,6 +3099,10 @@ device_parsers:
device_replacement: 'Huawei $1'
brand_replacement: 'Huawei'
model_replacement: '$1'
- regex: 'HUAWEI ([A-Za-z0-9\-]+)'
device_replacement: 'Huawei $1'
brand_replacement: 'Huawei'
model_replacement: '$1'
- regex: 'vodafone([A-Za-z0-9]+)'
device_replacement: 'Huawei Vodafone $1'
brand_replacement: 'Huawei'
@@ -3235,26 +3280,26 @@ device_parsers:
device_replacement: 'Generic Smartphone'
brand_replacement: 'Generic'
model_replacement: 'Smartphone'
- regex: 'Android[\- ][\d]+\.[\d]+; [A-Za-z]{2}\-[A-Za-z]{0,2}; WOWMobile (.+) Build[/ ]'
- regex: 'Android[\- ][\d]+\.[\d]+; [A-Za-z]{2}\-[A-Za-z]{0,2}; WOWMobile (.+)( Build[/ ]|\))'
brand_replacement: 'Generic_Android'
model_replacement: '$1'
- regex: 'Android[\- ][\d]+\.[\d]+\-update1; [A-Za-z]{2}\-[A-Za-z]{0,2} *; *(.+?) Build[/ ]'
- regex: 'Android[\- ][\d]+\.[\d]+\-update1; [A-Za-z]{2}\-[A-Za-z]{0,2} *; *(.+?)( Build[/ ]|\))'
brand_replacement: 'Generic_Android'
model_replacement: '$1'
- regex: 'Android[\- ][\d]+(?:\.[\d]+)(?:\.[\d]+|); *[A-Za-z]{2}[_\-][A-Za-z]{0,2}\-? *; *(.+?) Build[/ ]'
- regex: 'Android[\- ][\d]+(?:\.[\d]+)(?:\.[\d]+|); *[A-Za-z]{2}[_\-][A-Za-z]{0,2}\-? *; *(.+?)( Build[/ ]|\))'
brand_replacement: 'Generic_Android'
model_replacement: '$1'
- regex: 'Android[\- ][\d]+(?:\.[\d]+)(?:\.[\d]+|); *[A-Za-z]{0,2}\- *; *(.+?) Build[/ ]'
- regex: 'Android[\- ][\d]+(?:\.[\d]+)(?:\.[\d]+|); *[A-Za-z]{0,2}\- *; *(.+?)( Build[/ ]|\))'
brand_replacement: 'Generic_Android'
model_replacement: '$1'
- regex: 'Android[\- ][\d]+(?:\.[\d]+)(?:\.[\d]+|); *[a-z]{0,2}[_\-]?[A-Za-z]{0,2};? Build[/ ]'
- regex: 'Android[\- ][\d]+(?:\.[\d]+)(?:\.[\d]+|); *[a-z]{0,2}[_\-]?[A-Za-z]{0,2};?( Build[/ ]|\))'
device_replacement: 'Generic Smartphone'
brand_replacement: 'Generic'
model_replacement: 'Smartphone'
- regex: 'Android[\- ][\d]+(?:\.[\d]+)(?:\.[\d]+|); *\-?[A-Za-z]{2}; *(.+?) Build[/ ]'
- regex: 'Android[\- ][\d]+(?:\.[\d]+)(?:\.[\d]+|); *\-?[A-Za-z]{2}; *(.+?)( Build[/ ]|\))'
brand_replacement: 'Generic_Android'
model_replacement: '$1'
- regex: 'Android[\- ][\d]+(?:\.[\d]+)(?:\.[\d]+|)(?:;.*|); *(.+?) Build[/ ]'
- regex: 'Android[\- ][\d]+(?:\.[\d]+)(?:\.[\d]+|)(?:;.*|); *(.+?)( Build[/ ]|\))'
brand_replacement: 'Generic_Android'
model_replacement: '$1'
- regex: '(GoogleTV)'

2
vendor/modules.txt vendored
View File

@@ -205,7 +205,7 @@ github.com/stretchr/testify/require
github.com/stretchr/testify/assert
# github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf
github.com/teris-io/shortid
# github.com/ua-parser/uap-go v0.0.0-20190303233514-1004ccd816b3
# github.com/ua-parser/uap-go v0.0.0-20190826212731-daf92ba38329
github.com/ua-parser/uap-go/uaparser
# github.com/uber/jaeger-client-go v2.16.0+incompatible
github.com/uber/jaeger-client-go/config