Compare commits
41 Commits
v6.3.0-bet
...
v6.3.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f77a692de5 | ||
|
|
d433e21275 | ||
|
|
ede3adc7d4 | ||
|
|
fdd211758e | ||
|
|
5d6512a7a2 | ||
|
|
b467557614 | ||
|
|
2148a9ff6e | ||
|
|
dc6219d8e0 | ||
|
|
67bad726f1 | ||
|
|
3f8624bffb | ||
|
|
7f1db70213 | ||
|
|
b2d86c76c6 | ||
|
|
8c168a6b83 | ||
|
|
f02d6c7be2 | ||
|
|
496d0323bd | ||
|
|
f455f02318 | ||
|
|
1e58fdaffd | ||
|
|
c27fd346d2 | ||
|
|
59fa8cc82e | ||
|
|
a557646484 | ||
|
|
be2e2330f5 | ||
|
|
84d0a71b25 | ||
|
|
e0ee72a2ff | ||
|
|
881c229ee3 | ||
|
|
9d97f48374 | ||
|
|
39f00259f3 | ||
|
|
84022650cb | ||
|
|
e368080dea | ||
|
|
a02c2b21d2 | ||
|
|
3a58974314 | ||
|
|
5954cb7220 | ||
|
|
f24ef80e52 | ||
|
|
917b278e45 | ||
|
|
483246016b | ||
|
|
43fe057baa | ||
|
|
f2fffadcd6 | ||
|
|
de06c1c1b8 | ||
|
|
830da0fda0 | ||
|
|
78fff0161a | ||
|
|
06d4641a8f | ||
|
|
e232629917 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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):
|
||||

|
||||
|
||||
Datas source selection & options & help are now above your metric queries.
|
||||
Data source selection & options & help are now above your metric queries.
|
||||

|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -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
4
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' }],
|
||||
|
||||
@@ -29,6 +29,7 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
|
||||
fields: [
|
||||
{
|
||||
name: timeSeries.target || 'Value',
|
||||
type: FieldType.number,
|
||||
unit: timeSeries.unit,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -77,7 +77,7 @@ export class CustomScrollbar extends Component<Props> {
|
||||
{...passedProps}
|
||||
className={cx(
|
||||
css`
|
||||
visibility: ${hideTrack ? 'none' : 'visible'};
|
||||
visibility: ${hideTrack ? 'hidden' : 'visible'};
|
||||
`,
|
||||
track
|
||||
)}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
{}, {},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
|
||||
@@ -415,6 +415,10 @@ describe('DashboardModel', () => {
|
||||
dashUri: '',
|
||||
title: 'test',
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
keepTime: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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[];
|
||||
};
|
||||
61
public/app/features/explore/QueryField.test.tsx
Normal file
61
public/app/features/explore/QueryField.test.tsx
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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]))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {} },
|
||||
};
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
44
public/app/plugins/datasource/prometheus/query_hints.test.ts
Normal file
44
public/app/plugins/datasource/prometheus/query_hints.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -65,6 +65,7 @@ export class GraphContextMenuCtrl {
|
||||
setSource = (source: FlotDataPoint | null) => {
|
||||
this.source = source;
|
||||
};
|
||||
|
||||
getSource = () => {
|
||||
return this.source;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
_version="1.2.7"
|
||||
_version="1.2.8"
|
||||
_tag="grafana/build-container:${_version}"
|
||||
|
||||
docker build -t $_tag .
|
||||
|
||||
5
vendor/github.com/ua-parser/uap-go/uaparser/parser.go
generated
vendored
5
vendor/github.com/ua-parser/uap-go/uaparser/parser.go
generated
vendored
@@ -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
|
||||
}
|
||||
|
||||
|
||||
87
vendor/github.com/ua-parser/uap-go/uaparser/yaml.go
generated
vendored
87
vendor/github.com/ua-parser/uap-go/uaparser/yaml.go
generated
vendored
@@ -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
2
vendor/modules.txt
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user