Merge remote-tracking branch 'origin' into ivana/remove-deduplication

This commit is contained in:
Ivana
2020-12-08 15:29:35 +01:00
615 changed files with 15880 additions and 10838 deletions
+142 -149
View File
File diff suppressed because it is too large Load Diff
+4 -7
View File
@@ -1,6 +1,9 @@
{
"extends": ["@grafana/eslint-config"],
"root": true,
"plugins": [
"no-only-tests"
],
"overrides": [
{
"files": [
@@ -12,13 +15,7 @@
"react-hooks/rules-of-hooks": "off",
"react-hooks/exhaustive-deps": "off",
"react/prop-types": "off",
"react/no-unescaped-entities": "off",
"react/display-name": "off",
"react/no-deprecated": "off",
"react/no-unknown-property": "off",
"react/no-children-prop": "off",
"react/no-find-dom-node": "off",
"react/no-render-return-value": "off"
"no-only-tests/no-only-tests": "error"
}
}
]
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
uses: actions/setup-node@v1
id: generate-docs
with:
node-version: '12'
node-version: '14'
- run: yarn install --pure-lockfile --no-progress
- run: ./scripts/ci-reference-docs-build.sh
- name: publish-to-git
@@ -41,4 +41,4 @@ jobs:
- shell: bash
run: |
test -n "${{ steps.publish.outputs.commit_hash }}"
test -n "${{ steps.publish.outputs.working_directory }}"
test -n "${{ steps.publish.outputs.working_directory }}"
+1 -1
View File
@@ -59,7 +59,7 @@ In order not to break all angular panel plugins and data sources we have some cu
* **Dashboard**: Fixes kiosk state after being redirected to login page and back. [#29273](https://github.com/grafana/grafana/pull/29273), [@torkelo](https://github.com/torkelo)
* **InfluxDB**: Update flux library to fix support for boolean label values. [#29310](https://github.com/grafana/grafana/pull/29310), [@ryantxu](https://github.com/ryantxu)
* **Security**: Fixes minor security issue with alert notification webhooks that allowed GET & DELETE requests. [#29330](https://github.com/grafana/grafana/pull/29330), [@wbrowne](https://github.com/wbrowne)
* **Table**: Fix incorrect condition for rendering table filter. [#29165](https://github.com/grafana/grafana/pull/29165), [@dprokop](https://github.com/dprokop)
* **Table**: Fixes issues with phantom extra 0 for zero values. [#29165](https://github.com/grafana/grafana/pull/29165), [@dprokop](https://github.com/dprokop)
<!-- 7.3.4 END -->
+4 -3
View File
@@ -1,4 +1,4 @@
FROM node:12.19.0-alpine3.12 as js-builder
FROM node:14.15.1-alpine3.12 as js-builder
WORKDIR /usr/src/app/
@@ -7,14 +7,14 @@ COPY packages packages
RUN yarn install --pure-lockfile --no-progress
COPY Gruntfile.js tsconfig.json .eslintrc .editorconfig .browserslistrc .prettierrc.js ./
COPY tsconfig.json .eslintrc .editorconfig .browserslistrc .prettierrc.js ./
COPY public public
COPY tools tools
COPY scripts scripts
COPY emails emails
ENV NODE_ENV production
RUN ./node_modules/.bin/grunt build
RUN yarn build
FROM golang:1.15.1-alpine3.12 as go-builder
@@ -64,6 +64,7 @@ RUN export GF_GID_NAME=$(getent group $GF_GID | cut -d':' -f1) && \
mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
"$GF_PATHS_PROVISIONING/dashboards" \
"$GF_PATHS_PROVISIONING/notifiers" \
"$GF_PATHS_PROVISIONING/plugins" \
"$GF_PATHS_LOGS" \
"$GF_PATHS_PLUGINS" \
"$GF_PATHS_DATA" && \
+4 -3
View File
@@ -1,4 +1,4 @@
FROM node:12.19.0-slim AS js-builder
FROM node:14.15.1-slim AS js-builder
WORKDIR /usr/src/app/
@@ -7,14 +7,14 @@ COPY packages packages
RUN yarn install --pure-lockfile
COPY Gruntfile.js tsconfig.json .eslintrc .editorconfig .browserslistrc .prettierrc.js ./
COPY tsconfig.json .eslintrc .editorconfig .browserslistrc .prettierrc.js ./
COPY public public
COPY tools tools
COPY scripts scripts
COPY emails emails
ENV NODE_ENV production
RUN ./node_modules/.bin/grunt build
RUN yarn build
FROM golang:1.15.1 AS go-builder
@@ -58,6 +58,7 @@ RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
"$GF_PATHS_PROVISIONING/dashboards" \
"$GF_PATHS_PROVISIONING/notifiers" \
"$GF_PATHS_PROVISIONING/plugins" \
"$GF_PATHS_LOGS" \
"$GF_PATHS_PLUGINS" \
"$GF_PATHS_DATA" && \
-66
View File
@@ -1,66 +0,0 @@
'use strict';
module.exports = function(grunt) {
var os = require('os');
var config = {
pkg: grunt.file.readJSON('package.json'),
baseDir: '.',
srcDir: 'public',
genDir: 'public_gen',
destDir: 'dist',
tempDir: 'tmp',
platform: process.platform.replace('win32', 'windows'),
enterprise: false,
libc: null,
};
if (grunt.option('platform')) {
config.platform = grunt.option('platform');
}
if (grunt.option('enterprise')) {
config.enterprise = true;
}
if (grunt.option('arch')) {
config.arch = grunt.option('arch');
} else {
config.arch = os.arch();
if (process.platform.match(/^win/)) {
config.arch = process.env.hasOwnProperty('ProgramFiles(x86)') ? 'x64' : 'x86';
}
}
if (grunt.option('libc')) {
config.libc = grunt.option('libc');
}
config.pkg.version = grunt.option('pkgVer') || config.pkg.version;
console.log('Version', config.pkg.version);
// load plugins
require('load-grunt-tasks')(grunt);
// load task definitions
grunt.loadTasks('./scripts/grunt');
// Utility function to load plugin settings into config
function loadConfig(config, path) {
require('glob')
.sync('*', { cwd: path })
.forEach(function(option) {
var key = option.replace(/\.js$/, '');
// If key already exists, extend it. It is your responsibility to avoid naming collisions
config[key] = config[key] || {};
grunt.util._.extend(config[key], require(path + option)(config, grunt));
});
// technically not required
return config;
}
// Merge that object with what with whatever we have here
loadConfig(config, './scripts/grunt/options/');
// pass the config to grunt
grunt.initConfig(config);
};
+10 -249
View File
@@ -30,13 +30,13 @@ const (
var (
//versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`)
goarch string
goos string
gocc string
cgo bool
libc string
pkgArch string
version string = "v1"
goarch string
goos string
gocc string
cgo bool
libc string
pkgArch string
version string = "v1"
buildTags []string
// deb & rpm does not support semver so have to handle their version a little differently
linuxPackageVersion string = "v1"
@@ -125,34 +125,7 @@ func main() {
}
case "build-frontend":
grunt(gruntBuildArg("build")...)
case "test":
test("./pkg/...")
grunt("test")
case "package":
grunt(gruntBuildArg("build")...)
grunt(gruntBuildArg("package")...)
if goos == linux {
createLinuxPackages()
}
case "package-only":
grunt(gruntBuildArg("package")...)
if goos == linux {
createLinuxPackages()
}
case "pkg-archive":
grunt(gruntBuildArg("package")...)
case "pkg-rpm":
grunt(gruntBuildArg("release")...)
createRpmPackages()
case "pkg-deb":
grunt(gruntBuildArg("release")...)
createDebPackages()
yarn("build")
case "sha-dist":
shaFilesInDist()
@@ -233,202 +206,8 @@ func readVersionFromPackageJson() {
}
}
type linuxPackageOptions struct {
packageType string
packageArch string
homeDir string
homeBinDir string
binPath string
serverBinPath string
cliBinPath string
configDir string
ldapFilePath string
etcDefaultPath string
etcDefaultFilePath string
initdScriptFilePath string
systemdServiceFilePath string
postinstSrc string
initdScriptSrc string
defaultFileSrc string
systemdFileSrc string
cliBinaryWrapperSrc string
depends []string
}
func createDebPackages() {
debPkgArch := pkgArch
if pkgArch == "armv7" || pkgArch == "armv6" {
debPkgArch = "armhf"
}
createPackage(linuxPackageOptions{
packageType: "deb",
packageArch: debPkgArch,
homeDir: "/usr/share/grafana",
homeBinDir: "/usr/share/grafana/bin",
binPath: "/usr/sbin",
configDir: "/etc/grafana",
etcDefaultPath: "/etc/default",
etcDefaultFilePath: "/etc/default/grafana-server",
initdScriptFilePath: "/etc/init.d/grafana-server",
systemdServiceFilePath: "/usr/lib/systemd/system/grafana-server.service",
postinstSrc: "packaging/deb/control/postinst",
initdScriptSrc: "packaging/deb/init.d/grafana-server",
defaultFileSrc: "packaging/deb/default/grafana-server",
systemdFileSrc: "packaging/deb/systemd/grafana-server.service",
cliBinaryWrapperSrc: "packaging/wrappers/grafana-cli",
depends: []string{"adduser", "libfontconfig1"},
})
}
func createRpmPackages() {
rpmPkgArch := pkgArch
switch {
case pkgArch == "armv7":
rpmPkgArch = "armhfp"
case pkgArch == "arm64":
rpmPkgArch = "aarch64"
}
createPackage(linuxPackageOptions{
packageType: "rpm",
packageArch: rpmPkgArch,
homeDir: "/usr/share/grafana",
homeBinDir: "/usr/share/grafana/bin",
binPath: "/usr/sbin",
configDir: "/etc/grafana",
etcDefaultPath: "/etc/sysconfig",
etcDefaultFilePath: "/etc/sysconfig/grafana-server",
initdScriptFilePath: "/etc/init.d/grafana-server",
systemdServiceFilePath: "/usr/lib/systemd/system/grafana-server.service",
postinstSrc: "packaging/rpm/control/postinst",
initdScriptSrc: "packaging/rpm/init.d/grafana-server",
defaultFileSrc: "packaging/rpm/sysconfig/grafana-server",
systemdFileSrc: "packaging/rpm/systemd/grafana-server.service",
cliBinaryWrapperSrc: "packaging/wrappers/grafana-cli",
depends: []string{"/sbin/service", "fontconfig", "freetype", "urw-fonts"},
})
}
func createLinuxPackages() {
if !skipDebGen {
createDebPackages()
}
if !skipRpmGen {
createRpmPackages()
}
}
func createPackage(options linuxPackageOptions) {
packageRoot, _ := ioutil.TempDir("", "grafana-linux-pack")
// create directories
runPrint("mkdir", "-p", filepath.Join(packageRoot, options.homeDir))
runPrint("mkdir", "-p", filepath.Join(packageRoot, options.configDir))
runPrint("mkdir", "-p", filepath.Join(packageRoot, "/etc/init.d"))
runPrint("mkdir", "-p", filepath.Join(packageRoot, options.etcDefaultPath))
runPrint("mkdir", "-p", filepath.Join(packageRoot, "/usr/lib/systemd/system"))
runPrint("mkdir", "-p", filepath.Join(packageRoot, "/usr/sbin"))
// copy grafana-cli wrapper
runPrint("cp", "-p", options.cliBinaryWrapperSrc, filepath.Join(packageRoot, "/usr/sbin/"+cliBinary))
// copy grafana-server binary
runPrint("cp", "-p", filepath.Join(workingDir, "tmp/bin/"+serverBinary), filepath.Join(packageRoot, "/usr/sbin/"+serverBinary))
// copy init.d script
runPrint("cp", "-p", options.initdScriptSrc, filepath.Join(packageRoot, options.initdScriptFilePath))
// copy environment var file
runPrint("cp", "-p", options.defaultFileSrc, filepath.Join(packageRoot, options.etcDefaultFilePath))
// copy systemd file
runPrint("cp", "-p", options.systemdFileSrc, filepath.Join(packageRoot, options.systemdServiceFilePath))
// copy release files
runPrint("cp", "-a", filepath.Join(workingDir, "tmp")+"/.", filepath.Join(packageRoot, options.homeDir))
// remove bin path
runPrint("rm", "-rf", filepath.Join(packageRoot, options.homeDir, "bin"))
// create /bin within home
runPrint("mkdir", "-p", filepath.Join(packageRoot, options.homeBinDir))
// The grafana-cli binary is exposed through a wrapper to ensure a proper
// configuration is in place. To enable that, we need to store the original
// binary in a separate location to avoid conflicts.
runPrint("cp", "-p", filepath.Join(workingDir, "tmp/bin/"+cliBinary), filepath.Join(packageRoot, options.homeBinDir, cliBinary))
args := []string{
"-s", "dir",
"--description", "Grafana",
"-C", packageRoot,
"--url", "https://grafana.com",
"--maintainer", "contact@grafana.com",
"--config-files", options.initdScriptFilePath,
"--config-files", options.etcDefaultFilePath,
"--config-files", options.systemdServiceFilePath,
"--after-install", options.postinstSrc,
"--version", linuxPackageVersion,
"-p", "./dist",
}
name := "grafana"
if enterprise {
name += "-enterprise"
args = append(args, "--replaces", "grafana")
}
fmt.Printf("pkgArch is set to '%s', generated arch is '%s'\n", pkgArch, options.packageArch)
if pkgArch == "armv6" {
name += "-rpi"
args = append(args, "--replaces", "grafana")
}
args = append(args, "--name", name)
description := "Grafana"
if enterprise {
description += " Enterprise"
}
if !enterprise {
args = append(args, "--license", "\"Apache 2.0\"")
}
if options.packageType == "rpm" {
args = append(args, "--rpm-posttrans", "packaging/rpm/control/posttrans")
}
if options.packageType == "deb" {
args = append(args, "--deb-no-default-config-files")
}
if options.packageArch != "" {
args = append(args, "-a", options.packageArch)
}
if linuxPackageIteration != "" {
args = append(args, "--iteration", linuxPackageIteration)
}
// add dependencies
for _, dep := range options.depends {
args = append(args, "--depends", dep)
}
args = append(args, ".")
fmt.Println("Creating package: ", options.packageType)
runPrint("fpm", append([]string{"-t", options.packageType}, args...)...)
}
func grunt(params ...string) {
if runtime.GOOS == windows {
runPrint(`.\node_modules\.bin\grunt`, params...)
} else {
runPrint("./node_modules/.bin/grunt", params...)
}
func yarn(params ...string) {
runPrint(`yarn run`, params...)
}
func genPackageVersion() string {
@@ -439,24 +218,6 @@ func genPackageVersion() string {
}
}
func gruntBuildArg(task string) []string {
args := []string{task}
args = append(args, fmt.Sprintf("--pkgVer=%v", genPackageVersion()))
if pkgArch != "" {
args = append(args, fmt.Sprintf("--arch=%v", pkgArch))
}
if libc != "" {
args = append(args, fmt.Sprintf("--libc=%s", libc))
}
if enterprise {
args = append(args, "--enterprise")
}
args = append(args, fmt.Sprintf("--platform=%v", goos))
return args
}
func setup() {
args := []string{"install", "-v"}
if goos == windows {
+1 -1
View File
@@ -20,7 +20,7 @@ We recommend using [Homebrew](https://brew.sh/) for installing any missing depen
```
brew install git
brew install go
brew install node@12
brew install node@14
npm install -g yarn
```
@@ -116,12 +116,29 @@ Build a list of points - an unordered or unnumbered list - by using "\-" (hyphen
## Images
_Do not_ use image shortcodes at this time.
Include images in a document using the following syntax:
**Example** \!\[Grafana Logo](/link/to/grafanalogo/logo.png)
```
![Alt text](link to image, starting with /img/docs/ if it is to an internal image)
```
**Examples:**
- \!\[Grafana Logo](/link/to/grafanalogo/logo.png)
- \!\[Example](/img/docs/folder_name/alert_test_rule.png)
This follows the format of "!", alt text wrapped in "[]" and the link URL wrapped in "()".
You can also use HTML such as the following:
```
<img src="example.png"
alt="Example image"
style="float: left; margin-right: 5px;" />
```
In most cases, use the markdown syntax rather than the HTML syntax. Only use the HTML if you need to change the image in ways unsupported by Markdown.
## Comments
You can include comments that will not appear in published markdown using the following syntax:
@@ -110,12 +110,13 @@ If that is the case, then use the following format for headings:
- Assume all graphics will be exclusively viewed on the web.
- Maximum image size is 3840px X 2160px.
- Screenshots should be readable, but not too large.
- _Do not_ use image shortcodes. Follow the guidance in the [Grafana markdown guide](https://github.com/grafana/grafana/blob/master/contribute/style-guides/documentation-markdown-guide.md#images).
- Markdown image links are preferred. Only use the HTML image links if you need to style the image in ways unsupported in Markdown.
### Capitalization
- Grafana, Loki, and Prometheus are always capitalized unless part of a code block.
- API names are always Title Case, followed by "API"—for example, "Dashboard Permissions API"
- Git is always capitalized, unless part of a code block.
- Abbreviations are always capitalized (such as API, HTTP, ID, JSON, SQL, or URL) unless they are part of a code block.
- Menu and submenu titles always use sentence case: capitalize the first word, and lowercase the rest.
- "Dashboards" when referring to the submenu title.
@@ -125,6 +126,22 @@ If that is the case, then use the following format for headings:
- Lowercase "dashboards" when referring to multiple dashboards.
- **Exceptions:** If a term is lowercased in the Grafana UI, then match the UI.
#### Git, GitHub
Git is always capitalized, unless part of a code block. GitHub is the correct spelling and capitalization.
#### Integrations
In general, "integration" is not capitalized. Only capitalize it if it is capitalized in the UI or part of a proper noun, like the name of a specific integration.
The first letter of the name of an integration is always capitalized, even if the original named source is lowercase.
**Examples:**
- MySQL Integration
- CockroachDB Integration
- Etcd Integration
- I installed an integration on my local Grafana.
### Links and references
When referencing another document, use "Refer to" rather than alternatives such as "See" or "Check out."
@@ -192,6 +209,18 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
Grafana products has some words, abbreviations, and terms particular to the Grafana discourse community.
#### changelog
One word, not two.
**Example**
- Read the full changelog.
**Exception:**
- When referring to the file containing the official changelog, use the filename: `CHANGELOG.md`.
#### checkout, check out
Two words if used as a verb, one word if used as a noun.
@@ -0,0 +1,116 @@
{
"__inputs": [
{
"name": "DS_GDEV-TESTDATA",
"label": "gdev-testdata",
"description": "",
"type": "datasource",
"pluginId": "testdata",
"pluginName": "TestData DB"
}
],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "7.4.0-pre"
},
{
"type": "datasource",
"id": "testdata",
"name": "TestData DB",
"version": "1.0.0"
},
{
"type": "panel",
"id": "text",
"name": "Text",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": null,
"iteration": 1606804991052,
"links": [],
"panels": [
{
"datasource": "${DS_GDEV-TESTDATA}",
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"content": "# variable: ${text}\n ",
"mode": "markdown"
},
"pluginVersion": "7.4.0-pre",
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"type": "text"
}
],
"schemaVersion": 27,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": "default value",
"value": "default value"
},
"description": null,
"error": null,
"hide": 0,
"label": null,
"name": "text",
"options": [
{
"selected": true,
"text": "default value",
"value": "default value"
}
],
"query": "default value",
"skipUrlSync": false,
"type": "textbox"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Templating - Textbox e2e scenarios",
"uid": "AejrN1AMz",
"version": 1
}
@@ -8,6 +8,7 @@
environment:
INFLUXDB_ADMIN_USER: grafana
INFLUXDB_ADMIN_PASSWORD: grafana
INFLUXDB_DB: site
volumes:
- ./docker/blocks/influxdb/influxdb.conf:/etc/influxdb/influxdb.conf
+1
View File
@@ -0,0 +1 @@
mysql_version=5.6
+2 -1
View File
@@ -1,3 +1,4 @@
FROM mysql:5.6
ARG mysql_version=5.6
FROM mysql:${mysql_version}
ADD setup.sql /docker-entrypoint-initdb.d
CMD ["mysqld"]
@@ -1,6 +1,8 @@
mysqltests:
build:
context: docker/blocks/mysql_tests
args:
- mysql_version=${mysql_version}
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: grafana_tests
+1
View File
@@ -0,0 +1 @@
postgres_version=9.3
@@ -1,3 +1,4 @@
FROM postgres:9.3
ARG postgres_version=9.3
FROM postgres:${postgres_version}
ADD setup.sql /docker-entrypoint-initdb.d
CMD ["postgres"]
@@ -1,6 +1,8 @@
postgrestest:
build:
context: docker/blocks/postgres_tests
args:
- postgres_version=${postgres_version}
environment:
POSTGRES_USER: grafanatest
POSTGRES_PASSWORD: grafanatest
+63
View File
@@ -0,0 +1,63 @@
---
# Sensu backend configuration
##
# backend configuration
##
state-dir: "/var/lib/sensu/sensu-backend"
#cache-dir: "/var/cache/sensu/sensu-backend"
#config-file: "/etc/sensu/backend.yml"
#debug: false
#deregistration-handler: "example_handler"
#log-level: "warn" # available log levels: panic, fatal, error, warn, info, debug
##
# agent configuration
##
#agent-host: "[::]" # listen on all IPv4 and IPv6 addresses
#agent-port: 8081
##
# api configuration
##
#api-listen-address: "[::]:8080" # listen on all IPv4 and IPv6 addresses
#api-url: "http://localhost:8080"
##
# dashboard configuration
##
#dashboard-cert-file: "/path/to/ssl/cert.pem"
#dashboard-key-file: "/path/to/ssl/key.pem"
#dashboard-host: "[::]" # listen on all IPv4 and IPv6 addresses
#dashboard-port: 3000
##
# ssl configuration
##
#cert-file: "/path/to/ssl/cert.pem"
#key-file: "/path/to/ssl/key.pem"
#trusted-ca-file: "/path/to/trusted-certificate-authorities.pem"
#insecure-skip-tls-verify: false
##
# store configuration
##
#etcd-advertise-client-urls: "http://localhost:2379"
#etcd-cert-file: "/path/to/ssl/cert.pem"
#etcd-client-cert-auth: false
#etcd-initial-advertise-peer-urls: "http://127.0.0.1:2380"
#etcd-initial-cluster: "default=http://127.0.0.1:2380"
#etcd-initial-cluster-state: "new" # new or existing
#etcd-initial-cluster-token: "sensu"
#etcd-key-file: "/path/to/ssl/key.pem"
#etcd-listen-client-urls: "http://127.0.0.1:2379"
#etcd-listen-peer-urls: "http://127.0.0.1:2380"
#etcd-name: "default"
#etcd-peer-cert-file: "/path/to/ssl/cert.pem"
#etcd-peer-client-cert-auth: false
#etcd-peer-key-file: "/path/to/ssl/key.pem"
#etcd-peer-trusted-ca-file: "/path/to/ssl/key.pem"
#etcd-trusted-ca-file: "/path/to/ssl/key.pem"
#no-embed-etcd: false
#etcd-cipher-suits
# - TLS_EXAMPLE
@@ -0,0 +1,18 @@
sensu-backend:
image: sensu/sensu:latest
container_name: sensu-backend
ports:
- "3080:3000"
- "8080:8080"
- "8081:8081"
volumes:
- ./docker/blocks/sensugo/backend.yml:/etc/sensu/backend.yml
- sensu-backend-data:/var/lib/sensu/etcd
environment:
SENSU_BACKEND_CLUSTER_ADMIN_USERNAME: admin
SENSU_BACKEND_CLUSTER_ADMIN_PASSWORD: Password123
command: "sensu-backend start --log-level info"
volumes:
sensu-backend-data: {}
+39
View File
@@ -0,0 +1,39 @@
# Notes on Sensu Go Docker Block
The API Key needed to connect to Sensu Go has to be created manually.
## Create the API Key
`docker exec -it sensu-backend /bin/ash`
Configure the `sensuctl` command using the pre-set username and password:
```bash
sensuctl configure -n --url http://127.0.0.1:8080 --username admin --password 'Password123' --namespace default
```
Generate the API Key:
```bash
sensuctl api-key grant admin
```
The output should look similar to this:
```
Created: /api/core/v2/apikeys/0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d
```
## Configuring the notification channel
### Backend URL
The Backend URL is the API port (8080) forwarded to the container, it should be
`http://localhost:8080`
### API Key
The `0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d` in the output above is the API Key
to use in configuring the Sensu Go notification channel.
+21 -9
View File
@@ -147,19 +147,20 @@ Since not all datasources have the same configuration settings we only have the
| tlsAuthWithCACert | boolean | _All_ | Enable TLS authentication using CA cert |
| tlsSkipVerify | boolean | _All_ | Controls whether a client verifies the server's certificate chain and host name. |
| graphiteVersion | string | Graphite | Graphite version |
| timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL and MSSQL | Lowest interval/step value that should be used for this data source |
| httpMode | string | Influxdb | HTTP Method. 'GET', 'POST', defaults to GET |
| httpMethod | string | Prometheus | HTTP Method. 'GET', 'POST', defaults to GET |
| timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL and MSSQL | Lowest interval/step value that should be used for this data source. |
| httpMode | string | Influxdb | HTTP Method. 'GET', 'POST', defaults to GET |
| httpMethod | string | Prometheus | HTTP Method. 'GET', 'POST', defaults to GET |
| esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56/60/70) |
| timeField | string | Elasticsearch | Which field that should be used as timestamp |
| interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' |
| logMessageField | string | Elasticsearch | Which field should be used as the log message |
| logLevelField | string | Elasticsearch | Which field should be used to indicate the priority of the log message |
| sigV4AuthType | string | Elasticsearch | SigV4 auth provider. default/credentials/keys |
| sigV4ExternalId | string | Elasticsearch | Optional SigV4 External ID |
| sigV4AssumeRoleArn | string | Elasticsearch | Optional SigV4 ARN role to assume |
| sigV4Region | string | Elasticsearch | SigV4 AWS region |
| sigV4Profile | string | Elasticsearch | Optional SigV4 credentials profile |
| sigV4Auth | boolean | Elasticsearch | Enable usage of SigV4 |
| sigV4AuthType | string | Elasticsearch | SigV4 auth provider. default/credentials/keys |
| sigV4ExternalId | string | Elasticsearch | Optional SigV4 External ID |
| sigV4AssumeRoleArn | string | Elasticsearch | Optional SigV4 ARN role to assume |
| sigV4Region | string | Elasticsearch | SigV4 AWS region |
| sigV4Profile | string | Elasticsearch | Optional SigV4 credentials profile |
| authType | string | Cloudwatch | Auth provider. default/credentials/keys |
| externalId | string | Cloudwatch | Optional External ID |
| assumeRoleArn | string | Cloudwatch | Optional ARN role to assume |
@@ -194,7 +195,7 @@ Secure json data is a map of settings that will be encrypted with [secret key]({
| basicAuthPassword | string | _All_ | password for basic authentication |
| accessKey | string | Cloudwatch | Access key for connecting to Cloudwatch |
| secretKey | string | Cloudwatch | Secret key for connecting to Cloudwatch |
| sigV4AccessKey | string | Elasticsearch | SigV4 access key. Required when using keys auth provider |
| sigV4AccessKey | string | Elasticsearch | SigV4 access key. Required when using keys auth provider |
| sigV4SecretKey | string | Elasticsearch | SigV4 secret key. Required when using keys auth provider |
#### Custom HTTP headers for datasources
@@ -479,6 +480,17 @@ The following sections detail the supported settings and secure settings for eac
| username | |
| password | yes |
#### Alert notification `sensugo`
| Name | Secure setting |
| -------- | -------------- |
| url | |
| apikey | yes |
| entity | |
| check | |
| handler | |
| namespace | |
#### Alert notification `prometheus-alertmanager`
| Name | Secure setting |
+1 -1
View File
@@ -9,7 +9,7 @@ weight = 200
Grafana alerting allows you to attach rules to your dashboard panels. When you save the dashboard, Grafana extracts the alert rules into a separate alert rule storage and schedules them for evaluation.
{{< imgbox max-width="1000px" img="/img/docs/alerting/drag_handles_gif.gif" caption="Alerting overview" >}}
![Alerting overview](/img/docs/alerting/drag_handles_gif.gif)
In the Alert tab of the graph panel you can configure how often the alert rule should be evaluated and the conditions that need to be met for the alert to change state and trigger its [notifications]({{< relref "notifications.md" >}}).
+5
View File
@@ -64,6 +64,7 @@ OpsGenie | `opsgenie` | yes, external only | yes
Prometheus Alertmanager | `prometheus-alertmanager` | yes, external only | yes
Pushover | `pushover` | yes | no
Sensu | `sensu` | yes, external only | no
[Sensu Go](#sensu-go) | `sensugo` | yes, external only | no
[Slack](#slack) | `slack` | yes | no
Telegram | `telegram` | yes | no
Threema | `threema` | yes, external only | no
@@ -212,6 +213,10 @@ Alertmanager handles alerts sent by client applications such as Prometheus serve
[Zenduty](https://www.zenduty.com) is an incident alerting and response orchestration platform that not alerts the right teams via SMS, Phone(Voice), Email, Slack, Microsoft Teams and Push notifications(Android/iOS) whenever a Grafana alert is triggered, but also helps you rapidly triage and remediate critical, user impacting incidents. Grafana alert are sent to Zenduty through Grafana's native webhook dispatcher. Refer the Zenduty-Grafana [integration documentation](https://docs.zenduty.com/docs/grafana) for configuring the integration.
### Sensu Go
[Sensu](https://sensu.io) is a complete solution for monitoring and observability at scale. Sensu Go is designed to give you visibility into everything you care about: traditional server closets, containers, applications, the cloud, and more. Grafana notifications can be sent to Sensu Go as events via the API. This operation requires an API Key. Refer to the [Sensu Go documentation](https://docs.sensu.io/sensu-go/latest/operations/control-access/use-apikeys/#api-key-authentication) for information on creating this key.
## Enable images in notifications {#external-image-store}
Grafana can render the panel associated with the alert rule as a PNG image and include that in the notification. Read more about the requirements and how to configure
+1 -1
View File
@@ -9,7 +9,7 @@ weight = 500
If alerts are not behaving as you expect, here are some steps you can take to troubleshoot and figure out what is going wrong.
{{< imgbox max-width="1000px" img="/img/docs/v4/alert_test_rule.png" caption="Test Rule" >}}
![Test Rule](/img/docs/v4/alert_test_rule.png)
The first level of troubleshooting you can do is click **Test Rule**. You will get result back that you can expand to the point where you can see the raw data that was returned from your query.
+1 -1
View File
@@ -118,7 +118,7 @@ allowed_groups = 8bab1c86-8fba-33e5-2089-1d1c80ec267d
You'll need to ensure that you've [enabled group attributes](https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-fed-group-claims#configure-the-azure-ad-application-registration-for-group-attributes) in your Azure AD Application Registration manifest file (Azure Portal -> Azure Active Directory -> Application Registrations -> Select Application -> Manifest)
```json
"groupMembershipClaims": "ApplicationGroup"
"groupMembershipClaims": "ApplicationGroup, SecurityGroup"
```
The `allowed_domains` option limits access to the users belonging to the specific domains. Domains should be separated by space or comma.
+21 -2
View File
@@ -286,12 +286,12 @@ datasources:
httpMode: GET
```
### InfluxDB 2.x example
### InfluxDB 2.x for Flux example
```yaml
apiVersion: 1
datasources:
- name: InfluxDB_v2
- name: InfluxDB_v2_Flux
type: influxdb
access: proxy
url: http://localhost:8086
@@ -303,3 +303,22 @@ datasources:
defaultBucket: bucket
tlsSkipVerify: true
```
### InfluxDB 2.x for InfluxQl example
```yaml
apiVersion: 1
datasources:
- name: InfluxDB_v2_InfluxQL
type: influxdb
access: proxy
url: http://localhost:8086
# This database should be mapped to a bucket
database: site
jsonData:
httpMode: GET
httpHeaderName1: 'Authorization'
secureJsonData:
httpHeaderValue1: 'Token <token>'
```
+2 -2
View File
@@ -1,9 +1,9 @@
+++
title = "Developers"
aliases = ["/docs/plugins/developing/"]
aliases = ["/docs/grafana/latest/plugins/developing/"]
weight = 190
+++
# Developers
This section of the documentation contains pages with resources for Grafana developers.
This section of the documentation contains pages with resources for Grafana developers.
+2 -1
View File
@@ -1,5 +1,6 @@
+++
title = "Build a plugin."
title = "Build a plugin"
aliases = ["/docs/grafana/latest/plugins/developing/"]
+++
# Build a plugin
+50 -32
View File
@@ -15,59 +15,69 @@ The plugin.json file is required for all plugins. When Grafana starts, it scans
| Property | Type | Required | Description |
|-----------------|-------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `dependencies` | [object](#dependencies) | **Yes** | Plugin dependencies. |
| `dependencies` | [object](#dependencies) | **Yes** | Dependencies needed by the plugin. |
| `id` | string | **Yes** | Unique name of the plugin. If the plugin is published on grafana.com, then the plugin id has to follow the naming conventions. |
| `info` | [object](#info) | **Yes** | Metadata for the plugin. Some fields are used on the plugins page in Grafana and others on grafana.com if the plugin is published. |
| `name` | string | **Yes** | Human-readable name of the plugin that is shown to the user in the UI. |
| `type` | string | **Yes** | Plugin type. Possible values are `app`, `datasource`, and `panel`. |
| `type` | string | **Yes** | Plugin type. Possible values are: `app`, `datasource`, `panel`. |
| `$schema` | string | No | Schema definition for the plugin.json file |
| `alerting` | boolean | No | For data source plugins. If the plugin supports alerting. |
| `annotations` | boolean | No | For data source plugins. If the plugin supports annotation queries. |
| `backend` | boolean | No | If the plugin has a backend component. |
| `category` | string | No | Plugin category used on the Add data source page. Possible values are: `tsdb`, `logging`, `cloud`, `tracing`, `sql`, `enterprise` and `other`. |
| `category` | string | No | Plugin category used on the Add data source page. Possible values are: `tsdb`, `logging`, `cloud`, `tracing`, `sql`, `enterprise`, `other`. |
| `executable` | string | No | The first part of the file name of the backend component executable. There can be multiple executables built for different operating system and architecture. Grafana will check for executables named `<executable>_<$GOOS>_<lower case $GOARCH><.exe for Windows>`, e.g. `plugin_linux_amd64`. Combination of $GOOS and $GOARCH can be found here: https://golang.org/doc/install/source#environment. |
| `hiddenQueries` | boolean | No | |
| `includes` | [object](#includes)[] | No | |
| `includes` | [object](#includes)[] | No | Resources to include in plugin. |
| `logs` | boolean | No | For data source plugins. If the plugin supports logs. |
| `metrics` | boolean | No | For data source plugins. If the plugin supports metric queries. Used in the Explore feature. |
| `mixed` | boolean | No | Not to be used by external plugins. Special property for the built-in mixed plugin. |
| `module` | string | No | |
| `partials` | [object](#partials) | No | |
| `preload` | boolean | No | Initialize plugin on startup. By default, the plugin initializes on first use. |
| `queryOptions` | [object](#queryoptions) | No | For data source plugins. There is a query options section in the plugin's query editor and these options can be turned on if needed. |
| `routes` | [object](#routes)[] | No | For data source plugins. Proxy routes used for plugin authentication and adding headers to HTTP requests made by the plugin. For more information, refer to [Authentication for data source plugins](https://grafana.com/docs/grafana/latest/developers/plugins/authentication/). |
| `skipDataQuery` | boolean | No | For panel plugins. Hides the query editor. |
| `sort` | number | No | Internal property for sorting. Cannot be used as will be overwritten by Grafana. |
| `state` | string | No | Marks a plugin as a pre-release. |
| `staticRoot` | string | No | |
| `state` | string | No | Marks a plugin as a pre-release. Possible values are: `alpha`, `beta`. |
| `streaming` | boolean | No | For data source plugins. If the plugin supports streaming. |
| `tables` | boolean | No | |
| `tracing` | boolean | No | For data source plugins. If the plugin supports tracing. |
## dependencies
Plugin dependencies.
Dependencies needed by the plugin.
### Properties
| Property | Type | Required | Description |
|---------------------|----------|----------|-------------------------------------------------------------------------------------------------------------------------------|
| `grafanaDependency` | string | **Yes** | Required Grafana version for this plugin. Validated using https://github.com/npm/node-semver. |
| `grafanaVersion` | string | No | (Deprecated) Required Grafana version for this plugin, e.g. `6.x.x 7.x.x` to denote plugin requires Grafana v6.x.x or v7.x.x. |
| `plugins` | string[] | No | An array of required plugins on which this plugin depends. |
| Property | Type | Required | Description |
|---------------------|----------------------|----------|-------------------------------------------------------------------------------------------------------------------------------|
| `grafanaDependency` | string | **Yes** | Required Grafana version for this plugin. Validated using https://github.com/npm/node-semver. |
| `grafanaVersion` | string | No | (Deprecated) Required Grafana version for this plugin, e.g. `6.x.x 7.x.x` to denote plugin requires Grafana v6.x.x or v7.x.x. |
| `plugins` | [object](#plugins)[] | No | An array of required plugins on which this plugin depends. |
### plugins
Plugin dependency. Used to display information about plugin dependencies in the Grafana UI.
#### Properties
| Property | Type | Required | Description |
|-----------|--------|----------|----------------------------------------------------|
| `id` | string | **Yes** | |
| `name` | string | **Yes** | |
| `type` | string | **Yes** | Possible values are: `app`, `datasource`, `panel`. |
| `version` | string | **Yes** | |
## includes
### Properties
| Property | Type | Required | Description |
|--------------|---------|----------|-------------|
| `addToNav` | boolean | No | |
| `component` | string | No | |
| `defaultNav` | boolean | No | |
| `name` | string | No | |
| `role` | string | No | |
| `type` | string | No | |
| Property | Type | Required | Description |
|--------------|---------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `addToNav` | boolean | No | Add the include to the side menu. |
| `component` | string | No | (Legacy) The Angular component to use for a page. |
| `defaultNav` | boolean | No | Page or dashboard when user clicks the icon in the side menu. |
| `icon` | string | No | Icon to use in the side menu. For information on available icon, refer to [Icons Overview](https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview). |
| `name` | string | No | |
| `path` | string | No | Used for app plugins. |
| `role` | string | No | Possible values are: `Admin`, `Editor`, `Viewer`. |
| `type` | string | No | Possible values are: `dashboard`, `page`, `panel`, `datasource`. |
## info
@@ -82,6 +92,7 @@ Metadata for the plugin. Some fields are used on the plugins page in Grafana and
| `updated` | string | **Yes** | Date when this plugin was built. |
| `version` | string | **Yes** | Project version of this commit, e.g. `6.7.x`. |
| `author` | [object](#author) | No | Information about the plugin author. |
| `build` | [object](#build) | No | Build information |
| `description` | string | No | Description of plugin. Used on the plugins page in Grafana and for search on grafana.com. |
| `links` | [object](#links)[] | No | An array of link objects to be displayed on this plugin's project page in the form `{name: 'foo', url: 'http://example.com'}` |
| `screenshots` | [object](#screenshots)[] | No | An array of screenshot objects in the form `{name: 'bar', path: 'img/screenshot.png'}` |
@@ -98,6 +109,21 @@ Information about the plugin author.
| `name` | string | No | Author's name. |
| `url` | string | No | Link to author's website. |
### build
Build information
#### Properties
| Property | Type | Required | Description |
|----------|--------|----------|------------------------------------------------------|
| `branch` | string | No | Git branch the plugin was built from. |
| `hash` | string | No | Git hash of the commit the plugin was built from |
| `number` | number | No | |
| `pr` | number | No | GitHub pull request the plugin was built from |
| `repo` | string | No | |
| `time` | number | No | Time when the plugin was built, as a Unix timestamp. |
### links
#### Properties
@@ -127,14 +153,6 @@ SVG images that are used as plugin icons.
| `name` | string | No | |
| `path` | string | No | |
## partials
### Properties
| Property | Type | Required | Description |
|----------|--------|----------|-------------|
| `config` | string | No | |
## queryOptions
For data source plugins. There is a query options section in the plugin's query editor and these options can be turned on if needed.
@@ -18,7 +18,7 @@
},
"type": {
"type": "string",
"description": "Plugin type. Possible values are `app`, `datasource`, and `panel`.",
"description": "Plugin type.",
"enum": ["app", "datasource", "panel"]
},
"name": {
@@ -27,7 +27,7 @@
},
"category": {
"type": "string",
"description": "Plugin category used on the Add data source page. Possible values are: `tsdb`, `logging`, `cloud`, `tracing`, `sql`, `enterprise` and `other`.",
"description": "Plugin category used on the Add data source page.",
"enum": ["tsdb", "logging", "cloud", "tracing", "sql", "enterprise", "other"]
},
"annotations": {
@@ -57,27 +57,41 @@
},
"includes": {
"type": "array",
"description": "Resources to include in plugin.",
"items": {
"type": "object",
"additionalItems": false,
"properties": {
"type": {
"type": "string"
"type": "string",
"enum": ["dashboard", "page", "panel", "datasource"]
},
"name": {
"type": "string"
},
"component": {
"type": "string"
"type": "string",
"description": "(Legacy) The Angular component to use for a page."
},
"role": {
"type": "string"
"type": "string",
"enum": ["Admin", "Editor", "Viewer"]
},
"path": {
"type": "string",
"description": "Used for app plugins."
},
"addToNav": {
"type": "boolean"
"type": "boolean",
"description": "Add the include to the side menu."
},
"defaultNav": {
"type": "boolean"
"type": "boolean",
"description": "Page or dashboard when user clicks the icon in the side menu."
},
"icon": {
"type": "string",
"description": "Icon to use in the side menu. For information on available icon, refer to [Icons Overview](https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview)."
}
}
}
@@ -90,34 +104,10 @@
"type": "boolean",
"description": "For panel plugins. Hides the query editor."
},
"partials": {
"type": "object",
"additionalItems": false,
"properties": {
"config": {
"type": "string"
}
}
},
"metrics": {
"type": "boolean",
"description": "For data source plugins. If the plugin supports metric queries. Used in the Explore feature."
},
"mixed": {
"type": "boolean",
"description": "Not to be used by external plugins. Special property for the built-in mixed plugin."
},
"module": {
"type": "string",
"description": ""
},
"sort": {
"type": "number",
"description": "Internal property for sorting. Cannot be used as will be overwritten by Grafana."
},
"staticRoot": {
"type": "string"
},
"streaming": {
"type": "boolean",
"description": "For data source plugins. If the plugin supports streaming."
@@ -134,7 +124,7 @@
},
"dependencies": {
"type": "object",
"description": "Plugin dependencies.",
"description": "Dependencies needed by the plugin.",
"required": ["grafanaDependency"],
"additionalProperties": false,
"properties": {
@@ -151,8 +141,27 @@
"plugins": {
"type": "array",
"description": "An array of required plugins on which this plugin depends.",
"additionalItems": false,
"items": {
"type": "string"
"type": "object",
"description": "Plugin dependency. Used to display information about plugin dependencies in the Grafana UI.",
"required": ["id", "name", "type", "version"],
"properties": {
"id": {
"type": "string",
"pattern": "^[0-9a-z]+\\-([0-9a-z]+\\-)?(app|panel|datasource)$"
},
"type": {
"type": "string",
"enum": ["app", "datasource", "panel"]
},
"name": {
"type": "string"
},
"version": {
"type": "string"
}
}
}
}
}
@@ -184,6 +193,37 @@
}
}
},
"build": {
"type": "object",
"description": "Build information",
"additionalProperties": false,
"properties": {
"time": {
"type": "number",
"description": "Time when the plugin was built, as a Unix timestamp."
},
"repo": {
"type": "string",
"description": ""
},
"branch": {
"type": "string",
"description": "Git branch the plugin was built from."
},
"hash": {
"type": "string",
"description": "Git hash of the commit the plugin was built from"
},
"number": {
"type": "number",
"description": ""
},
"pr": {
"type": "number",
"description": "GitHub pull request the plugin was built from"
}
}
},
"description": {
"type": "string",
"description": "Description of plugin. Used on the plugins page in Grafana and for search on grafana.com."
@@ -1,5 +1,5 @@
+++
title = "With Grafana and MS SQL"
title = "With Grafana and MS SQL Server"
description = "Guide for getting started with Grafana"
keywords = ["grafana", "intro", "guide", "started", "SQL", "MSSQL"]
aliases = ["/docs/grafana/latest/guides/gettingstarted","/docs/grafana/latest/guides/getting_started"]
@@ -15,7 +15,7 @@ Use the instructions in [Getting started with Grafana]({{< relref "getting-start
- Install Grafana.
- Log in to Grafana.
- Create your first dashboard.
> **Note:** You must install Grafana 5.1+ in order to use the integrated MSSQL data source.
> **Note:** You must install Grafana 5.1+ in order to use the integrated MS SQL data source.
## Step 2. Download MS SQL Server
@@ -63,7 +63,7 @@ To add MS SQL Server data source:
`User` | Database user's login/username.
`Password` | Database user's password.
For installations from the [grafana/grafana](https://github.com/grafana/grafana/tree/master) repository, `gdev-mssql` data source is available. Once you add this data source, the `Datasource tests - MSSQL` dashboard with three panels showing metrics generated from test database is available.
For installations from the [grafana/grafana](https://github.com/grafana/grafana/tree/master) repository, `gdev-mssql` data source is available. Once you add this data source, you can use the `Datasource tests - MSSQL` dashboard with three panels showing metrics generated from a test database.
<img src="/img/docs/getting-started/gdev-sql-dashboard.png" class="no-shadow" width="700px">
@@ -30,7 +30,7 @@ To log in to Grafana for the first time:
To create your first dashboard:
1. Click the **+** icon on the left panel, select **Create Dashboard**, and then click **Add new panel**.
1. In the New Dashbard/Edit Panel view, go to the **Query** tab.
1. In the New Dashboard/Edit Panel view, go to the **Query** tab.
1. Configure your [query]({{< relref "../panels/queries.md" >}}) by selecting ``-- Grafana --`` from the [data source selector]({{< relref "../panels/queries.md/#data-source-selector" >}}). This generates the Random Walk dashboard.
1. Click the **Save** icon in the top right corner of your screen to save the dashboard.
1. Add a descriptive name, and then click **Save**.
@@ -56,4 +56,4 @@ The following topics are of interest to Grafana server admin users:
- [Provisioning]({{< relref "../administration/provisioning.md" >}})
- [Grafana CLI]({{< relref "../administration/cli.md" >}})
<!-- BEGIN Optimal Workshop Intercept Snippet --><div id='owInviteSnippet' style='position:fixed;right:20px;bottom:20px;width:280px;padding:20px;margin:0;border-radius:6px;background:#1857B8;color:#F7F8FA;text-align:left;z-index:2200000000;opacity:0;transition:opacity 500ms;-webkit-transition:opacity 500ms;display:none;'><div id='owInviteMessage' style='padding:0;margin:0 0 20px 0;font-size:16px;'>Got a spare two and a half minutes to help us improve the docs?</div><a id='owInviteOk' href='https://Grafana.optimalworkshop.com/questions/grafana-docs?tag=docs&utm_medium=intercept' onclick='this.parentNode.style.display="none";' target='_blank' style='color:#F7FAFF;font-size:16px;font-weight:bold;text-decoration:underline;'>Yes, I&#x27;ll help</a><a id='owInviteCancel' href='javascript:void(0)' onclick='this.parentNode.style.display="none";' style='color:#F7F8FA;font-size:14px;text-decoration:underline;float:right;'>Close</a></div><script>var owOnload=function(){if(-1==document.cookie.indexOf('ow-intercept-quiz-4ior230e')){var o=new XMLHttpRequest;o.onloadend=function(){try{var o=document.getElementById('owInviteSnippet');var date=new Date();date.setMonth(date.getMonth()+1);this.response&&JSON.parse(this.response).active===!0&&(document.cookie='ow-intercept-quiz-4ior230e=Done;path=/;expires='+date.toUTCString()+';',setTimeout(function(){o.style.display='block',o.style.opacity=1},2e3))}catch(e){}},o.open('POST','https://app.optimalworkshop.com/survey_status/questions/4ior230e/active'),o.send()}};if(window.addEventListener){window.addEventListener('load',function(){owOnload();});}else if(window.attachEvent){window.attachEvent('onload',function(){owOnload();});}</script><!-- END Optimal Workshop snippet -->
<!-- BEGIN Optimal Workshop Intercept Snippet --><div id='owInviteSnippet' style='position:fixed;right:20px;bottom:20px;width:280px;padding:20px;margin:0;border-radius:6px;background:#1857B8;color:#F7F8FA;text-align:left;z-index:2200000000;opacity:0;transition:opacity 500ms;-webkit-transition:opacity 500ms;display:none;'><div id='owInviteMessage' style='padding:0;margin:0 0 20px 0;font-size:16px;'>Got a spare two and a half minutes to help us improve the docs?</div><a id='owInviteOk' href='https://Grafana.optimalworkshop.com/questions/grafana-docs?tag=docs&utm_medium=intercept' onclick='this.parentNode.style.display="none";' target='_blank' style='color:#F7FAFF;font-size:16px;font-weight:bold;text-decoration:underline;'>Yes, I&#x27;ll help</a><a id='owInviteCancel' href='javascript:void(0)' onclick='this.parentNode.style.display="none";' style='color:#F7F8FA;font-size:14px;text-decoration:underline;float:right;'>Close</a></div><script>var owOnload=function(){if(-1==document.cookie.indexOf('ow-intercept-quiz-4ior230e')){var o=new XMLHttpRequest;o.onloadend=function(){try{var o=document.getElementById('owInviteSnippet');var date=new Date();date.setMonth(date.getMonth()+1);this.response&&JSON.parse(this.response).active===!0&&(document.cookie='ow-intercept-quiz-4ior230e=Done;path=/;expires='+date.toUTCString()+';',setTimeout(function(){o.style.display='block',o.style.opacity=1},2e3))}catch(e){}},o.open('POST','https://app.optimalworkshop.com/survey_status/questions/4ior230e/active'),o.send()}};if(window.addEventListener){window.addEventListener('load',function(){owOnload();});}else if(window.attachEvent){window.attachEvent('onload',function(){owOnload();});}</script><!-- END Optimal Workshop snippet -->
+5 -4
View File
@@ -68,7 +68,8 @@ JSON Body schema:
"deleteKey":"XXXXXXX",
"deleteUrl":"myurl/api/snapshots-delete/XXXXXXX",
"key":"YYYYYYY",
"url":"myurl/dashboard/snapshot/YYYYYYY"
"url":"myurl/dashboard/snapshot/YYYYYYY",
"id": 1,
}
```
@@ -192,7 +193,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
HTTP/1.1 200
Content-Type: application/json
{"message":"Snapshot deleted. It might take an hour before it's cleared from any CDN caches."}
{"message":"Snapshot deleted. It might take an hour before it's cleared from any CDN caches.", "id": 1}
```
## Delete Snapshot by deleteKey
@@ -214,5 +215,5 @@ Accept: application/json
HTTP/1.1 200
Content-Type: application/json
{"message":"Snapshot deleted. It might take an hour before it's cleared from any CDN caches."}
```
{"message":"Snapshot deleted. It might take an hour before it's cleared from any CDN caches.", "id": 1}
```
@@ -7,34 +7,72 @@ weight = 300
Grafana comes with the following transformations:
- [Reduce](#reduce)
- [Filter by name](#filter-by-name)
- [Filter data by query](#filter-data-by-query)
- [Organize fields](#organize-fields)
- [Outer join](#join-by-field-outer-join)
- [Series to rows](#series-to-rows)
- [Add field from calculation](#add-field-from-calculation)
- [Labels to fields](#labels-to-fields)
- [Group by](#group-by)
- [Merge](#merge)
- [Reduce](#reduce)
- [Filter by name](#filter-by-name)
- [Filter data by query](#filter-data-by-query)
- [Filter data by value](#filter-data-by-value)
- [Organize fields](#organize-fields)
- [Outer join](#join-by-field-outer-join)
- [Series to rows](#series-to-rows)
- [Add field from calculation](#add-field-from-calculation)
- [Labels to fields](#labels-to-fields)
- [Concatenate fields](#concatenate-fields)
- [Group by](#group-by)
- [Merge](#merge)
Keep reading for detailed descriptions of each type of transformation and the options available for each, as well as suggestions on how to use them.
## Reduce
Apply a _Reduce_ transformation when you want to simplify your results down to one value. Reduce removes the time component. If visualized as a table, it reduces a column down to one row (value).
The _Reduce_ transformation will apply a calculation to each field in the frame and return a single value. Time fields are removed when applying
this transformation.
In the **Calculations** field, enter one or more calculation types. Click to see a list of calculation choices. For information about available calculations, refer to the [Calculation list]({{< relref "../calculations-list.md" >}}).
Consider the input:
Once you select at least one calculation, Grafana reduces the results down to one value using the calculation you select. If you select more than one calculation, then more than one value is displayed.
Query A:
Here's an example of a table with time series data. Before I apply the transformation, you can see all the data organized by time.
| Time | Temp | Uptime |
| ------------------- | ------- | ------- |
| 2020-07-07 11:34:20 | 12.3 | 256122 |
| 2020-07-07 11:24:20 | 15.4 | 1230233 |
{{< docs-imagebox img="/img/docs/transformations/reduce-before-7-0.png" class="docs-image--no-shadow" max-width= "1100px" >}}
Query B:
| Time | AQI | Errors |
| ------------------- | ------- | ------ |
| 2020-07-07 11:34:20 | 6.5 | 15 |
| 2020-07-07 11:24:20 | 3.2 | 5 |
The reduce transformer has two modes:
- **Series to rows -** Creates a row for each field and a column for each calculation.
- **Reduce fields -** Keeps the existing frame structure, but collapses each field into a single value.
For example, if you used the **First** and **Last** calculation with a **Series to rows** transformation, then
the result would be:
| Field | First | Last |
| ------- | ------- | ------- |
| Temp | 12.3 | 15.4 |
| Uptime | 256122 | 1230233 |
| AQI | 6.5 | 3.2 |
| Errors | 15 | 5 |
The **Reduce fields** with the **Last** calculation,
results in two frames, each with one row:
Query A:
| Temp | Uptime |
| ------- | ------- |
| 15.4 | 1230233 |
Query B:
| AQI | Errors |
| ------- | ------ |
| 3.2 | 5 |
After I apply the transformation, there is no time value and each column has been reduced to one row showing the results of the calculations that I chose.
{{< docs-imagebox img="/img/docs/transformations/reduce-after-7-0.png" class="docs-image--no-shadow" max-width= "1100px" >}}
## Merge
@@ -138,7 +176,8 @@ Use this transformation to add a new field calculated from two other fields. Eac
- **Reduce row -** Apply selected calculation on each row of selected fields independently.
- **Binary option -** Apply basic math operation(sum, multiply, etc) on values in a single row from two selected fields.
- **Field name -** Select the names of fields you want to use in the calculation for the new field.
- **Calculation -** Select a calculation to use when Grafana creates the new field. Click in the field to see a list of calculation choices. For information about available calculations, refer to the [Calculation list]({{< relref "../calculations-list.md" >}}).
- **Calculation -** If you select **Reduce row** mode, then the **Calculation** field appears. Click in the field to see a list of calculation choices you can use to create the new field. For information about available calculations, refer to the [Calculation list]({{< relref "../calculations-list.md" >}}).
- **Operation -** If you select **Binary option** mode, then the **Operation** fields appear. These fields allow you to do basic math operations on values in a single row from two selected fields. You can also use numerical values for binary operations.
- **Alias -** (Optional) Enter the name of your new field. If you leave this blank, then the field will be named to match the calculation.
- **Replace all fields -** (Optional) Select this option if you want to hide all other fields and display only your calculated field in the visualization.
@@ -184,7 +223,7 @@ After I apply the transformation, my labels appear in the table as fields.
> **Note:** This transformation is only available in Grafana 7.2+.
This transformation groups the data by a specified field (column) value and processes calculations on each group. Click to see a list of calculation choices. For information about available calculations, refer to the [List of calculations]({{< relref "../calculations-list.md" >}}).
This transformation groups the data by a specified field (column) value and processes calculations on each group. Click to see a list of calculation choices. For information about available calculations, refer to the [List of calculations]({{< relref "../calculations-list.md" >}}).
Here's an example of original data.
@@ -202,17 +241,17 @@ Here's an example of original data.
This transformation goes in two steps. First you specify one or multiple fields to group the data by. This will group all the same values of those fields together, as if you sorted them. For instance if we group by the Server ID field, then it would group the data this way:
| Time | Server ID | CPU Temperature | Server Status |
| ------------------- | ------------ | --------------- | ------------- |
| 2020-07-07 11:34:20 | **server 1** | 80 | Shutdown |
| 2020-07-07 09:28:06 | **server 1** | 80 | OK |
| 2020-07-07 09:23:07 | **server 1** | 86 | OK |
| 2020-07-07 10:32:20 | server 2 | 90 | Overload |
| 2020-07-07 09:30:05 | server 2 | 88 | OK |
| 2020-07-07 09:25:05 | server 2 | 88 | OK |
| 2020-07-07 11:34:20 | **_server 3_** | 62 | OK |
| 2020-07-07 10:31:22 | **_server 3_** | 55 | OK |
| 2020-07-07 09:30:57 | **_server 3_** | 62 | Rebooting |
| Time | Server ID | CPU Temperature | Server Status |
| ------------------- | -------------- | --------------- | ------------- |
| 2020-07-07 11:34:20 | **server 1** | 80 | Shutdown |
| 2020-07-07 09:28:06 | **server 1** | 80 | OK |
| 2020-07-07 09:23:07 | **server 1** | 86 | OK |
| 2020-07-07 10:32:20 | server 2 | 90 | Overload |
| 2020-07-07 09:30:05 | server 2 | 88 | OK |
| 2020-07-07 09:25:05 | server 2 | 88 | OK |
| 2020-07-07 11:34:20 | **_server 3_** | 62 | OK |
| 2020-07-07 10:31:22 | **_server 3_** | 55 | OK |
| 2020-07-07 09:30:57 | **_server 3_** | 62 | Rebooting |
All rows with the same value of Server ID are grouped together.
@@ -240,11 +279,36 @@ We would then get :
This transformation allows you to extract some key information out of your time series and display them in a convenient way.
## Concatenate fields
> **Note:** This transformation is only available in Grafana 7.3+.
This transformation combines all fields from all frames into one result. Consider:
Query A:
| Temp | Uptime |
| ------- | ------- |
| 15.4 | 1230233 |
Query B:
| AQI | Errors |
| ------- | ------ |
| 3.2 | 5 |
After you concatenate the fields, the data frame would be:
| Temp | Uptime | AQI | Errors |
| ------- | ------- | ------- | ------ |
| 15.4 | 1230233 | 3.2 | 5 |
## Series to rows
> **Note:** This transformation is only available in Grafana 7.1+.
Use this transformation to combine the result from multiple time series data queries into one single result. This is helpful when using the table panel visualization.
Use this transformation to combine the result from multiple time series data queries into one single result. This is helpful when using the table panel visualization.
The result from this transformation will contain three columns: Time, Metric, and Value. The Metric column is added so you easily can see from which query the metric originates from. Customize this value by defining Label on the source query.
@@ -276,3 +340,61 @@ Here is the result after applying the Series to rows transformation.
| 2020-07-07 10:31:22 | Temperature | 22 |
| 2020-07-07 09:30:57 | Humidity | 33 |
| 2020-07-07 09:30:05 | Temperature | 19 |
## Filter by value
This transformation allows you to filter your data directly in Grafana and remove some data points from your query result. You have the option to include or exclude data that match one or more conditions you define. The conditions are applied on a selected field.
The available conditions are:
- **Regex**: match a regex expression
- **Is Null**: match if the value is null
- **Is Not Null**: match if the value is not null
- **Equal**: match if the value is equal to the specified value
- **Different**: match if the value is different than the specified value
- **Greater**\*: match if the value is greater than the specified value
- **Lower**\*: match if the value is lower than the specified value
- **Greater or equal**\*: match if the value is greater or equal
- **Lower or equal**\*: match if the value is lower or equal
- **Range**\*: match a range between a specified minimum and maximum, min and max included
\* Those conditions are only available for number fields.
Consider the following data set:
| Time | Temperature | Altitude |
| ------------------- | ----------- | -------- |
| 2020-07-07 11:34:23 | 32 | 101 |
| 2020-07-07 11:34:22 | 28 | 125 |
| 2020-07-07 11:34:21 | 26 | 110 |
| 2020-07-07 11:34:20 | 23 | 98 |
| 2020-07-07 10:32:24 | 31 | 95 |
| 2020-07-07 10:31:22 | 20 | 85 |
| 2020-07-07 09:30:57 | 19 | 101 |
If you **Include** the data points that have a temperature below 30°C, the configuration will look as follows:
- Filter Type: `Include`
- Condition: Rows where `Temperature` matches `Lower Than` `100`
And you will get the following result, where only the temperatures below 30°C are included:
| Time | Temperature | Altitude |
| ------------------- | ----------- | -------- |
| 2020-07-07 11:34:22 | 28 | 125 |
| 2020-07-07 11:34:21 | 26 | 110 |
| 2020-07-07 11:34:20 | 23 | 98 |
| 2020-07-07 10:31:22 | 20 | 85 |
| 2020-07-07 09:30:57 | 19 | 101 |
You can add more than one condition to the filter. For example, you might want to include the data only if the altitude is greater than 100. To do so, add that condition to the following configuration:
- Filter type: `Include` rows that `Match All` conditions
- Condition 1: Rows where `Temperature` matches `Lower` than `30`
- Condition 2: Rows where `Altitude` matches `Greater` than `100`
When you have more than one condition, you can choose if you want the action (include / exclude) to be applied on rows that **Match all** conditions or **Match any** of the conditions you added.
In the example above we chose **Match all** because we wanted to include the rows that have a temperature lower than 30 _AND_ an altitude higher than 100. If we wanted to include the rows that have a temperature lower than 30 _OR_ an altitude higher than 100 instead, then we would select **Match any**. This would include the first row in the original data, which has a temperature of 32°C (does not match the first condition) but an altitude of 101 (which matches the second condition), so it is included.
Conditions that are invalid or incompletely configured are ignored.
+1 -1
View File
@@ -1,6 +1,6 @@
+++
title = "Install plugins"
aliases = ["/docs/plugins/installation/"]
aliases = ["/docs/grafana/latest/plugins/installation/"]
weight = 1
+++
+1 -1
View File
@@ -1,7 +1,7 @@
+++
title = "Plugin signatures"
type = "docs"
aliases = ["/docs/plugins/plugin-signature-verification"]
aliases = ["/docs/grafana/latest/plugins/plugin-signature-verification"]
+++
# Plugin signatures
@@ -13,5 +13,5 @@ list = false
* **Dashboard**: Fixes kiosk state after being redirected to login page and back. [#29273](https://github.com/grafana/grafana/pull/29273), [@torkelo](https://github.com/torkelo)
* **InfluxDB**: Update flux library to fix support for boolean label values. [#29310](https://github.com/grafana/grafana/pull/29310), [@ryantxu](https://github.com/ryantxu)
* **Security**: Fixes minor security issue with alert notification webhooks that allowed GET & DELETE requests. [#29330](https://github.com/grafana/grafana/pull/29330), [@wbrowne](https://github.com/wbrowne)
* **Table**: Fix incorrect condition for rendering table filter. [#29165](https://github.com/grafana/grafana/pull/29165), [@dprokop](https://github.com/dprokop)
* **Table**: Fixes issues with phantom extra 0 for zero values. [#29165](https://github.com/grafana/grafana/pull/29165), [@dprokop](https://github.com/dprokop)
@@ -78,6 +78,8 @@ demo.robustperception.io:9100
## Filter and modify using named text and value capture groups
> **Note:** This feature is available in Grafana 7.4+.
Using named capture groups, you can capture separate 'text' and 'value' parts from the options returned by the variable query. This allows the variable drop-down list to contain a friendly name for each value that can be selected.
For example, when querying the `node_hwmon_chip_names` Prometheus metric, the `chip_name` is a lot friendlier that the `chip` value. So the following variable query result:
@@ -1,9 +1,9 @@
+++
title = "Formatting multi-value variables"
title = "Multi-value variables"
weight = 600
+++
# Formatting multi-value variables
# Multi-value variables
Interpolating a variable with multiple values selected is tricky as it is not straight forward how to format the multiple values into a string that is valid in the given context where the variable is used. Grafana tries to solve this by allowing each data source plugin to inform the templating interpolation engine what format to use for multiple values.
@@ -21,7 +21,7 @@ InfluxDB and Prometheus use regex expressions, so the same variable would be int
Elasticsearch uses lucene query syntax, so the same variable would be formatted as `("host1" OR "host2" OR "host3")`. In this case, every value must be escaped so that the value only contains lucene control words and quotation marks.
## Formatting troubles
## Troubleshoot multi-value variables
Automatic escaping and formatting can cause problems and it can be tricky to grasp the logic behind it. Especially for InfluxDB and Prometheus where the use of regex syntax requires that the variable is used in regex operator context.
@@ -1,5 +1,5 @@
+++
title = "Variables"
title = "Repeat panels or rows"
keywords = ["grafana", "templating", "documentation", "guide", "template", "variable", "repeat"]
weight = 800
+++
+1 -1
View File
@@ -1,5 +1,5 @@
+++
title = "Variables syntax and types"
title = "Variable syntax"
keywords = ["grafana", "templating", "documentation", "guide", "template", "variable"]
aliases = ["/docs/grafana/latest/reference/templating"]
weight = 100
+5 -3
View File
@@ -26,7 +26,7 @@ e2e.scenario({
expect(li.text()).equals('Query1'); // there's already a query so therefore Query + 1
});
e2e.components.QueryTab.content().should('be.visible');
e2e.components.TransformTab.content().should('not.be.visible');
e2e.components.TransformTab.content().should('not.exist');
e2e.components.AlertTab.content().should('not.be.visible');
// Bottom pane tabs
@@ -37,7 +37,9 @@ e2e.scenario({
e2e.components.Tab.active().within((li: JQuery<HTMLLIElement>) => {
expect(li.text()).equals('Transform0'); // there's no transform so therefore Transform + 0
});
e2e.components.TransformTab.content().should('be.visible');
e2e.components.Transforms.card('Merge')
.scrollIntoView()
.should('be.visible');
e2e.components.QueryTab.content().should('not.be.visible');
e2e.components.AlertTab.content().should('not.be.visible');
@@ -50,7 +52,7 @@ e2e.scenario({
});
e2e.components.AlertTab.content().should('be.visible');
e2e.components.QueryTab.content().should('not.be.visible');
e2e.components.TransformTab.content().should('not.be.visible');
e2e.components.TransformTab.content().should('not.exist');
e2e.components.Tab.title('Query')
.should('be.visible')
@@ -46,10 +46,9 @@ describe('Variables - Add variable', () => {
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
.should('be.visible')
.within(select => {
e2e.components.Select.singleValue().should('have.text', '');
e2e.components.Select.singleValue().should('have.text', 'gdev-testdata');
});
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput().should('not.exist');
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelect()
.should('be.visible')
.within(select => {
@@ -0,0 +1,299 @@
import { e2e } from '@grafana/e2e';
const PAGE_UNDER_TEST = 'AejrN1AMz';
describe('TextBox - load options scenarios', function() {
it('default options should be correct', function() {
e2e.flows.login('admin', 'admin');
e2e.flows.openDashboard({ uid: PAGE_UNDER_TEST });
e2e().server();
e2e()
.route({
method: 'GET',
url: `/api/dashboards/uid/${PAGE_UNDER_TEST}`,
})
.as('dash');
e2e().wait('@dash');
validateTextboxAndMarkup('default value');
});
it('loading variable from url should be correct', function() {
e2e.flows.login('admin', 'admin');
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?var-text=not default value` });
e2e().server();
e2e()
.route({
method: 'GET',
url: `/api/dashboards/uid/${PAGE_UNDER_TEST}`,
})
.as('dash');
e2e().wait('@dash');
validateTextboxAndMarkup('not default value');
});
});
describe.skip('TextBox - change query scenarios', function() {
it('when changing the query value and not saving current as default should revert query value', function() {
copyExistingDashboard();
changeQueryInput();
e2e.components.BackButton.backArrow()
.should('be.visible')
.click({ force: true });
validateTextboxAndMarkup('changed value');
saveDashboard(false);
e2e()
.get('@dashuid')
.then((dashuid: any) => {
expect(dashuid).not.to.eq(PAGE_UNDER_TEST);
e2e.flows.openDashboard({ uid: dashuid });
e2e().wait('@load-dash');
validateTextboxAndMarkup('default value');
validateVariable('changed value');
});
});
it('when changing the query value and saving current as default should change query value', function() {
copyExistingDashboard();
changeQueryInput();
e2e.components.BackButton.backArrow()
.should('be.visible')
.click({ force: true });
validateTextboxAndMarkup('changed value');
saveDashboard(true);
e2e()
.get('@dashuid')
.then((dashuid: any) => {
expect(dashuid).not.to.eq(PAGE_UNDER_TEST);
e2e.flows.openDashboard({ uid: dashuid });
e2e().wait('@load-dash');
validateTextboxAndMarkup('changed value');
validateVariable('changed value');
});
});
});
describe.skip('TextBox - change picker value scenarios', function() {
it('when changing the input value and not saving current as default should revert query value', function() {
copyExistingDashboard();
changeTextBoxInput();
validateTextboxAndMarkup('changed value');
saveDashboard(false);
e2e()
.get('@dashuid')
.then((dashuid: any) => {
expect(dashuid).not.to.eq(PAGE_UNDER_TEST);
e2e.flows.openDashboard({ uid: dashuid });
e2e().wait('@load-dash');
validateTextboxAndMarkup('default value');
validateVariable('default value');
});
});
it('when changing the input value and saving current as default should change query value', function() {
copyExistingDashboard();
changeTextBoxInput();
validateTextboxAndMarkup('changed value');
saveDashboard(true);
e2e()
.get('@dashuid')
.then((dashuid: any) => {
expect(dashuid).not.to.eq(PAGE_UNDER_TEST);
e2e.flows.openDashboard({ uid: dashuid });
e2e().wait('@load-dash');
validateTextboxAndMarkup('changed value');
validateVariable('changed value');
});
});
});
function copyExistingDashboard() {
e2e.flows.login('admin', 'admin');
e2e().server();
e2e()
.route({
method: 'GET',
url: '/api/search?query=&type=dash-folder&permission=Edit',
})
.as('dash-settings');
e2e()
.route({
method: 'POST',
url: '/api/dashboards/db/',
})
.as('save-dash');
e2e()
.route({
method: 'GET',
url: /\/api\/dashboards\/uid\/(?!AejrN1AMz)\w+/,
})
.as('load-dash');
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?editview=settings&orgId=1` });
e2e().wait('@dash-settings');
e2e.pages.Dashboard.Settings.General.saveAsDashBoard()
.should('be.visible')
.click();
e2e.pages.SaveDashboardAsModal.newName()
.should('be.visible')
.type(`${Date.now()}`);
e2e.pages.SaveDashboardAsModal.save()
.should('be.visible')
.click();
e2e().wait('@save-dash');
e2e().wait('@load-dash');
e2e.pages.Dashboard.SubMenu.submenuItem().should('be.visible');
e2e()
.location()
.then(loc => {
const dashuid = /\/d\/(\w+)\//.exec(loc.href)![1];
e2e()
.wrap(dashuid)
.as('dashuid');
});
e2e().wait(500);
}
function saveDashboard(saveVariables: boolean) {
e2e.pages.Dashboard.Toolbar.toolbarItems('Save dashboard')
.should('be.visible')
.click();
if (saveVariables) {
e2e.pages.SaveDashboardModal.saveVariables()
.should('exist')
.click({ force: true });
}
e2e.pages.SaveDashboardModal.save()
.should('be.visible')
.click();
e2e().wait('@save-dash');
}
function validateTextboxAndMarkup(value: string) {
e2e.pages.Dashboard.SubMenu.submenuItem()
.should('be.visible')
.within(() => {
e2e.pages.Dashboard.SubMenu.submenuItemLabels('text').should('be.visible');
e2e()
.get('input')
.should('be.visible')
.should('have.value', value);
});
e2e.components.Panels.Visualization.Text.container()
.should('be.visible')
.within(() => {
e2e()
.get('h1')
.should('be.visible')
.should('have.text', `variable: ${value}`);
});
}
function validateVariable(value: string) {
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings')
.should('be.visible')
.click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables')
.should('be.visible')
.click();
e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields('text')
.should('be.visible')
.click();
e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInput()
.should('be.visible')
.should('have.value', value);
}
function changeTextBoxInput() {
e2e.pages.Dashboard.SubMenu.submenuItemLabels('text').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItem()
.should('be.visible')
.within(() => {
e2e()
.get('input')
.should('be.visible')
.should('have.value', 'default value')
.clear()
.type('changed value')
.type('{enter}');
});
e2e()
.location()
.should(loc => {
expect(loc.search).to.contain('var-text=changed%20value');
});
}
function changeQueryInput() {
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings')
.should('be.visible')
.click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables')
.should('be.visible')
.click();
e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields('text')
.should('be.visible')
.click();
e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInput()
.should('be.visible')
.clear()
.type('changed value')
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption()
.should('have.length', 1)
.should('have.text', 'changed value');
}
+3 -2
View File
@@ -62,9 +62,9 @@ require (
github.com/opentracing/opentracing-go v1.2.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.7.1
github.com/prometheus/client_golang v1.8.0
github.com/prometheus/client_model v0.2.0
github.com/prometheus/common v0.10.0
github.com/prometheus/common v0.14.0
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
github.com/robfig/cron/v3 v3.0.0
@@ -77,6 +77,7 @@ require (
github.com/uber/jaeger-client-go v2.25.0+incompatible
github.com/unknwon/com v1.0.1
github.com/urfave/cli/v2 v2.1.1
github.com/weaveworks/common v0.0.0-20201119133501-0619918236ec
github.com/xorcare/pointer v1.1.0
github.com/yudai/gojsondiff v1.0.0
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
+22
View File
@@ -329,6 +329,8 @@ github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fatih/structtag v1.1.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA=
github.com/fluent/fluent-bit-go v0.0.0-20190925192703-ea13c021720c/go.mod h1:WQX+afhrekY9rGK+WT4xvKSlzmia9gDoLYu4GGYGASQ=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
@@ -489,6 +491,7 @@ github.com/gocql/gocql v0.0.0-20200121121104-95d072f1b5bb/go.mod h1:DL0ekTmBSTdl
github.com/gocql/gocql v0.0.0-20200526081602-cd04bd7f22a7/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY=
github.com/godbus/dbus v0.0.0-20190402143921-271e53dc4968/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/googleapis v1.1.0 h1:kFkMAZBNAn4j7K0GiZr8cRYzejq68VbheufiV3YuyFI=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -499,6 +502,7 @@ github.com/gogo/protobuf v1.2.2-0.20190730201129-28a6bbf47e48/go.mod h1:SlYgWuQ5
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/status v1.0.3 h1:WkVBY59mw7qUNTr/bLwO7J2vesJ0rQ2C3tMXrTd3w5M=
github.com/gogo/status v1.0.3/go.mod h1:SavQ51ycCLnc7dGyJxp8YAmudx8xqiVrRf+6IXRsugc=
github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
@@ -610,6 +614,7 @@ github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de/go.mod h1:wJfORR
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
@@ -803,6 +808,8 @@ github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH6
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@@ -980,6 +987,7 @@ github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM
github.com/opentracing-contrib/go-grpc v0.0.0-20180928155321-4b5a12d3ff02/go.mod h1:JNdpVEzCpXBgIiv4ds+TzhN1hrtxq6ClLrTlT9OQRSc=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing-contrib/go-stdlib v0.0.0-20190519235532-cf7a6c988dc9/go.mod h1:PLldrQSroqzH70Xl+1DQcGnefIbqsKR7UDaiux3zV+w=
github.com/opentracing-contrib/go-stdlib v1.0.0 h1:TBS7YuVotp8myLon4Pv7BtCBzOTo1DeZCld0Z63mW2w=
github.com/opentracing-contrib/go-stdlib v1.0.0/go.mod h1:qtI1ogk+2JhVPIXVc6q+NHziSmy2W5GbdQZFUHADCBU=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
@@ -1039,6 +1047,8 @@ github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O
github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4=
github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.8.0 h1:zvJNkoCFAnYFNC24FV8nW4JdRJ3GIFcLbg65lL/JDcw=
github.com/prometheus/client_golang v1.8.0/go.mod h1:O9VU6huf47PktckDQfMTX0Y8tY0/7TSWwj+ITvv0TnM=
github.com/prometheus/client_model v0.0.0-20170216185247-6f3806018612/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@@ -1057,6 +1067,8 @@ github.com/prometheus/common v0.8.0/go.mod h1:PC/OgXc+UN7B4ALwvn1yzVZmVwvhXp5Jsb
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.14.0 h1:RHRyE8UocrbjU+6UvRzwi6HjiDfxrrBU91TtbKzkGp4=
github.com/prometheus/common v0.14.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/node_exporter v1.0.0-rc.0.0.20200428091818-01054558c289/go.mod h1:FGbBv5OPKjch+jNUJmEQpMZytIdyW0NdBtWFcfSKusc=
github.com/prometheus/procfs v0.0.0-20180612222113-7d6f385de8be/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@@ -1071,6 +1083,8 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/prometheus v0.0.0-20180315085919-58e2a31db8de/go.mod h1:oAIUtOny2rjMX0OWN5vPR5/q/twIROJvdqnQKDdil/s=
github.com/prometheus/prometheus v0.0.0-20190818123050-43acd0e2e93f/go.mod h1:rMTlmxGCvukf2KMu3fClMDKLLoJ5hl61MhcJ7xKakf0=
github.com/prometheus/prometheus v1.8.2-0.20200107122003-4708915ac6ef/go.mod h1:7U90zPoLkWjEIQcy/rweQla82OCTUzxVHE51G3OhJbI=
@@ -1135,7 +1149,10 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
@@ -1194,6 +1211,7 @@ github.com/ua-parser/uap-go v0.0.0-20190826212731-daf92ba38329 h1:VBsKFh4W1JEMz3
github.com/ua-parser/uap-go v0.0.0-20190826212731-daf92ba38329/go.mod h1:OBcG9bn7sHtXgarhUEb3OfCnNsgtGnkVf41ilSZ3K3E=
github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-client-go v2.20.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-client-go v2.22.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-client-go v2.23.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-client-go v2.24.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-client-go v2.25.0+incompatible h1:IxcNZ7WRY1Y3G4poYlx24szfsn/3LvK9QHCq9oQw8+U=
@@ -1222,6 +1240,9 @@ github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
github.com/weaveworks/common v0.0.0-20200206153930-760e36ae819a/go.mod h1:6enWAqfQBFrE8X/XdJwZr8IKgh1chStuFR0mjU/UOUw=
github.com/weaveworks/common v0.0.0-20200625145055-4b1847531bc9/go.mod h1:c98fKi5B9u8OsKGiWHLRKus6ToQ1Tubeow44ECO1uxY=
github.com/weaveworks/common v0.0.0-20201119133501-0619918236ec h1:5JmevdpzK10Z2ua0VDToj7Kg2+/t0FzdYBjsurYRE8k=
github.com/weaveworks/common v0.0.0-20201119133501-0619918236ec/go.mod h1:ykzWac1LtVfOxdCK+jD754at1Ws9dKCwFeUzkFBffPs=
github.com/weaveworks/promrus v1.2.0 h1:jOLf6pe6/vss4qGHjXmGz4oDJQA+AOCqEL3FvvZGz7M=
github.com/weaveworks/promrus v1.2.0/go.mod h1:SaE82+OJ91yqjrE1rsvBWVzNZKcHYFtMUyS1+Ogs/KA=
github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
@@ -1516,6 +1537,7 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd h1:WgqgiQvkiZWz7XLhphjt2GI2GcGCTIZs9jqXMWmH+oc=
golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+23 -32
View File
@@ -7,13 +7,15 @@
"repository": "github:grafana/grafana",
"scripts": {
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
"build": "grunt build",
"build": "node ./node_modules/webpack/bin/webpack.js --config scripts/webpack/webpack.prod.js",
"dev": "webpack --progress --colors --config scripts/webpack/webpack.dev.js",
"e2e": "./e2e/start-and-run-suite",
"e2e:debug": "./e2e/start-and-run-suite debug",
"e2e:dev": "./e2e/start-and-run-suite dev",
"test": "jest --notify --watch",
"lint": "eslint . --ext .js,.tsx,.ts --cache",
"lint": "yarn run lint:ts && yarn run lint:sass",
"lint:ts": "eslint . --ext .js,.tsx,.ts --cache",
"lint:sass": "yarn run sass-lint -c public/sass/.sass-lint.yml 'public/sass/**/*.scss, packages/**/*.scss' -v -i '**/node_modules/**/*.scss'",
"test:ci": "mkdir -p reports/junit && export JEST_JUNIT_OUTPUT_DIR=reports/junit && jest --ci --reporters=default --reporters=jest-junit -w ${TEST_MAX_WORKERS:-100%}",
"lint:fix": "yarn lint --fix",
"packages:build": "lerna run clean && lerna run build --ignore @grafana-plugins/input-datasource",
@@ -27,7 +29,7 @@
"packages:publishDev": "lerna publish from-package --contents dist --dist-tag dev --yes --registry http://grafana-npm.local:4873 --force-publish=*",
"packages:typecheck": "lerna run typecheck",
"packages:clean": "lerna run clean",
"precommit": "grafana-toolkit precommit",
"precommit": "yarn run lint-staged",
"prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\"",
"prettier:write": "prettier --list-different \"**/*.{ts,tsx,scss}\" --write",
"start": "grafana-toolkit core:start --watchTheme",
@@ -38,10 +40,10 @@
"storybook": "yarn workspace @grafana/ui storybook --ci",
"storybook:build": "yarn workspace @grafana/ui storybook:build",
"themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts",
"typecheck": "tsc --noEmit",
"typecheck": "tsc --noEmit && yarn run packages:typecheck",
"plugins:build-bundled": "grafana-toolkit plugin:bundle-managed",
"watch": "yarn start -d watch,start core:start --watchTheme",
"ci:test-frontend": "yarn run prettier:check && yarn run packages:typecheck && yarn run typecheck && yarn grunt no-only-tests && yarn run test:ci"
"ci:test-frontend": "yarn run prettier:check && yarn run typecheck && yarn run lint && yarn run test:ci && yarn grafana-toolkit node-version-check"
},
"grafana": {
"whatsNewUrl": "https://grafana.com/docs/grafana/latest/guides/whats-new-in-v7-3/",
@@ -49,7 +51,7 @@
},
"husky": {
"hooks": {
"pre-commit": "lint-staged && npm run precommit"
"pre-commit": "yarn run precommit"
}
},
"lint-staged": {
@@ -72,7 +74,7 @@
"@babel/preset-react": "7.8.3",
"@babel/preset-typescript": "7.8.3",
"@emotion/core": "10.0.27",
"@grafana/api-documenter": "7.11.1",
"@grafana/api-documenter": "7.11.2",
"@grafana/api-extractor": "7.10.1",
"@grafana/eslint-config": "2.0.6",
"@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1",
@@ -90,7 +92,7 @@
"@types/enzyme-adapter-react-16": "1.0.6",
"@types/file-saver": "2.0.1",
"@types/is-hotkey": "0.1.1",
"@types/jest": "26.0.0",
"@types/jest": "26.0.15",
"@types/jquery": "3.3.38",
"@types/lodash": "4.14.149",
"@types/lru-cache": "^5.1.0",
@@ -102,7 +104,7 @@
"@types/react": "16.9.9",
"@types/react-beautiful-dnd": "12.1.2",
"@types/react-dom": "16.9.9",
"@types/react-grid-layout": "0.16.7",
"@types/react-grid-layout": "1.1.1",
"@types/react-redux": "7.1.7",
"@types/react-select": "3.0.8",
"@types/react-test-renderer": "16.9.2",
@@ -124,7 +126,7 @@
"autoprefixer": "9.7.4",
"axios": "0.19.2",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "26.0.1",
"babel-jest": "26.6.3",
"babel-loader": "8.0.6",
"babel-plugin-angularjs-annotate": "0.10.0",
"clean-webpack-plugin": "3.0.0",
@@ -134,9 +136,10 @@
"es-abstract": "1.18.0-next.1",
"es6-promise": "4.2.8",
"es6-shim": "0.35.5",
"eslint": "7.4.0",
"eslint": "^7.14.0",
"eslint-config-prettier": "6.11.0",
"eslint-plugin-jsdoc": "28.6.1",
"eslint-plugin-no-only-tests": "^2.4.0",
"eslint-plugin-prettier": "3.1.4",
"eslint-plugin-react": "7.21.5",
"eslint-plugin-react-hooks": "4.1.2",
@@ -146,24 +149,12 @@
"fork-ts-checker-webpack-plugin": "5.0.0",
"gaze": "1.1.3",
"glob": "7.1.6",
"grunt": "1.0.4",
"grunt-angular-templates": "1.1.0",
"grunt-cli": "1.3.2",
"grunt-contrib-clean": "2.0.0",
"grunt-contrib-copy": "1.0.0",
"grunt-exec": "3.0.0",
"grunt-newer": "1.3.0",
"grunt-notify": "0.4.5",
"grunt-postcss": "0.9.0",
"grunt-sass-lint": "0.2.4",
"grunt-usemin": "3.1.1",
"grunt-webpack": "3.1.3",
"html-loader": "0.5.5",
"html-webpack-harddisk-plugin": "1.0.1",
"html-webpack-plugin": "3.2.0",
"husky": "4.2.1",
"jest": "26.4.2",
"jest-canvas-mock": "2.2.0",
"jest": "26.6.3",
"jest-canvas-mock": "2.3.0",
"jest-date-mock": "1.0.8",
"jest-matcher-utils": "26.0.0",
"lerna": "^3.20.2",
@@ -193,10 +184,10 @@
"sinon": "8.1.1",
"style-loader": "1.1.3",
"terser-webpack-plugin": "2.3.5",
"ts-jest": "26.3.0",
"ts-node": "8.8.1",
"tslib": "2.0.1",
"typescript": "4.0.2",
"ts-jest": "26.4.4",
"ts-node": "9.0.0",
"tslib": "2.0.3",
"typescript": "4.1.2",
"webpack": "4.41.5",
"webpack-bundle-analyzer": "3.6.0",
"webpack-cleanup-plugin": "0.5.1",
@@ -267,7 +258,7 @@
"react": "17.0.1",
"react-beautiful-dnd": "13.0.0",
"react-dom": "17.0.1",
"react-grid-layout": "0.17.1",
"react-grid-layout": "1.2.0",
"react-highlight-words": "0.16.0",
"react-loadable": "5.5.0",
"react-popper": "2.2.4",
@@ -318,9 +309,9 @@
"puppeteer": "node_modules/puppeteer-core"
},
"engines": {
"node": ">= 12"
"node": ">= 14"
},
"volta": {
"node": "12.19.1"
"node": "14.15.1"
}
}
+23 -1
View File
@@ -27,7 +27,29 @@ Every commit to master that has changes within the `packages` directory is a sub
<lerna.json version>-<COMMIT_SHA>
```
Automatic prereleases are published under the `canary` dist tag.
Automatic prereleases are published under the `canary` dist tag to the [github package registry](https://docs.github.com/en/free-pro-team@latest/packages/publishing-and-managing-packages/about-github-packages).
#### Consuming prereleases
As mentioned above the `canary` releases are published to the Github package registry rather than the NPM registry. If you wish to make use of these prereleases please follow these steps:
1. You must use a personal access token to install packages from Github. To create an access token [click here](https://github.com/settings/tokens) and create a token with the `read:packages` scope. Make a copy of the token.
2. Create / modify your `~/.npmrc` file with the following:
```
@grafana:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken={INSERT_GH_TOKEN_HERE}
```
3. Update the package.json of your project to use either the `canary` channel or a version of the `canary` channel
```json
// plugin's package.json
{
...
"@grafana/data": "canary"
}
```
### Manual release
+2 -2
View File
@@ -37,7 +37,7 @@
"@rollup/plugin-json": "4.0.3",
"@rollup/plugin-node-resolve": "7.1.1",
"@types/braintree__sanitize-url": "4.0.0",
"@types/jest": "26.0.0",
"@types/jest": "26.0.15",
"@types/jquery": "3.3.38",
"@types/lodash": "4.14.123",
"@types/marked": "1.1.0",
@@ -53,6 +53,6 @@
"rollup-plugin-typescript2": "0.26.0",
"rollup-plugin-visualizer": "3.3.1",
"sinon": "8.1.1",
"typescript": "4.0.2"
"typescript": "4.1.2"
}
}
@@ -55,6 +55,9 @@ export function arrowTableToDataFrame(table: Table): ArrowDataFrame {
case ArrowType.Decimal:
case ArrowType.FloatingPoint: {
type = FieldType.number;
if (col.nullCount) {
values = new WrappedColumn(col);
}
break;
}
case ArrowType.Int: {
@@ -64,6 +67,9 @@ export function arrowTableToDataFrame(table: Table): ArrowDataFrame {
}
case ArrowType.Bool: {
type = FieldType.boolean;
if (col.nullCount) {
values = new WrappedColumn(col);
}
break;
}
case ArrowType.Timestamp: {
@@ -202,3 +208,20 @@ class NumberColumn extends FunctionalVector<number> {
return Number(v);
}
}
// The `toArray()` arrow function will return a native ArrayBuffer -- this works fine
// if there are no null values, but the native arrays do not support nulls. This
// class simply wraps the vector so `toArray` creates a new array
class WrappedColumn extends FunctionalVector {
constructor(private col: Column) {
super();
}
get length() {
return this.col.length;
}
get(index: number): number {
return this.col.get(index);
}
}
@@ -265,7 +265,7 @@ Object {
"labels": undefined,
"name": "float32_values",
"type": "number",
"values": Array [
"values": Float32Array [
1.401298464324817e-45,
1.401298464324817e-45,
1,
@@ -24,6 +24,7 @@ import { SortedVector } from '../vector/SortedVector';
import { ArrayDataFrame } from './ArrayDataFrame';
import { getFieldDisplayName } from '../field/fieldState';
import { fieldIndexComparer } from '../field/fieldComparers';
import { vectorToArray } from '../vector/vectorToArray';
function convertTableToDataFrame(table: TableData): DataFrame {
const fields = table.columns.map(c => {
@@ -444,16 +445,10 @@ export function getDataFrameRow(data: DataFrame, row: number): any[] {
export function toDataFrameDTO(data: DataFrame): DataFrameDTO {
const fields: FieldDTO[] = data.fields.map(f => {
let values = f.values.toArray();
if (!Array.isArray(values)) {
// Apache arrow will pack objects into typed arrays
// Float64Array, etc
// TODO: Float64Array could be used directly
values = [];
for (let i = 0; i < f.values.length; i++) {
values.push(f.values.get(i));
}
// The byte buffers serialize like objects
if (values instanceof Float64Array) {
values = vectorToArray(f.values);
}
return {
name: f.name,
type: f.type,
@@ -77,7 +77,6 @@ export interface GetFieldDisplayValuesOptions {
replaceVariables: InterpolateFunction;
sparkline?: boolean; // Calculate the sparkline
theme: GrafanaTheme;
autoMinMax?: boolean;
timeZone?: TimeZone;
}
@@ -122,7 +121,11 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
continue;
}
const config = field.config; // already set by the prepare task
let config = field.config; // already set by the prepare task
if (field.state?.range) {
// Us the global min/max values
config = { ...config, ...field.state?.range };
}
const displayName = field.config.displayName ?? defaultDisplayName;
const display =
@@ -157,7 +160,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
});
values.push({
name,
name: '',
field: config,
display: displayValue,
view,
@@ -183,7 +183,6 @@ describe('applyFieldOverrides', () => {
overrides: [],
},
replaceVariables: (value: any) => value,
getDataSourceSettingsByUid: undefined as any,
theme: getTestTheme(),
fieldConfigRegistry: new FieldConfigOptionsRegistry(),
});
@@ -245,7 +244,6 @@ describe('applyFieldOverrides', () => {
overrides: [],
},
fieldConfigRegistry: customFieldRegistry,
getDataSourceSettingsByUid: undefined as any,
replaceVariables: v => v,
theme: getTestTheme(),
})[0];
@@ -263,7 +261,6 @@ describe('applyFieldOverrides', () => {
data: [f0], // the frame
fieldConfig: src as FieldConfigSource, // defaults + overrides
replaceVariables: (undefined as any) as InterpolateFunction,
getDataSourceSettingsByUid: undefined as any,
theme: getTestTheme(),
fieldConfigRegistry: customFieldRegistry,
})[0];
@@ -291,18 +288,16 @@ describe('applyFieldOverrides', () => {
data: [f0], // the frame
fieldConfig: src as FieldConfigSource, // defaults + overrides
replaceVariables: (undefined as any) as InterpolateFunction,
getDataSourceSettingsByUid: undefined as any,
theme: getTestTheme(),
autoMinMax: true,
})[0];
const valueColumn = data.fields[1];
const config = valueColumn.config;
const range = valueColumn.state!.range!;
// Keep max from the original setting
expect(config.max).toEqual(0);
expect(range.max).toEqual(0);
// Don't Automatically pick the min value
expect(config.min).toEqual(-20);
expect(range.min).toEqual(-20);
});
it('getLinks should use applied field config', () => {
@@ -315,9 +310,7 @@ describe('applyFieldOverrides', () => {
replaceVariablesCalls.push(variables);
return value;
}) as InterpolateFunction,
getDataSourceSettingsByUid: undefined as any,
theme: getTestTheme(),
autoMinMax: true,
fieldConfigRegistry: customFieldRegistry,
})[0];
@@ -567,7 +560,6 @@ describe('getLinksSupplier', () => {
{},
replaceSpy,
// this is used only for internal links so isn't needed here
() => ({} as any),
{
theme: getTestTheme(),
}
@@ -600,6 +592,7 @@ describe('getLinksSupplier', () => {
title: '',
internal: {
datasourceUid: '0',
datasourceName: 'testDS',
query: '12345',
},
},
@@ -615,7 +608,6 @@ describe('getLinksSupplier', () => {
{},
// We do not need to interpolate anything for this test
(value, vars, format) => value,
uid => ({ name: 'testDS' } as any),
{ theme: getTestTheme() }
);
const links = supplier({ valueRowIndex: 0 });
@@ -2,7 +2,6 @@ import {
ApplyFieldOverrideOptions,
DataFrame,
DataLink,
DataSourceInstanceSettings,
DynamicConfigValue,
Field,
FieldColorModeId,
@@ -13,6 +12,7 @@ import {
GrafanaTheme,
InterpolateFunction,
LinkModel,
NumericRange,
ScopedVars,
TimeZone,
ValueLinkConfig,
@@ -40,12 +40,7 @@ interface OverrideProps {
properties: DynamicConfigValue[];
}
interface GlobalMinMax {
min?: number | null;
max?: number | null;
}
export function findNumericFieldMinMax(data: DataFrame[]): GlobalMinMax {
export function findNumericFieldMinMax(data: DataFrame[]): NumericRange {
let min: number | null = null;
let max: number | null = null;
@@ -69,7 +64,7 @@ export function findNumericFieldMinMax(data: DataFrame[]): GlobalMinMax {
}
}
return { min, max };
return { min, max, delta: (max ?? 0) - (min ?? 0) };
}
/**
@@ -88,7 +83,7 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
const fieldConfigRegistry = options.fieldConfigRegistry ?? standardFieldConfigEditorRegistry;
let seriesIndex = 0;
let range: GlobalMinMax | undefined = undefined;
let globalRange: NumericRange | undefined = undefined;
// Prepare the Matchers
const override: OverrideProps[] = [];
@@ -134,7 +129,6 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
data: options.data!,
dataFrameIndex: index,
replaceVariables: options.replaceVariables,
getDataSourceSettingsByUid: options.getDataSourceSettingsByUid,
fieldConfigRegistry: fieldConfigRegistry,
};
@@ -178,18 +172,14 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
}
// Set the Min/Max value automatically
if (options.autoMinMax && field.type === FieldType.number) {
if (!isNumber(config.min) || !isNumber(config.max)) {
if (!range) {
range = findNumericFieldMinMax(options.data!); // Global value
}
if (!isNumber(config.min)) {
config.min = range.min;
}
if (!isNumber(config.max)) {
config.max = range.max;
}
let range: NumericRange | undefined = undefined;
if (field.type === FieldType.number) {
if (!globalRange && (!isNumber(config.min) || !isNumber(config.max))) {
globalRange = findNumericFieldMinMax(options.data!);
}
const min = config.min ?? globalRange!.min;
const max = config.max ?? globalRange!.max;
range = { min, max, delta: max! - min! };
}
// Some color modes needs series index to assign field color so we count
@@ -207,6 +197,7 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
...field.state,
displayName: null,
seriesIndex,
range,
},
};
@@ -218,17 +209,10 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
});
// Attach data links supplier
newField.getLinks = getLinksSupplier(
newFrame,
newField,
fieldScopedVars,
context.replaceVariables,
context.getDataSourceSettingsByUid,
{
theme: options.theme,
timeZone: options.timeZone,
}
);
newField.getLinks = getLinksSupplier(newFrame, newField, fieldScopedVars, context.replaceVariables, {
theme: options.theme,
timeZone: options.timeZone,
});
return newField;
});
@@ -343,7 +327,6 @@ export const getLinksSupplier = (
field: Field,
fieldScopedVars: ScopedVars,
replaceVariables: InterpolateFunction,
getDataSourceSettingsByUid: (uid: string) => DataSourceInstanceSettings | undefined,
options: {
theme: GrafanaTheme;
timeZone?: TimeZone;
@@ -408,9 +391,13 @@ export const getLinksSupplier = (
if (link.internal) {
// For internal links at the moment only destination is Explore.
return mapInternalLinkToExplore(link, variables, {} as any, field, {
return mapInternalLinkToExplore({
link,
internalLink: link.internal,
scopedVars: variables,
field,
range: {} as any,
replaceVariables,
getDataSourceSettingsByUid,
});
} else {
let href = locationUtil.assureBaseUrl(link.url.replace(/\n/g, ''));
@@ -29,10 +29,8 @@ describe('getFieldDisplayValuesProxy', () => {
overrides: [],
},
replaceVariables: (val: string) => val,
getDataSourceSettingsByUid: (val: string) => ({} as any),
timeZone: 'utc',
theme: getTestTheme(),
autoMinMax: true,
})[0];
it('should define all display functions', () => {
+15 -9
View File
@@ -1,6 +1,6 @@
import { isNumber } from 'lodash';
import { reduceField, ReducerID } from '../transformations/fieldReducer';
import { Field, FieldType, GrafanaTheme, Threshold } from '../types';
import { Field, FieldConfig, FieldType, GrafanaTheme, NumericRange, Threshold } from '../types';
import { getFieldColorModeForField } from './fieldColor';
import { getActiveThresholdForValue } from './thresholds';
@@ -15,7 +15,7 @@ export type ScaleCalculator = (value: number) => ScaledValue;
export function getScaleCalculator(field: Field, theme: GrafanaTheme): ScaleCalculator {
const mode = getFieldColorModeForField(field);
const getColor = mode.getCalculator(field, theme);
const info = getMinMaxAndDelta(field);
const info = field.state?.range ?? getMinMaxAndDelta(field);
return (value: number) => {
let percent = 0;
@@ -34,13 +34,7 @@ export function getScaleCalculator(field: Field, theme: GrafanaTheme): ScaleCalc
};
}
interface FieldMinMaxInfo {
min?: number | null;
max?: number | null;
delta: number;
}
function getMinMaxAndDelta(field: Field): FieldMinMaxInfo {
function getMinMaxAndDelta(field: Field): NumericRange {
if (field.type !== FieldType.number) {
return { min: 0, max: 100, delta: 100 };
}
@@ -70,3 +64,15 @@ function getMinMaxAndDelta(field: Field): FieldMinMaxInfo {
delta: max! - min!,
};
}
export function getFieldConfigWithMinMax(field: Field, local?: boolean): FieldConfig {
const { config } = field;
let { min, max } = config;
if (isNumber(min) && !isNumber(max)) {
return config; // noop
}
if (local || !field.state?.range) {
return { ...config, ...getMinMaxAndDelta(field) };
}
return { ...config, ...field.state.range };
}
+5
View File
@@ -13,4 +13,9 @@ export * from './text';
export * from './valueFormats';
export * from './field';
export * from './events';
export {
ValueMatcherOptions,
BasicValueMatcherOptions,
RangeValueMatcherOptions,
} from './transformations/matchers/valueMatchers/types';
export { PanelPlugin } from './panel/PanelPlugin';
@@ -9,9 +9,16 @@ import {
FrameMatcherInfo,
FieldMatcher,
FrameMatcher,
ValueMatcherInfo,
ValueMatcher,
} from '../types/transformations';
import { Registry } from '../utils/Registry';
import { getNullValueMatchers } from './matchers/valueMatchers/nullMatchers';
import { getNumericValueMatchers } from './matchers/valueMatchers/numericMatchers';
import { getEqualValueMatchers } from './matchers/valueMatchers/equalMatchers';
import { getRangeValueMatchers } from './matchers/valueMatchers/rangeMatchers';
import { getSimpleFieldMatchers } from './matchers/simpleFieldMatcher';
import { getRegexValueMatcher } from './matchers/valueMatchers/regexMatchers';
/**
* Registry that contains all of the built in field matchers.
@@ -38,6 +45,20 @@ export const frameMatchers = new Registry<FrameMatcherInfo>(() => {
];
});
/**
* Registry that contains all of the built in value matchers.
* @public
*/
export const valueMatchers = new Registry<ValueMatcherInfo>(() => {
return [
...getNullValueMatchers(),
...getNumericValueMatchers(),
...getEqualValueMatchers(),
...getRangeValueMatchers(),
...getRegexValueMatcher(),
];
});
/**
* Resolves a field matcher from the registry for given config.
* Will throw an error if matcher can not be resolved.
@@ -45,6 +66,9 @@ export const frameMatchers = new Registry<FrameMatcherInfo>(() => {
*/
export function getFieldMatcher(config: MatcherConfig): FieldMatcher {
const info = fieldMatchers.get(config.id);
if (!info) {
throw new Error('Unknown field matcher: ' + config.id);
}
return info.get(config.options);
}
@@ -55,5 +79,21 @@ export function getFieldMatcher(config: MatcherConfig): FieldMatcher {
*/
export function getFrameMatchers(config: MatcherConfig): FrameMatcher {
const info = frameMatchers.get(config.id);
if (!info) {
throw new Error('Unknown frame matcher: ' + config.id);
}
return info.get(config.options);
}
/**
* Resolves a value matcher from the registry for given config.
* Will throw an error if matcher can not be resolved.
* @public
*/
export function getValueMatcher(config: MatcherConfig): ValueMatcher {
const info = valueMatchers.get(config.id);
if (!info) {
throw new Error('Unknown value matcher: ' + config.id);
}
return info.get(config.options);
}
@@ -23,6 +23,7 @@ export enum FieldMatcherID {
byNames = 'byNames',
byRegexp = 'byRegexp',
byRegexpOrNames = 'byRegexpOrNames',
byFrameRefID = 'byFrameRefID',
// byIndex = 'byIndex',
// byLabel = 'byLabel',
}
@@ -36,3 +37,19 @@ export enum FrameMatcherID {
byIndex = 'byIndex',
byLabel = 'byLabel',
}
/**
* @public
*/
export enum ValueMatcherID {
regex = 'regex',
isNull = 'isNull',
isNotNull = 'isNotNull',
greater = 'greater',
greaterOrEqual = 'greaterOrEqual',
lower = 'lower',
lowerOrEqual = 'lowerOrEqual',
equal = 'equal',
notEqual = 'notEqual',
between = 'between',
}
@@ -306,3 +306,31 @@ describe('Field Regexp or Names Matcher', () => {
}
});
});
describe('Fields returned by query with refId', () => {
it('Match all fields in frame with refId: A', () => {
const data = [
toDataFrame({
refId: 'A',
fields: [{ name: 'field_1' }, { name: 'field_2' }],
}),
toDataFrame({
refId: 'B',
fields: [{ name: 'field_1' }, { name: 'field_2' }],
}),
];
const matcher = getFieldMatcher({
id: FieldMatcherID.byFrameRefID,
options: 'A',
});
const frameA = data[0];
expect(matcher(frameA.fields[0], frameA, data)).toBe(true);
expect(matcher(frameA.fields[1], frameA, data)).toBe(true);
const frameB = data[1];
expect(matcher(frameB.fields[0], frameB, data)).toBe(false);
expect(matcher(frameB.fields[1], frameB, data)).toBe(false);
});
});
@@ -66,6 +66,28 @@ const regexpFieldNameMatcher: FieldMatcherInfo<string> = {
},
};
/**
* Field matcher that will match all fields that exists in a
* data frame with configured refId.
* @public
*/
const fieldsInFrameMatcher: FieldMatcherInfo<string> = {
id: FieldMatcherID.byFrameRefID,
name: 'Fields by frame refId',
description: 'match all fields returned in data frame with refId.',
defaultOptions: '',
get: (refId: string): FieldMatcher => {
return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => {
return frame.refId === refId;
};
},
getOptionsDisplayText: (refId: string): string => {
return `Math all fields returned by query with reference ID: ${refId}`;
},
};
const regexpOrMultipleNamesMatcher: FieldMatcherInfo<RegexpOrNamesMatcherOptions> = {
id: FieldMatcherID.byRegexpOrNames,
name: 'Field Name by Regexp or Names',
@@ -127,7 +149,13 @@ const frameNameMatcher: FrameMatcherInfo<string> = {
* Registry Initialization
*/
export function getFieldNameMatchers(): FieldMatcherInfo[] {
return [fieldNameMatcher, regexpFieldNameMatcher, multipleFieldNamesMatcher, regexpOrMultipleNamesMatcher];
return [
fieldNameMatcher,
regexpFieldNameMatcher,
multipleFieldNamesMatcher,
regexpOrMultipleNamesMatcher,
fieldsInFrameMatcher,
];
}
export function getFrameNameMatchers(): FrameMatcherInfo[] {
@@ -0,0 +1,108 @@
import { toDataFrame } from '../../../dataframe';
import { DataFrame } from '../../../types/dataFrame';
import { getValueMatcher } from '../../matchers';
import { ValueMatcherID } from '../ids';
describe('value equals to matcher', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{
name: 'temp',
values: [23, null, 10, 'asd', '23'],
},
],
}),
];
const matcher = getValueMatcher({
id: ValueMatcherID.equal,
options: {
value: 23,
},
});
it('should match when option value is same', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 0;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('should not match when option value is different', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 2;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
it('should not match when option value is different type', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 3;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
it('should match when option value is different type but same', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 4;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
});
describe('value not equals matcher', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{
name: 'temp',
values: [23, null, 10, 'asd', '23'],
},
],
}),
];
const matcher = getValueMatcher({
id: ValueMatcherID.notEqual,
options: {
value: 23,
},
});
it('should not match when option value is same', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 0;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
it('should match when option value is different', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 2;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('should match when option value is different type', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 3;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('should not match when option value is different type but same', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 4;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
});
@@ -0,0 +1,42 @@
import { Field } from '../../../types/dataFrame';
import { ValueMatcherInfo } from '../../../types/transformations';
import { ValueMatcherID } from '../ids';
import { BasicValueMatcherOptions } from './types';
const isEqualValueMatcher: ValueMatcherInfo<BasicValueMatcherOptions> = {
id: ValueMatcherID.equal,
name: 'Is equal',
description: 'Match where value for given field is equal to options value.',
get: options => {
return (valueIndex: number, field: Field) => {
const value = field.values.get(valueIndex);
// eslint-disable-next-line eqeqeq
return value == options.value;
};
},
getOptionsDisplayText: () => {
return `Matches all rows where field is null.`;
},
isApplicable: () => true,
getDefaultOptions: () => ({ value: '' }),
};
const isNotEqualValueMatcher: ValueMatcherInfo<BasicValueMatcherOptions> = {
id: ValueMatcherID.notEqual,
name: 'Is not equal',
description: 'Match where value for given field is not equal to options value.',
get: options => {
return (valueIndex: number, field: Field) => {
const value = field.values.get(valueIndex);
// eslint-disable-next-line eqeqeq
return value != options.value;
};
},
getOptionsDisplayText: () => {
return `Matches all rows where field is not null.`;
},
isApplicable: () => true,
getDefaultOptions: () => ({ value: '' }),
};
export const getEqualValueMatchers = (): ValueMatcherInfo[] => [isEqualValueMatcher, isNotEqualValueMatcher];
@@ -0,0 +1,72 @@
import { toDataFrame } from '../../../dataframe';
import { DataFrame } from '../../../types/dataFrame';
import { getValueMatcher } from '../../matchers';
import { ValueMatcherID } from '../ids';
describe('value null matcher', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{
name: 'temp',
values: [23, null, 10],
},
],
}),
];
const matcher = getValueMatcher({
id: ValueMatcherID.isNull,
options: {},
});
it('should match null values', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 1;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('should not match non-null values', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 0;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
});
describe('value not null matcher', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{
name: 'temp',
values: [23, null, 10],
},
],
}),
];
const matcher = getValueMatcher({
id: ValueMatcherID.isNotNull,
options: {},
});
it('should match not null values', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 0;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('should match non-null values', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 1;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
});
@@ -0,0 +1,40 @@
import { Field } from '../../../types/dataFrame';
import { ValueMatcherInfo } from '../../../types/transformations';
import { ValueMatcherID } from '../ids';
import { ValueMatcherOptions } from './types';
const isNullValueMatcher: ValueMatcherInfo<ValueMatcherOptions> = {
id: ValueMatcherID.isNull,
name: 'Is null',
description: 'Match where value for given field is null.',
get: () => {
return (valueIndex: number, field: Field) => {
const value = field.values.get(valueIndex);
return value === null;
};
},
getOptionsDisplayText: () => {
return `Matches all rows where field is null.`;
},
isApplicable: () => true,
getDefaultOptions: () => ({}),
};
const isNotNullValueMatcher: ValueMatcherInfo<ValueMatcherOptions> = {
id: ValueMatcherID.isNotNull,
name: 'Is not null',
description: 'Match where value for given field is not null.',
get: () => {
return (valueIndex: number, field: Field) => {
const value = field.values.get(valueIndex);
return value !== null;
};
},
getOptionsDisplayText: () => {
return `Matches all rows where field is not null.`;
},
isApplicable: () => true,
getDefaultOptions: () => ({}),
};
export const getNullValueMatchers = (): ValueMatcherInfo[] => [isNullValueMatcher, isNotNullValueMatcher];
@@ -0,0 +1,180 @@
import { toDataFrame } from '../../../dataframe';
import { DataFrame } from '../../../types/dataFrame';
import { getValueMatcher } from '../../matchers';
import { ValueMatcherID } from '../ids';
describe('value greater than matcher', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{
name: 'temp',
values: [23, 11, 10],
},
],
}),
];
const matcher = getValueMatcher({
id: ValueMatcherID.greater,
options: {
value: 11,
},
});
it('should match values greater than 11', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 0;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('should not match values equlas to 11', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 1;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
it('should not match values lower than 11', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 2;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
});
describe('value greater than or equal matcher', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{
name: 'temp',
values: [23, 11, 10],
},
],
}),
];
const matcher = getValueMatcher({
id: ValueMatcherID.greaterOrEqual,
options: {
value: 11,
},
});
it('should match values greater than 11', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 0;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('should match values equlas to 11', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 1;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('should not match values lower than 11', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 2;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
});
describe('value lower than matcher', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{
name: 'temp',
values: [23, 11, 10],
},
],
}),
];
const matcher = getValueMatcher({
id: ValueMatcherID.lower,
options: {
value: 11,
},
});
it('should match values lower than 11', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 2;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('should not match values equal to 11', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 1;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
it('should not match values greater than 11', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 0;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
});
describe('value lower than or equal matcher', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{
name: 'temp',
values: [23, 11, 10],
},
],
}),
];
const matcher = getValueMatcher({
id: ValueMatcherID.lowerOrEqual,
options: {
value: 11,
},
});
it('should match values lower than 11', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 2;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('should match values equal to 11', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 1;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('should not match values greater than 11', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 0;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
});
@@ -0,0 +1,91 @@
import { Field, FieldType } from '../../../types/dataFrame';
import { ValueMatcherInfo } from '../../../types/transformations';
import { ValueMatcherID } from '../ids';
import { BasicValueMatcherOptions } from './types';
const isGreaterValueMatcher: ValueMatcherInfo<BasicValueMatcherOptions<number>> = {
id: ValueMatcherID.greater,
name: 'Is greater',
description: 'Match when field value is greater than option.',
get: options => {
return (valueIndex: number, field: Field) => {
const value = field.values.get(valueIndex);
if (isNaN(value)) {
return false;
}
return value > options.value;
};
},
getOptionsDisplayText: options => {
return `Matches all rows where field value is greater than: ${options.value}.`;
},
isApplicable: field => field.type === FieldType.number,
getDefaultOptions: () => ({ value: 0 }),
};
const isGreaterOrEqualValueMatcher: ValueMatcherInfo<BasicValueMatcherOptions<number>> = {
id: ValueMatcherID.greaterOrEqual,
name: 'Is greater or equal',
description: 'Match when field value is lower or greater than option.',
get: options => {
return (valueIndex: number, field: Field) => {
const value = field.values.get(valueIndex);
if (isNaN(value)) {
return false;
}
return value >= options.value;
};
},
getOptionsDisplayText: options => {
return `Matches all rows where field value is lower or greater than: ${options.value}.`;
},
isApplicable: field => field.type === FieldType.number,
getDefaultOptions: () => ({ value: 0 }),
};
const isLowerValueMatcher: ValueMatcherInfo<BasicValueMatcherOptions<number>> = {
id: ValueMatcherID.lower,
name: 'Is lower',
description: 'Match when field value is lower than option.',
get: options => {
return (valueIndex: number, field: Field) => {
const value = field.values.get(valueIndex);
if (isNaN(value)) {
return false;
}
return value < options.value;
};
},
getOptionsDisplayText: options => {
return `Matches all rows where field value is lower than: ${options.value}.`;
},
isApplicable: field => field.type === FieldType.number,
getDefaultOptions: () => ({ value: 0 }),
};
const isLowerOrEqualValueMatcher: ValueMatcherInfo<BasicValueMatcherOptions<number>> = {
id: ValueMatcherID.lowerOrEqual,
name: 'Is lower or equal',
description: 'Match when field value is lower or equal than option.',
get: options => {
return (valueIndex: number, field: Field) => {
const value = field.values.get(valueIndex);
if (isNaN(value)) {
return false;
}
return value <= options.value;
};
},
getOptionsDisplayText: options => {
return `Matches all rows where field value is lower or equal than: ${options.value}.`;
},
isApplicable: field => field.type === FieldType.number,
getDefaultOptions: () => ({ value: 0 }),
};
export const getNumericValueMatchers = (): ValueMatcherInfo[] => [
isGreaterValueMatcher,
isGreaterOrEqualValueMatcher,
isLowerValueMatcher,
isLowerOrEqualValueMatcher,
];
@@ -0,0 +1,49 @@
import { toDataFrame } from '../../../dataframe';
import { DataFrame } from '../../../types/dataFrame';
import { getValueMatcher } from '../../matchers';
import { ValueMatcherID } from '../ids';
describe('value between matcher', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{
name: 'temp',
values: [23, 11, 10, 25],
},
],
}),
];
const matcher = getValueMatcher({
id: ValueMatcherID.between,
options: {
from: 10,
to: 25,
},
});
it('should match values greater than 10 but lower than 25', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 0;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('should not match values greater than 25', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 4;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
it('should not match values lower than 11', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 2;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
});
@@ -0,0 +1,26 @@
import { Field, FieldType } from '../../../types/dataFrame';
import { ValueMatcherInfo } from '../../../types/transformations';
import { ValueMatcherID } from '../ids';
import { RangeValueMatcherOptions } from './types';
const isBetweenValueMatcher: ValueMatcherInfo<RangeValueMatcherOptions<number>> = {
id: ValueMatcherID.between,
name: 'Is between',
description: 'Match when field value is between given option values.',
get: options => {
return (valueIndex: number, field: Field) => {
const value = field.values.get(valueIndex);
if (isNaN(value)) {
return false;
}
return value > options.from && value < options.to;
};
},
getOptionsDisplayText: options => {
return `Matches all rows where field value is between ${options.from} and ${options.to}.`;
},
isApplicable: field => field.type === FieldType.number,
getDefaultOptions: () => ({ from: 0, to: 100 }),
};
export const getRangeValueMatchers = (): ValueMatcherInfo[] => [isBetweenValueMatcher];
@@ -0,0 +1,85 @@
import { toDataFrame } from '../../../dataframe';
import { DataFrame } from '../../../types/dataFrame';
import { getValueMatcher } from '../../matchers';
import { ValueMatcherID } from '../ids';
describe('regex value matcher', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{
name: 'temp',
values: ['.', 'asdf', 100, '25.5'],
},
],
}),
];
describe('option with value .*', () => {
const matcher = getValueMatcher({
id: ValueMatcherID.regex,
options: {
value: '.*',
},
});
it('should match all values', () => {
const frame = data[0];
const field = frame.fields[0];
for (let i = 0; i < field.values.length; i++) {
expect(matcher(i, field, frame, data)).toBeTruthy();
}
});
});
describe('option with value \\w+', () => {
const matcher = getValueMatcher({
id: ValueMatcherID.regex,
options: {
value: '\\w+',
},
});
it('should match wordy values', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 1;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('should not match non-wordy values', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 0;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
});
describe('option with value \\d+', () => {
const matcher = getValueMatcher({
id: ValueMatcherID.regex,
options: {
value: '\\d+',
},
});
it('should match numeric values', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 2;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('should not match non-numeric values', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 1;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
});
});
@@ -0,0 +1,25 @@
import { Field } from '../../../types/dataFrame';
import { ValueMatcherInfo } from '../../../types/transformations';
import { ValueMatcherID } from '../ids';
import { BasicValueMatcherOptions } from './types';
const regexValueMatcher: ValueMatcherInfo<BasicValueMatcherOptions<string>> = {
id: ValueMatcherID.regex,
name: 'Regex',
description: 'Match when field value is matching regex.',
get: options => {
const regex = new RegExp(options.value);
return (valueIndex: number, field: Field) => {
const value = field.values.get(valueIndex);
return regex.test(value);
};
},
getOptionsDisplayText: options => {
return `Matches all rows where field value is matching regex: ${options.value}`;
},
isApplicable: () => true,
getDefaultOptions: () => ({ value: '.*' }),
};
export const getRegexValueMatcher = (): ValueMatcherInfo[] => [regexValueMatcher];
@@ -0,0 +1,23 @@
/**
* Describes a empty value matcher option.
* @public
*/
export interface ValueMatcherOptions {}
/**
* Describes a basic value matcher option that has a single value.
* @public
*/
export interface BasicValueMatcherOptions<T = any> extends ValueMatcherOptions {
value: T;
}
/**
* Describes a range value matcher option that has a to and a from value to
* be able to match a range.
* @public
*/
export interface RangeValueMatcherOptions<T = any> extends ValueMatcherOptions {
from: T;
to: T;
}
@@ -14,6 +14,7 @@ import { labelsToFieldsTransformer } from './transformers/labelsToFields';
import { ensureColumnsTransformer } from './transformers/ensureColumns';
import { groupByTransformer } from './transformers/groupBy';
import { mergeTransformer } from './transformers/merge';
import { filterByValueTransformer } from './transformers/filterByValue';
export const standardTransformers = {
noopTransformer,
@@ -21,6 +22,7 @@ export const standardTransformers = {
filterFieldsByNameTransformer,
filterFramesTransformer,
filterFramesByRefIdTransformer,
filterByValueTransformer,
orderFieldsTransformer,
organizeFieldsTransformer,
reduceTransformer,
@@ -0,0 +1,219 @@
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
import { DataTransformerConfig, FieldType, MatcherConfig } from '../../types';
import { ArrayVector } from '../../vector';
import { transformDataFrame } from '../transformDataFrame';
import { toDataFrame } from '../../dataframe/processDataFrame';
import {
FilterByValueMatch,
filterByValueTransformer,
FilterByValueTransformerOptions,
FilterByValueType,
} from './filterByValue';
import { DataTransformerID } from './ids';
import { ValueMatcherID } from '../matchers/ids';
import { BasicValueMatcherOptions } from '../matchers/valueMatchers/types';
const seriesAWithSingleField = toDataFrame({
name: 'A',
length: 7,
fields: [
{ name: 'time', type: FieldType.time, values: new ArrayVector([1000, 2000, 3000, 4000, 5000, 6000, 7000]) },
{ name: 'numbers', type: FieldType.number, values: new ArrayVector([1, 2, 3, 4, 5, 6, 7]) },
],
});
describe('FilterByValue transformer', () => {
beforeAll(() => {
mockTransformationsRegistry([filterByValueTransformer]);
});
it('should exclude values', async () => {
const lower: MatcherConfig<BasicValueMatcherOptions<number>> = {
id: ValueMatcherID.lower,
options: { value: 6 },
};
const cfg: DataTransformerConfig<FilterByValueTransformerOptions> = {
id: DataTransformerID.filterByValue,
options: {
type: FilterByValueType.exclude,
match: FilterByValueMatch.all,
filters: [
{
fieldName: 'numbers',
config: lower,
},
],
},
};
await expect(transformDataFrame([cfg], [seriesAWithSingleField])).toEmitValuesWith(received => {
const processed = received[0];
expect(processed.length).toEqual(1);
expect(processed[0].fields).toEqual([
{
name: 'time',
type: FieldType.time,
values: new ArrayVector([6000, 7000]),
state: { displayName: 'time' },
config: {},
},
{
name: 'numbers',
type: FieldType.number,
values: new ArrayVector([6, 7]),
state: { displayName: 'numbers' },
config: {},
},
]);
});
});
it('should include values', async () => {
const lowerOrEqual: MatcherConfig<BasicValueMatcherOptions<number>> = {
id: ValueMatcherID.lowerOrEqual,
options: { value: 5 },
};
const cfg: DataTransformerConfig<FilterByValueTransformerOptions> = {
id: DataTransformerID.filterByValue,
options: {
type: FilterByValueType.include,
match: FilterByValueMatch.all,
filters: [
{
fieldName: 'numbers',
config: lowerOrEqual,
},
],
},
};
await expect(transformDataFrame([cfg], [seriesAWithSingleField])).toEmitValuesWith(received => {
const processed = received[0];
expect(processed.length).toEqual(1);
expect(processed[0].fields).toEqual([
{
name: 'time',
type: FieldType.time,
values: new ArrayVector([1000, 2000, 3000, 4000, 5000]),
state: { displayName: 'time' },
config: {},
},
{
name: 'numbers',
type: FieldType.number,
values: new ArrayVector([1, 2, 3, 4, 5]),
state: { displayName: 'numbers' },
config: {},
},
]);
});
});
it('should match any condition', async () => {
const lowerOrEqual: MatcherConfig<BasicValueMatcherOptions<number>> = {
id: ValueMatcherID.lowerOrEqual,
options: { value: 4 },
};
const equal: MatcherConfig<BasicValueMatcherOptions<number>> = {
id: ValueMatcherID.equal,
options: { value: 7 },
};
const cfg: DataTransformerConfig<FilterByValueTransformerOptions> = {
id: DataTransformerID.filterByValue,
options: {
type: FilterByValueType.include,
match: FilterByValueMatch.any,
filters: [
{
fieldName: 'numbers',
config: lowerOrEqual,
},
{
fieldName: 'numbers',
config: equal,
},
],
},
};
await expect(transformDataFrame([cfg], [seriesAWithSingleField])).toEmitValuesWith(received => {
const processed = received[0];
expect(processed.length).toEqual(1);
expect(processed[0].fields).toEqual([
{
name: 'time',
type: FieldType.time,
values: new ArrayVector([1000, 2000, 3000, 4000, 7000]),
state: { displayName: 'time' },
config: {},
},
{
name: 'numbers',
type: FieldType.number,
values: new ArrayVector([1, 2, 3, 4, 7]),
state: { displayName: 'numbers' },
config: {},
},
]);
});
});
it('should match all condition', async () => {
const greaterOrEqual: MatcherConfig<BasicValueMatcherOptions<number>> = {
id: ValueMatcherID.greaterOrEqual,
options: { value: 4 },
};
const lowerOrEqual: MatcherConfig<BasicValueMatcherOptions<number>> = {
id: ValueMatcherID.lowerOrEqual,
options: { value: 5 },
};
const cfg: DataTransformerConfig<FilterByValueTransformerOptions> = {
id: DataTransformerID.filterByValue,
options: {
type: FilterByValueType.include,
match: FilterByValueMatch.all,
filters: [
{
fieldName: 'numbers',
config: lowerOrEqual,
},
{
fieldName: 'numbers',
config: greaterOrEqual,
},
],
},
};
await expect(transformDataFrame([cfg], [seriesAWithSingleField])).toEmitValuesWith(received => {
const processed = received[0];
expect(processed.length).toEqual(1);
expect(processed[0].fields).toEqual([
{
name: 'time',
type: FieldType.time,
values: new ArrayVector([4000, 5000]),
state: { displayName: 'time' },
config: {},
},
{
name: 'numbers',
type: FieldType.number,
values: new ArrayVector([4, 5]),
state: { displayName: 'numbers' },
config: {},
},
]);
});
});
});
@@ -0,0 +1,159 @@
import { map } from 'rxjs/operators';
import { noopTransformer } from './noop';
import { DataTransformerID } from './ids';
import { DataTransformerInfo, MatcherConfig } from '../../types/transformations';
import { DataFrame, Field } from '../../types/dataFrame';
import { getFieldDisplayName } from '../../field/fieldState';
import { getValueMatcher } from '../matchers';
import { ArrayVector } from '../../vector/ArrayVector';
export enum FilterByValueType {
exclude = 'exclude',
include = 'include',
}
export enum FilterByValueMatch {
all = 'all',
any = 'any',
}
export interface FilterByValueFilter {
fieldName: string;
config: MatcherConfig;
}
export interface FilterByValueTransformerOptions {
filters: FilterByValueFilter[];
type: FilterByValueType;
match: FilterByValueMatch;
}
export const filterByValueTransformer: DataTransformerInfo<FilterByValueTransformerOptions> = {
id: DataTransformerID.filterByValue,
name: 'Filter data by values',
description: 'select a subset of results based on values',
defaultOptions: {
filters: [],
type: FilterByValueType.include,
match: FilterByValueMatch.any,
},
operator: options => source => {
const filters = options.filters;
const matchAll = options.match === FilterByValueMatch.all;
const include = options.type === FilterByValueType.include;
if (!Array.isArray(filters) || filters.length === 0) {
return source.pipe(noopTransformer.operator({}));
}
return source.pipe(
map(data => {
if (!Array.isArray(data) || data.length === 0) {
return data;
}
const rows = new Set<number>();
for (const frame of data) {
const fieldIndexByName = groupFieldIndexByName(frame, data);
const matchers = createFilterValueMatchers(filters, fieldIndexByName);
for (let index = 0; index < frame.length; index++) {
if (rows.has(index)) {
continue;
}
let matching = true;
for (const matcher of matchers) {
const match = matcher(index, frame, data);
if (!matchAll && match) {
matching = true;
break;
}
if (matchAll && !match) {
matching = false;
break;
}
matching = match;
}
if (matching) {
rows.add(index);
}
}
}
const processed: DataFrame[] = [];
const frameLength = include ? rows.size : data[0].length - rows.size;
for (const frame of data) {
const fields: Field[] = [];
for (const field of frame.fields) {
const buffer = [];
for (let index = 0; index < frame.length; index++) {
if (include && rows.has(index)) {
buffer.push(field.values.get(index));
continue;
}
if (!include && !rows.has(index)) {
buffer.push(field.values.get(index));
continue;
}
}
// TODO: what parts needs to be excluded from field.
fields.push({
...field,
values: new ArrayVector(buffer),
config: {},
});
}
processed.push({
...frame,
fields: fields,
length: frameLength,
});
}
return processed;
})
);
},
};
const createFilterValueMatchers = (
filters: FilterByValueFilter[],
fieldIndexByName: Record<string, number>
): Array<(index: number, frame: DataFrame, data: DataFrame[]) => boolean> => {
const noop = () => false;
return filters.map(filter => {
const fieldIndex = fieldIndexByName[filter.fieldName] ?? -1;
if (fieldIndex < 0) {
console.warn(`[FilterByValue] Could not find index for field name: ${filter.fieldName}`);
return noop;
}
const matcher = getValueMatcher(filter.config);
return (index, frame, data) => matcher(index, frame.fields[fieldIndex], frame, data);
});
};
const groupFieldIndexByName = (frame: DataFrame, data: DataFrame[]): Record<string, number> => {
return frame.fields.reduce((all: Record<string, number>, field, fieldIndex) => {
const fieldName = getFieldDisplayName(field, frame, data);
all[fieldName] = fieldIndex;
return all;
}, {});
};
@@ -16,6 +16,7 @@ export enum DataTransformerID {
filterFieldsByName = 'filterFieldsByName',
filterFrames = 'filterFrames',
filterByRefId = 'filterByRefId',
filterByValue = 'filterByValue',
noop = 'noop',
ensureColumns = 'ensureColumns',
groupBy = 'groupBy',
@@ -6,64 +6,25 @@ import {
SelectFieldConfigSettings,
StringFieldConfigSettings,
} from '../field';
import { OptionEditorConfig } from './options';
/**
* Option editor registry item
*/
export interface OptionsEditorItem<TOptions, TSettings, TEditorProps, TValue> extends RegistryItem {
/**
* Path of the options property to control.
*
* @example
* Given options object of a type:
* ```ts
* interface CustomOptions {
* a: {
* b: string;
* }
* }
* ```
*
* path can be either 'a' or 'a.b'.
*/
path: (keyof TOptions & string) | string;
export interface OptionsEditorItem<TOptions, TSettings, TEditorProps, TValue>
extends RegistryItem,
OptionEditorConfig<TOptions, TSettings, TValue> {
/**
* React component used to edit the options property
*/
editor: ComponentType<TEditorProps>;
/**
* Custom settings of the editor.
*/
settings?: TSettings;
/**
* Array of strings representing category of the options property.
*/
category?: string[];
defaultValue?: TValue;
/**
* Function that enables configuration of when option editor should be shown based on current options properties.
*
* @param currentConfig Current options values
*/
showIf?: (currentConfig: TOptions) => boolean | undefined;
/**
* Function that returns number of items if given option represents a collection, i.e. array of items.
/*
* @param value
*/
getItemsCount?: (value?: TValue) => number;
}
/**
* Configuration of option editor registry item
*/
interface OptionEditorConfig<TOptions, TSettings, TValue = any> {
id: keyof TOptions & string;
name: string;
description?: string;
settings?: TSettings;
defaultValue?: TValue;
}
/**
* Describes an API for option editors UI builder
*/
@@ -37,6 +37,7 @@ export interface FeatureToggles {
ngalert: boolean;
// Just for demo at the moment
traceToLogs: boolean;
panelLibrary: boolean;
/**
* @remarks
@@ -126,6 +126,13 @@ export interface FieldState {
*/
calcs?: FieldCalcs;
/**
* The numeric range for values in this field. This value will respect the min/max
* set in field config, or when set to `auto` this will have the min/max for all data
* in the response
*/
range?: NumericRange;
/**
* Appropriate values for templating
*/
@@ -138,6 +145,12 @@ export interface FieldState {
seriesIndex?: number;
}
export interface NumericRange {
min?: number | null;
max?: number | null;
delta: number;
}
export interface DataFrame extends QueryResultBase {
name?: string;
fields: Field[]; // All fields of equal length
+8 -4
View File
@@ -36,10 +36,14 @@ export interface DataLink<T extends DataQuery = any> {
// datas ource that we want to show to the user. Usually this results in a link to explore but can also lead to
// more custom onClick behaviour if needed.
// @internal and subject to change in future releases
internal?: {
query: T;
datasourceUid: string;
};
internal?: InternalDataLink<T>;
}
/** @internal */
export interface InternalDataLink<T extends DataQuery = any> {
query: T;
datasourceUid: string;
datasourceName: string;
}
export type LinkTarget = '_blank' | '_self' | undefined;
@@ -567,6 +567,7 @@ export interface DataSourceInstanceSettings<T extends DataSourceJsonData = DataS
username?: string;
password?: string; // when access is direct, for some legacy datasources
database?: string;
isDefault?: boolean;
/**
* This is the full Authorization header if basic auth is enabled.
@@ -582,7 +583,6 @@ export interface DataSourceSelectItem {
name: string;
value: string | null;
meta: DataSourcePluginMeta;
sort: string;
}
/**
@@ -18,6 +18,17 @@ export interface DisplayValue extends FormattedValue {
title?: string;
}
/**
* Explicit control for text settings
*/
export interface TextDisplayOptions {
/* Explicit text size */
titleSize?: number;
/* Explicit text size */
valueSize?: number;
}
/**
* These represents the display value with the longest title and text.
* Used to align widths and heights when displaying multiple DisplayValues
@@ -1,16 +1,9 @@
import { ComponentType } from 'react';
import {
MatcherConfig,
FieldConfig,
Field,
DataFrame,
GrafanaTheme,
TimeZone,
DataSourceInstanceSettings,
} from '../types';
import { MatcherConfig, FieldConfig, Field, DataFrame, GrafanaTheme, TimeZone } from '../types';
import { InterpolateFunction } from './panel';
import { StandardEditorProps, FieldConfigOptionsRegistry, StandardEditorContext } from '../field';
import { OptionsEditorItem } from './OptionsUIRegistryBuilder';
import { OptionEditorConfig } from './options';
export interface DynamicConfigValue {
id: string;
@@ -48,51 +41,13 @@ export interface FieldOverrideEditorProps<TValue, TSettings> extends Omit<Standa
context: FieldOverrideContext;
}
export interface FieldConfigEditorConfig<TOptions, TSettings = any, TValue = any> {
/**
* Path of the field config property to control.
*
* @example
* Given field config object of a type:
* ```ts
* interface CustomFieldConfig {
* a: {
* b: string;
* }
* }
* ```
*
* path can be either 'a' or 'a.b'.
*/
path: (keyof TOptions & string) | string;
/**
* Name of the field config property. Will be displayed in the UI as form element label.
*/
name: string;
/**
* Description of the field config property. Will be displayed in the UI as form element description.
*/
description?: string;
/**
* Array of strings representing category of the field config property. First element in the array will make option render as collapsible section.
*/
category?: string[];
/**
* Custom settings of the editor.
*/
settings?: TSettings;
export interface FieldConfigEditorConfig<TOptions, TSettings = any, TValue = any>
extends OptionEditorConfig<TOptions, TSettings, TValue> {
/**
* Function that allows specifying whether or not this field config should apply to a given field.
* @param field
*/
shouldApply?: (field: Field) => boolean;
defaultValue?: TValue;
/**
* Function that enables configuration of when field config property editor should be shown based on current panel field config.
*
* @param currentConfig Current field config values
*/
showIf?: (currentConfig: TOptions) => boolean;
}
export interface FieldConfigPropertyItem<TOptions = any, TValue = any, TSettings extends {} = any>
@@ -114,10 +69,8 @@ export interface ApplyFieldOverrideOptions {
data?: DataFrame[];
fieldConfig: FieldConfigSource;
replaceVariables: InterpolateFunction;
getDataSourceSettingsByUid: (uid: string) => DataSourceInstanceSettings | undefined;
theme: GrafanaTheme;
timeZone?: TimeZone;
autoMinMax?: boolean;
fieldConfigRegistry?: FieldConfigOptionsRegistry;
}
@@ -0,0 +1,53 @@
/**
* Base class for editor builders
*
* @beta
*/
export interface OptionEditorConfig<TOptions, TSettings = any, TValue = any> {
/**
* Path of the option property to control.
*
* @example
* Given options object of a type:
* ```ts
* interface Options {
* a: {
* b: string;
* }
* }
* ```
*
* path can be either 'a' or 'a.b'.
*/
path: (keyof TOptions & string) | string;
/**
* Name of the option. Will be displayed in the UI as form element label.
*/
name: string;
/**
* Description of the option. Will be displayed in the UI as form element description.
*/
description?: string;
/**
* Custom settings of the editor.
*/
settings?: TSettings;
/**
* Array of strings representing category of the option. First element in the array will make option render as collapsible section.
*/
category?: string[];
/**
* Set this value if undefined
*/
defaultValue?: TValue;
/**
* Function that enables configuration of when option editor should be shown based on current panel option properties.
*/
showIf?: (currentOptions: TOptions) => boolean | undefined;
}
+3 -41
View File
@@ -9,6 +9,7 @@ import { FieldConfigSource } from './fieldOverrides';
import { Registry } from '../utils';
import { StandardEditorProps } from '../field';
import { OptionsEditorItem } from './OptionsUIRegistryBuilder';
import { OptionEditorConfig } from './options';
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
@@ -140,47 +141,8 @@ export interface PanelOptionsEditorProps<TValue> extends StandardEditorProps<TVa
export interface PanelOptionsEditorItem<TOptions = any, TValue = any, TSettings = any>
extends OptionsEditorItem<TOptions, TSettings, PanelOptionsEditorProps<TValue>, TValue> {}
export interface PanelOptionsEditorConfig<TOptions, TSettings = any, TValue = any> {
/**
* Path of the option property to control.
*
* @example
* Given options object of a type:
* ```ts
* interface Options {
* a: {
* b: string;
* }
* }
* ```
*
* path can be either 'a' or 'a.b'.
*/
path: (keyof TOptions & string) | string;
/**
* Name of the option. Will be displayed in the UI as form element label.
*/
name: string;
/**
* Description of the option. Will be displayed in the UI as form element description.
*/
description?: string;
/**al
* Custom settings of the editor.
*/
settings?: TSettings;
/**
* Array of strings representing category of the option. First element in the array will make option render as collapsible section.
*/
category?: string[];
defaultValue?: TValue;
/**
* Function that enables configuration of when option editor should be shown based on current panel option properties.
*
* @param currentConfig Current panel options
*/
showIf?: (currentConfig: TOptions) => boolean | undefined;
}
export interface PanelOptionsEditorConfig<TOptions, TSettings = any, TValue = any>
extends OptionEditorConfig<TOptions, TSettings, TValue> {}
/**
* @internal
@@ -25,8 +25,14 @@ export interface DataTransformerConfig<TOptions = any> {
options: TOptions;
}
export type FieldMatcher = (field: Field, frame: DataFrame, allFrames: DataFrame[]) => boolean;
export type FrameMatcher = (frame: DataFrame) => boolean;
export type FieldMatcher = (field: Field, frame: DataFrame, allFrames: DataFrame[]) => boolean;
/**
* Value matcher type to describe the matcher function
* @public
*/
export type ValueMatcher = (valueIndex: number, field: Field, frame: DataFrame, allFrames: DataFrame[]) => boolean;
export interface FieldMatcherInfo<TOptions = any> extends RegistryItemWithOptions<TOptions> {
get: (options: TOptions) => FieldMatcher;
@@ -36,6 +42,16 @@ export interface FrameMatcherInfo<TOptions = any> extends RegistryItemWithOption
get: (options: TOptions) => FrameMatcher;
}
/**
* Registry item to represent all the different valu matchers supported
* in the Grafana platform.
* @public
*/
export interface ValueMatcherInfo<TOptions = any> extends RegistryItemWithOptions<TOptions> {
get: (options: TOptions) => ValueMatcher;
isApplicable: (field: Field) => boolean;
getDefaultOptions: (field: Field) => TOptions;
}
export interface MatcherConfig<TOptions = any> {
id: string;
options?: TOptions;
@@ -4,33 +4,34 @@ import { ArrayVector } from '../vector';
describe('mapInternalLinkToExplore', () => {
it('creates internal link', () => {
const link = mapInternalLinkToExplore(
{
url: '',
title: '',
internal: {
datasourceUid: 'uid',
query: { query: '12344' },
},
const dataLink = {
url: '',
title: '',
internal: {
datasourceUid: 'uid',
datasourceName: 'dsName',
query: { query: '12344' },
},
{},
{} as any,
{
};
const link = mapInternalLinkToExplore({
link: dataLink,
internalLink: dataLink.internal,
scopedVars: {},
range: {} as any,
field: {
name: 'test',
type: FieldType.number,
config: {},
values: new ArrayVector([2]),
},
{
replaceVariables: val => val,
getDataSourceSettingsByUid: uid => ({ name: 'testDS' } as any),
}
);
replaceVariables: val => val,
});
expect(link).toEqual(
expect.objectContaining({
title: 'testDS',
href: '/explore?left={"datasource":"testDS","queries":[{"query":"12344"}]}',
title: 'dsName',
href: '/explore?left={"datasource":"dsName","queries":[{"query":"12344"}]}',
onClick: undefined,
})
);
+15 -25
View File
@@ -1,8 +1,8 @@
import {
DataLink,
DataQuery,
DataSourceInstanceSettings,
Field,
InternalDataLink,
InterpolateFunction,
LinkModel,
ScopedVars,
@@ -27,41 +27,31 @@ export const DataLinkBuiltInVars = {
};
// We inject these because we cannot import them directly as they reside inside grafana main package.
type Options = {
export type LinkToExploreOptions = {
link: DataLink;
scopedVars: ScopedVars;
range: TimeRange;
field: Field;
internalLink: InternalDataLink;
onClickFn?: (options: { datasourceUid: string; query: any; range?: TimeRange }) => void;
replaceVariables: InterpolateFunction;
getDataSourceSettingsByUid: (uid: string) => DataSourceInstanceSettings | undefined;
};
export function mapInternalLinkToExplore(
link: DataLink,
scopedVars: ScopedVars,
range: TimeRange,
field: Field,
options: Options
): LinkModel<Field> {
if (!link.internal) {
throw new Error('Trying to map external link as internal');
}
const { onClickFn, replaceVariables, getDataSourceSettingsByUid } = options;
export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkModel<Field> {
const { onClickFn, replaceVariables, link, scopedVars, range, field, internalLink } = options;
const interpolatedQuery = interpolateQuery(link, scopedVars, replaceVariables);
return {
title: link.title
? replaceVariables(link.title || '', scopedVars)
: getDataSourceSettingsByUid(link.internal.datasourceUid)?.name || 'Unknown datasource',
const title = link.title ? link.title : internalLink.datasourceName;
return {
title: replaceVariables(title),
// In this case this is meant to be internal link (opens split view by default) the href will also points
// to explore but this way you can open it in new tab.
href: generateInternalHref(
getDataSourceSettingsByUid(link.internal.datasourceUid)?.name || 'unknown',
interpolatedQuery,
range
),
href: generateInternalHref(internalLink.datasourceName, interpolatedQuery, range),
onClick: onClickFn
? () => {
onClickFn?.({
datasourceUid: link.internal!.datasourceUid,
onClickFn({
datasourceUid: internalLink.datasourceUid,
query: interpolatedQuery,
range,
});
@@ -1,7 +1,7 @@
import { Vector } from '../types/vector';
export function vectorToArray<T>(v: Vector<T>): T[] {
const arr: T[] = [];
const arr: T[] = Array(v.length);
for (let i = 0; i < v.length; i++) {
arr[i] = v.get(i);
}
+2 -2
View File
@@ -36,14 +36,14 @@
"rollup-plugin-typescript2": "0.26.0",
"rollup-plugin-visualizer": "3.3.1",
"ts-loader": "6.2.1",
"ts-node": "8.8.1"
"ts-node": "9.0.0"
},
"types": "src/index.ts",
"dependencies": {
"@grafana/tsconfig": "^1.0.0-rc1",
"commander": "5.0.0",
"execa": "4.0.0",
"typescript": "4.0.2",
"typescript": "4.1.2",
"yaml": "^1.8.3"
}
}
@@ -1,5 +1,3 @@
import { Pages } from './pages';
export const Components = {
DataSource: {
TestData: {
@@ -37,6 +35,9 @@ export const Components = {
BarGauge: {
value: 'Bar gauge value',
},
Text: {
container: () => '.markdown-html',
},
},
},
Drawer: {
@@ -54,8 +55,8 @@ export const Components = {
},
OptionsPane: {
content: 'Panel editor option pane content',
close: Pages.Dashboard.Toolbar.toolbarItems('Close options pane'),
open: Pages.Dashboard.Toolbar.toolbarItems('Open options pane'),
close: 'Dashboard navigation bar button Close options pane',
open: 'Dashboard navigation bar button Open options pane',
select: 'Panel editor option pane select',
tab: (title: string) => `Panel editor option pane tab ${title}`,
},
@@ -109,6 +110,7 @@ export const Components = {
transformationEditorDebugger: (name: string) => `Transformation editor debugger ${name}`,
},
Transforms: {
card: (name: string) => `New transform ${name}`,
Reduce: {
modeLabel: 'Transform mode label',
calculationsLabel: 'Transform calculations label',
@@ -1,3 +1,5 @@
import { Components } from './components';
export const Pages = {
Login: {
url: '/login',
@@ -14,7 +16,6 @@ export const Pages = {
delete: 'Data source settings page Delete button',
saveAndTest: 'Data source settings page Save and Test button',
alert: 'Data source settings page Alert',
alertMessage: 'Data source settings page Alert message',
},
DataSources: {
url: '/datasources',
@@ -88,7 +89,7 @@ export const Pages = {
submitButton: 'Variable editor Submit button',
},
QueryVariable: {
queryOptionsDataSourceSelect: 'Variable editor Form Query DataSource select',
queryOptionsDataSourceSelect: Components.DataSourcePicker.container,
queryOptionsRefreshSelect: 'Variable editor Form Query Refresh select',
queryOptionsRegExInput: 'Variable editor Form Query RegEx field',
queryOptionsSortSelect: 'Variable editor Form Query Sort select',
@@ -100,6 +101,9 @@ export const Pages = {
ConstantVariable: {
constantOptionsQueryInput: 'Variable editor Form Constant Query field',
},
TextBoxVariable: {
textBoxOptionsQueryInput: 'Variable editor Form TextBox Query field',
},
},
},
},
+1 -1
View File
@@ -54,7 +54,7 @@
"execa": "4.0.0",
"resolve-as-bin": "2.1.0",
"ts-loader": "6.2.1",
"typescript": "4.0.2",
"typescript": "4.1.2",
"yaml": "^1.8.3"
}
}
@@ -85,8 +85,10 @@ export const addDataSource = (config?: Partial<AddDataSourceConfig>) => {
form();
e2e.pages.DataSource.saveAndTest().click();
e2e.pages.DataSource.alert().should('exist');
e2e.pages.DataSource.alertMessage().contains(expectedAlertMessage); // assertion
e2e.pages.DataSource.alert()
.should('exist')
.contains(expectedAlertMessage); // assertion
e2e().logToConsole('Added data source with name:', name);
return e2e()
+2 -2
View File
@@ -31,7 +31,7 @@
"@grafana/tsconfig": "^1.0.0-rc1",
"@rollup/plugin-commonjs": "11.0.2",
"@rollup/plugin-node-resolve": "7.1.1",
"@types/jest": "26.0.0",
"@types/jest": "26.0.15",
"@types/rollup-plugin-visualizer": "2.6.0",
"@types/systemjs": "^0.20.6",
"lodash": "4.17.19",
@@ -41,7 +41,7 @@
"rollup-plugin-terser": "5.3.0",
"rollup-plugin-typescript2": "0.26.0",
"rollup-plugin-visualizer": "3.3.1",
"typescript": "4.0.2"
"typescript": "4.1.2"
},
"types": "src/index.ts"
}
+1
View File
@@ -57,6 +57,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
meta: false,
ngalert: false,
traceToLogs: false,
panelLibrary: false,
};
licenseInfo: LicenseInfo = {} as LicenseInfo;
rendererAvailable = false;
@@ -16,19 +16,25 @@ export interface DataSourceSrv {
get(name?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi>;
/**
* Returns metadata based on UID.
* Get a list of data sources
*/
getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined;
getList(filters?: GetDataSourceListFilters): DataSourceInstanceSettings[];
/**
* Get all data sources
* Get settings and plugin metadata by name or uid
*/
getAll(): DataSourceInstanceSettings[];
getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined;
}
/**
* Get all data sources except for internal ones that usually should not be listed like mixed data source.
*/
getExternal(): DataSourceInstanceSettings[];
/** @public */
export interface GetDataSourceListFilters {
mixed?: boolean;
metrics?: boolean;
tracing?: boolean;
annotations?: boolean;
dashboard?: boolean;
variables?: boolean;
pluginId?: string;
}
let singletonInstance: DataSourceSrv;

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