Merge branch 'main' into ifrost/graphite-on-react-4

This commit is contained in:
Piotr Jamróz
2021-08-11 09:46:22 +02:00
105 changed files with 1749 additions and 2724 deletions
+34 -1
View File
@@ -235,6 +235,9 @@ services:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_USER: grafana
node:
type: no-parallel
trigger:
event:
- pull_request
@@ -563,6 +566,9 @@ services:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_USER: grafana
node:
type: no-parallel
trigger:
branch:
- main
@@ -663,6 +669,9 @@ steps:
depends_on:
- initialize
node:
type: no-parallel
trigger:
branch:
- main
@@ -1005,6 +1014,9 @@ services:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_USER: grafana
node:
type: no-parallel
trigger:
ref:
- refs/tags/v*
@@ -1477,6 +1489,9 @@ services:
image_pull_secrets:
- dockerconfigjson
node:
type: no-parallel
trigger:
ref:
- refs/tags/v*
@@ -1617,6 +1632,9 @@ steps:
depends_on:
- initialize
node:
type: no-parallel
trigger:
ref:
- refs/tags/v*
@@ -1948,6 +1966,9 @@ services:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_USER: grafana
node:
type: no-parallel
trigger:
event:
- custom
@@ -2414,6 +2435,9 @@ services:
image_pull_secrets:
- dockerconfigjson
node:
type: no-parallel
trigger:
event:
- custom
@@ -2554,6 +2578,9 @@ steps:
depends_on:
- initialize
node:
type: no-parallel
trigger:
event:
- custom
@@ -2860,6 +2887,9 @@ services:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_USER: grafana
node:
type: no-parallel
trigger:
ref:
- refs/heads/v*
@@ -3325,6 +3355,9 @@ services:
image_pull_secrets:
- dockerconfigjson
node:
type: no-parallel
trigger:
ref:
- refs/heads/v*
@@ -3496,6 +3529,6 @@ get:
---
kind: signature
hmac: 68edb93a18f2e16f8a9b2bf3d21073e181e56d2420feec37ae80b121fc2faeeb
hmac: 4f23649a1678c66fb96af929675bcf569cca3b208d425eace86150d981ee9fbb
...
+11
View File
@@ -1,3 +1,14 @@
<!-- 8.1.1 START -->
# 8.1.1 (2021-08-09)
### Bug fixes
* **CloudWatch Logs:** Fix crash when no region is selected. [#37639](https://github.com/grafana/grafana/pull/37639), [@aocenas](https://github.com/aocenas)
* **Reporting:** Fix timezone parsing for scheduler (enterprise)
<!-- 8.1.1 END -->
<!-- 8.1.0 START -->
# 8.1.0 (2021-08-05)
+107 -19
View File
@@ -22,7 +22,7 @@ Family: scuemata.#Family & {
// Theme of dashboard.
style: *"light" | "dark"
// Timezone of dashboard,
timezone?: *"browser" | "utc"
timezone?: *"browser" | "utc" | ""
// Whether a dashboard is editable or not.
editable: bool | *true
// 0 for no shared crosshair or tooltip (default).
@@ -66,14 +66,62 @@ Family: scuemata.#Family & {
showIn: number | *0
}]
// Auto-refresh interval.
refresh?: string
refresh?: string | false
// Version of the JSON schema, incremented each time a Grafana update brings
// changes to said schema.
schemaVersion: number | *25
// FIXME this is the old schema numbering system, and will be replaced by scuemata
schemaVersion: number | *30
// Version of the dashboard, incremented each time the dashboard is updated.
version?: number
panels?: [...#Panel]
// TODO docs
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-saturated" | "continuous-GrYlRd" | "fixed" @cuetsy(targetType="enum")
// TODO docs
#FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(targetType="type")
// TODO docs
#FieldColor: {
// The main color scheme mode
mode: #FieldColorModeId | string
// Stores the fixed color value if mode is fixed
fixedColor?: string
// Some visualizations need to know how to assign a series color from by value color schemes
seriesBy?: #FieldColorSeriesByMode
} @cuetsy(targetType="interface")
// TODO docs
#Threshold: {
// TODO docs
// FIXME the corresponding typescript field is required/non-optional, but nulls currently appear here when serializing -Infinity to JSON
value?: number
// TODO docs
color: string
// TODO docs
// TODO are the values here enumerable into a disjunction?
// Some seem to be listed in typescript comment
state?: string
} @cuetsy(targetType="interface")
#ThresholdsMode: "absolute" | "percentage" @cuetsy(targetType="enum")
#ThresholdsConfig: {
mode: #ThresholdsMode
// Must be sorted by 'value', first value is always -Infinity
steps: [...#Threshold]
} @cuetsy(targetType="interface")
// Schema for panel targets is specified by datasource
// plugins. We use a placeholder definition, which the Go
// schema loader either left open/as-is with the Base
// variant of the Dashboard and Panel families, or filled
// with types derived from plugins in the Instance variant.
// When working directly from CUE, importers can extend this
// type directly to achieve the same effect.
#Target: {...}
// Dashboard panels. Panels are canonically defined inline
// because they share a version timeline with the dashboard
// schema; they do not vary independently. We create a separate,
@@ -83,6 +131,15 @@ Family: scuemata.#Family & {
// The panel plugin type id.
type: !=""
// TODO docs
id?: number
// FIXME this almost certainly has to be changed in favor of scuemata versions
pluginVersion?: string
// TODO docs
tags?: [...string]
// Internal - the exact major and minor versions of the panel plugin
// schema. Hidden and therefore not a part of the data model, but
// expected to be filled with panel plugin schema versions so that it's
@@ -93,6 +150,9 @@ Family: scuemata.#Family & {
// TODO 2-tuple list instead of struct?
panelSchema?: { maj: number, min: number }
// TODO docs
targets?: [...#Target]
// Panel title.
title?: string
// Description.
@@ -115,20 +175,33 @@ Family: scuemata.#Family & {
static?: bool
}
// Panel links.
// links?: [..._panelLink]
// FIXME this is temporarily specified as a closed list so
// that validation will pass when no links are present, but
// to force a failure as soon as it's checked against there
// being anything in the list so it can be fixed in
// accordance with that object
links?: []
// Name of template variable to repeat for.
repeat?: string
// Direction to repeat in if 'repeat' is set.
// "h" for horizontal, "v" for vertical.
repeatDirection: *"h" | "v"
// Schema for panel targets is specified by datasource
// plugins. We use a placeholder definition, which the Go
// schema loader either left open/as-is with the Base
// variant of the Dashboard and Panel families, or filled
// with types derived from plugins in the Instance variant.
// When working directly from CUE, importers can extend this
// type directly to achieve the same effect.
targets?: [...{...}]
// TODO docs
maxDataPoints?: number
// TODO docs
// TODO tighter constraint
interval?: string
// TODO docs
// TODO tighter constraint
timeFrom?: string
// TODO docs
// TODO tighter constraint
timeShift?: string
// The values depend on panel type
options: {...}
@@ -166,17 +239,25 @@ Family: scuemata.#Family & {
min?: number
max?: number
// // Convert input values into a display string
// mappings?: ValueMapping[];
// Convert input values into a display string
//
// TODO this one corresponds to a complex type with
// generics on the typescript side. Ouch. Will
// either need special care, or we'll just need to
// accept a very loosely specified schema. It's very
// unlikely we'll be able to translate cue to
// typescript generics in the general case, though
// this particular one *may* be able to work.
mappings?: [...{...}]
// // Map numeric values to states
// thresholds?: ThresholdsConfig;
// Map numeric values to states
thresholds?: #ThresholdsConfig
// // Map values to a display color
// color?: FieldColor;
color?: #FieldColor
// // Used when reducing field values
// nullValueMode?: NullValueMode;
// nullValueMode?: NullValueMode
// // The behavior when clicking on a result
links?: [...]
@@ -184,10 +265,17 @@ Family: scuemata.#Family & {
// Alternative to empty string
noValue?: string
// TODO conundrum: marking this struct as open would
// - i think - preclude closed validation of
// plugin-defined config bits. But, marking it
// closed makes it impossible to use just this
// schema (the "base" variant) to validate the base
// components of a dashboard.
//
// Can always exist. Valid fields within this are
// defined by the panel plugin - that's the
// PanelFieldConfig that comes from the plugin.
custom?: {}
custom?: {...}
}
overrides: [...{
matcher: {
+3 -3
View File
@@ -33,7 +33,7 @@ ScaleDistribution: "linear" | "log" @c
GraphGradientMode: "none" | "opacity" | "hue" | "scheme" @cuetsy(targetType="enum")
LineStyle: {
fill?: "solid" | "dash" | "dot" | "square"
dash?: [number]
dash?: [...number]
} @cuetsy(targetType="interface")
LineConfig: {
lineColor?: string
@@ -68,7 +68,7 @@ AxisConfig: {
HideSeriesConfig: {
tooltip: bool
legend: bool
graph: bool
viz: bool
} @cuetsy(targetType="interface")
LegendPlacement: "bottom" | "right" @cuetsy(targetType="type")
LegendDisplayMode: "list" | "table" | "hidden" @cuetsy(targetType="enum")
@@ -86,7 +86,7 @@ GraphFieldConfig: LineConfig & FillConfig & PointsConfig & AxisConfig & {
VizLegendOptions: {
displayMode: LegendDisplayMode
placement: LegendPlacement
calcs: [string]
calcs: [...string]
} @cuetsy(targetType="interface")
VizTooltipOptions: {
mode: TooltipDisplayMode
File diff suppressed because it is too large Load Diff
@@ -13,7 +13,6 @@
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 3151,
"links": [],
@@ -90,9 +89,7 @@
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "CPU per host",
"tooltip": {
"shared": true,
@@ -101,33 +98,24 @@
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
"align": false
}
},
{
@@ -199,9 +187,7 @@
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Login Count per host",
"tooltip": {
"shared": true,
@@ -210,33 +196,24 @@
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
"align": false
}
}
],
@@ -255,4 +232,4 @@
"title": "Datasource tests - OpenTSDB v2.3",
"uid": "rZRUGik7k",
"version": 3
}
}
@@ -13,13 +13,11 @@
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 625,
"links": [],
"panels": [
{
"datasource": null,
"gridPos": {
"h": 4,
"w": 24,
@@ -38,13 +36,10 @@
"target": ""
}
],
"timeFrom": null,
"timeShift": null,
"transparent": true,
"type": "text"
},
{
"cacheTimeout": null,
"datasource": "gdev-testdata",
"gridPos": {
"h": 8,
@@ -107,8 +102,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -166,7 +160,6 @@
"type": "state-timeline"
},
{
"cacheTimeout": null,
"datasource": "gdev-testdata",
"gridPos": {
"h": 15,
@@ -207,8 +200,7 @@
"mode": "absolute",
"steps": [
{
"color": "blue",
"value": null
"color": "blue"
},
{
"color": "green",
@@ -293,8 +285,7 @@
"mode": "absolute",
"steps": [
{
"color": "blue",
"value": null
"color": "blue"
},
{
"color": "green",
@@ -343,7 +334,6 @@
"type": "timeseries"
},
{
"cacheTimeout": null,
"datasource": "gdev-testdata",
"gridPos": {
"h": 13,
@@ -386,8 +376,7 @@
"mode": "absolute",
"steps": [
{
"color": "blue",
"value": null
"color": "blue"
},
{
"color": "#EAB839",
@@ -473,8 +462,7 @@
"mode": "absolute",
"steps": [
{
"color": "blue",
"value": null
"color": "blue"
},
{
"color": "#EAB839",
@@ -522,7 +510,6 @@
"type": "status-history"
},
{
"cacheTimeout": null,
"datasource": "gdev-testdata",
"gridPos": {
"h": 3,
@@ -573,8 +560,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -713,8 +699,7 @@
"mode": "absolute",
"steps": [
{
"color": "blue",
"value": null
"color": "blue"
}
]
},
@@ -757,7 +742,6 @@
"type": "barchart"
},
{
"cacheTimeout": null,
"datasource": "gdev-testdata",
"gridPos": {
"h": 3,
@@ -783,7 +767,6 @@
"type": "text"
},
{
"cacheTimeout": null,
"datasource": "gdev-testdata",
"gridPos": {
"h": 6,
@@ -848,8 +831,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -924,8 +906,6 @@
"stringInput": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "Interpolation mode: smooth",
"type": "timeseries"
},
@@ -969,8 +949,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -1045,8 +1024,6 @@
"stringInput": "1,20,90,30,5,0"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Interpolation mode: Step before",
"type": "timeseries"
},
@@ -1090,8 +1067,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -1166,13 +1142,10 @@
"stringInput": "1,20,90,30,5,0"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Interpolation mode: Step after",
"type": "timeseries"
},
{
"cacheTimeout": null,
"datasource": "gdev-testdata",
"gridPos": {
"h": 8,
@@ -1240,8 +1213,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -1300,8 +1272,6 @@
"stringInput": "10,11,12,11,10,11,12,12,11,10,9,10,11,12,10,10,11,12,13,11,10,9,10,11,12,13,14,10,10"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Auto min max",
"type": "timeseries"
},
@@ -1351,8 +1321,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -1411,8 +1380,6 @@
"stringInput": "10,11,12,11,10,11,12,12,11,10,9,10,11,12,200,10,11,12,13,11,10,9,10,11,12,13,14,10,10"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Hard min 0, max 30",
"type": "timeseries"
},
@@ -1462,8 +1429,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -1522,13 +1488,10 @@
"stringInput": "10,11,12,11,10,11,12,12,11,10,9,10,11,12,200,10,11,12,13,11,10,9,10,11,12,13,14,10,10"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Soft min 0, soft max 30",
"type": "timeseries"
},
{
"cacheTimeout": null,
"datasource": "gdev-testdata",
"gridPos": {
"h": 5,
@@ -1595,8 +1558,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -1769,7 +1731,6 @@
"type": "timeseries"
},
{
"cacheTimeout": null,
"datasource": "gdev-testdata",
"gridPos": {
"h": 5,
@@ -1844,8 +1805,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -2170,8 +2130,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -2493,8 +2452,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -2573,7 +2531,6 @@
"type": "timeseries"
},
{
"cacheTimeout": null,
"datasource": "gdev-testdata",
"gridPos": {
"h": 3,
@@ -2620,8 +2577,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -2655,7 +2611,6 @@
"y": 83
},
"id": 49,
"maxDataPoints": null,
"options": {
"bucketOffset": 0,
"bucketSize": 10,
@@ -2719,8 +2674,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -2754,7 +2708,6 @@
"y": 83
},
"id": 77,
"maxDataPoints": null,
"options": {
"showHeader": true
},
@@ -2838,4 +2791,4 @@
"title": "New Features in v8.0",
"uid": "8mux8PqGz",
"version": 17
}
}
+1 -23
View File
@@ -19,12 +19,10 @@
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
{
"datasource": null,
"gridPos": {
"h": 26,
"w": 6,
@@ -34,7 +32,6 @@
"id": 7,
"links": [],
"options": {
"folderId": null,
"maxItems": 100,
"query": "",
"showHeadings": true,
@@ -45,13 +42,10 @@
},
"pluginVersion": "8.1.0-pre",
"tags": [],
"timeFrom": null,
"timeShift": null,
"title": "Starred",
"type": "dashlist"
},
{
"datasource": null,
"gridPos": {
"h": 13,
"w": 6,
@@ -75,13 +69,10 @@
"tags": [
"panel-tests"
],
"timeFrom": null,
"timeShift": null,
"title": "tag: panel-tests",
"type": "dashlist"
},
{
"datasource": null,
"gridPos": {
"h": 13,
"w": 6,
@@ -91,7 +82,6 @@
"id": 3,
"links": [],
"options": {
"folderId": null,
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -108,13 +98,10 @@
"gdev",
"demo"
],
"timeFrom": null,
"timeShift": null,
"title": "tag: dashboard-demo",
"type": "dashlist"
},
{
"datasource": null,
"gridPos": {
"h": 26,
"w": 6,
@@ -124,7 +111,6 @@
"id": 5,
"links": [],
"options": {
"folderId": null,
"maxItems": 1000,
"query": "",
"showHeadings": false,
@@ -141,13 +127,10 @@
"gdev",
"datasource-test"
],
"timeFrom": null,
"timeShift": null,
"title": "Data source tests",
"type": "dashlist"
},
{
"datasource": null,
"gridPos": {
"h": 13,
"w": 6,
@@ -173,13 +156,10 @@
"templating",
"gdev"
],
"timeFrom": null,
"timeShift": null,
"title": "tag: templating ",
"type": "dashlist"
},
{
"datasource": null,
"gridPos": {
"h": 13,
"w": 6,
@@ -205,8 +185,6 @@
"gdev",
"demo"
],
"timeFrom": null,
"timeShift": null,
"title": "tag: transforms",
"type": "dashlist"
}
@@ -250,4 +228,4 @@
"title": "Grafana Dev Overview & Home",
"uid": "j6T00KRZz",
"version": 2
}
}
@@ -19,12 +19,10 @@
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -49,8 +47,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -96,7 +93,6 @@
"type": "barchart"
},
{
"datasource": null,
"description": "Should be smaller given the longer value",
"fieldConfig": {
"defaults": {
@@ -123,8 +119,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -170,7 +165,6 @@
"type": "barchart"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -195,8 +189,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -242,7 +235,6 @@
"type": "barchart"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -267,8 +259,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -339,8 +330,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -386,7 +376,6 @@
"type": "barchart"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -412,8 +401,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -459,7 +447,6 @@
"type": "barchart"
},
{
"datasource": null,
"description": "",
"fieldConfig": {
"defaults": {
@@ -485,8 +472,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -552,4 +538,4 @@
"title": "BarChart - Panel Tests - Value sizing",
"uid": "WFlOM-jM1",
"version": 9
}
}
@@ -13,7 +13,6 @@
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
@@ -59,8 +58,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "orange",
@@ -104,7 +102,6 @@
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -145,8 +142,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "orange",
@@ -192,7 +188,6 @@
"startValue": 1
}
],
"timeFrom": null,
"title": "Color bars by discrete thresholds",
"type": "timeseries"
},
@@ -238,8 +233,7 @@
"mode": "absolute",
"steps": [
{
"color": "blue",
"value": null
"color": "blue"
},
{
"color": "green",
@@ -328,8 +322,7 @@
"mode": "absolute",
"steps": [
{
"color": "blue",
"value": null
"color": "blue"
},
{
"color": "green",
@@ -418,8 +411,7 @@
"mode": "absolute",
"steps": [
{
"color": "blue",
"value": null
"color": "blue"
},
{
"color": "green",
@@ -467,7 +459,6 @@
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -508,8 +499,7 @@
"mode": "absolute",
"steps": [
{
"color": "blue",
"value": null
"color": "blue"
},
{
"color": "green",
@@ -590,4 +580,4 @@
"title": "Panel Tests - Graph NG - By value color schemes",
"uid": "aBXrJ0R7z",
"version": 20
}
}
@@ -13,7 +13,6 @@
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [
{
@@ -69,8 +68,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -132,8 +130,6 @@
"target": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "Show gaps",
"type": "timeseries"
},
@@ -180,8 +176,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -251,8 +246,6 @@
"target": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "Gaps & null between every point for series B",
"type": "timeseries"
},
@@ -299,8 +292,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -390,8 +382,6 @@
"target": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "No nulls but unaligned series",
"type": "timeseries"
},
@@ -436,8 +426,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -499,8 +488,6 @@
"target": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "Connected",
"type": "timeseries"
},
@@ -547,8 +534,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -618,8 +604,6 @@
"target": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "Same as above but connected",
"type": "timeseries"
},
@@ -666,8 +650,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -757,8 +740,6 @@
"target": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "Same as above but connected",
"type": "timeseries"
},
@@ -803,8 +784,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -903,8 +883,6 @@
"stringInput": "1,20,90,30,5,0"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Null values in first series & show gaps ",
"transformations": [],
"type": "timeseries"
@@ -950,8 +928,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -1050,14 +1027,11 @@
"stringInput": "10,25,null,null,50,10"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Null values in second series show gaps (bugged)",
"transformations": [],
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -1097,8 +1071,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -1157,13 +1130,10 @@
"stringInput": "1,20,90,null,30,5,0"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Span nulls below 1hr",
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -1205,8 +1175,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -1264,4 +1233,4 @@
"title": "Panel Tests - Graph NG - Gaps and Connected",
"uid": "8mmCAF1Mz",
"version": 12
}
}
@@ -13,13 +13,11 @@
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 632,
"links": [],
"panels": [
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -40,8 +38,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -100,8 +97,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -139,7 +135,6 @@
"type": "histogram"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -160,8 +155,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -212,7 +206,6 @@
"type": "histogram"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -233,8 +226,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -285,7 +277,6 @@
"type": "histogram"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -300,8 +291,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -353,7 +343,6 @@
"type": "table"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -368,8 +357,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -439,4 +427,4 @@
"title": "Panel Tests - Histogram",
"uid": "UTv--wqMk",
"version": 4
}
}
@@ -13,12 +13,10 @@
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -55,8 +53,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -113,7 +110,6 @@
"type": "state-timeline"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -151,8 +147,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -207,7 +202,6 @@
"type": "state-timeline"
},
{
"datasource": null,
"description": "Should show gaps",
"fieldConfig": {
"defaults": {
@@ -246,8 +240,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -301,7 +294,6 @@
"type": "state-timeline"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -319,8 +311,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -339,7 +330,6 @@
"y": 19
},
"id": 4,
"interval": null,
"maxDataPoints": 20,
"options": {
"alignValue": "center",
@@ -411,4 +401,4 @@
"title": "Timeline Demo",
"uid": "mIJjFy8Kz",
"version": 13
}
}
@@ -13,13 +13,11 @@
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 329,
"links": [],
"panels": [
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -34,8 +32,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -165,7 +162,6 @@
1616557148000
],
[
null,
1616558756000
],
[
@@ -173,7 +169,6 @@
1616561658000
],
[
null,
1616562446000
],
[
@@ -181,7 +176,6 @@
1616564104000
],
[
null,
1616564548000
],
[
@@ -197,7 +191,6 @@
"type": "state-timeline"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -212,8 +205,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -267,7 +259,6 @@
"type": "state-timeline"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -282,8 +273,7 @@
"mode": "percentage",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "#EAB839",
@@ -306,7 +296,6 @@
"y": 21
},
"id": 4,
"interval": null,
"maxDataPoints": 20,
"options": {
"colWidth": 0.9,
@@ -20,12 +20,10 @@
},
"description": "",
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -64,8 +62,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -122,7 +119,6 @@
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -137,8 +133,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -209,7 +204,6 @@
"type": "table"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -224,8 +218,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -291,7 +284,6 @@
"type": "table"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -306,8 +298,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -377,7 +368,6 @@
"type": "table"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -392,8 +382,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -446,7 +435,6 @@
"type": "table"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -470,8 +458,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -565,4 +552,4 @@
"title": "Transforms - Config from query",
"uid": "Juj4_7ink",
"version": 1
}
}
@@ -19,7 +19,6 @@
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
@@ -39,8 +38,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -87,8 +85,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -146,7 +143,6 @@
"type": "table"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -157,8 +153,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -209,7 +204,6 @@
"type": "stat"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -221,8 +215,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -286,8 +279,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -334,8 +326,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -393,7 +384,6 @@
"type": "table"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -404,8 +394,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -454,7 +443,6 @@
"type": "bargauge"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -466,8 +454,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
}
]
}
@@ -530,7 +517,6 @@
"type": "gauge"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
@@ -541,8 +527,7 @@
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
@@ -612,4 +597,4 @@
"title": "Transforms - Rows to fields",
"uid": "PMtIInink",
"version": 1
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ weight = 400
An exemplar is a specific trace representative of a repeated pattern of data in a given time interval. It helps you identify higher cardinality metadata from specific events within time series data.
Suppose your company website is experiencing a surge in traffic volumes. While more than eighty percent of the users are able to access the website in under two seconds, some users are experiencing a higher than normal response time resulting in bad user experience
Suppose your company website is experiencing a surge in traffic volumes. While more than eighty percent of the users are able to access the website in under two seconds, some users are experiencing a higher than normal response time resulting in bad user experience.
To identify the factors that are contributing to the latency, you must compare a trace for a fast response against a trace for a slow response. Given the vast amount of data in a typical production environment, it will be extremely laborious and time-consuming effort.
+2 -4
View File
@@ -3,16 +3,14 @@ title = "Alertmanager"
description = "Guide for using Alertmanager in Grafana"
keywords = ["grafana", "prometheus", "guide"]
aliases = ["/docs/grafana/latest/features/datasources/alertmanager"]
weight = 1300
weight = 150
+++
# Alertmanager data source
Grafana includes built-in support for Prometheus Alertmanager. It is presently in alpha and not accessible unless [alpha plugins are enabled in Grafana settings](https://grafana.com/docs/grafana/latest/administration/configuration/#enable_alpha). Once you add it as a data source, you can use the [Grafana alerting UI](https://grafana.com/docs/grafana/latest/alerting/) to manage silences, contact points as well as notification policies. A drop down option in these pages allows you to switch between Grafana and any configured Alertmanager data sources .
> **Note:** New in Grafana 8.0.
> **Note:** Currently only the [Cortex implementation of Prometheus alertmanager](https://cortexmetrics.io/docs/proposals/scalable-alertmanager/) is supported.
> **Note:** Currently, the [Cortex implementation of Prometheus Alertmanager](https://cortexmetrics.io/docs/proposals/scalable-alertmanager/) is required to edit rules.
## Provision the Alertmanager data source
+1 -1
View File
@@ -41,7 +41,7 @@ There are three different authentication methods available. `AWS SDK Default` pe
### IAM roles
Currently all access to CloudWatch is done server side by the Grafana backend using the official AWS SDK. Providing you have chosen the _AWS SDK Default_ authentication method, and your Grafana server is running on AWS, you can use IAM Roles to handle authentication automically.
Currently all access to CloudWatch is done server side by the Grafana backend using the official AWS SDK. Providing you have chosen the _AWS SDK Default_ authentication method, and your Grafana server is running on AWS, you can use IAM Roles to handle authentication automatically.
See the AWS documentation on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)
+1 -1
View File
@@ -3,7 +3,7 @@ title = "Elasticsearch"
description = "Guide for using Elasticsearch in Grafana"
keywords = ["grafana", "elasticsearch", "guide"]
aliases = ["/docs/grafana/latest/features/datasources/elasticsearch"]
weight = 400
weight = 325
+++
# Using Elasticsearch in Grafana
@@ -3,7 +3,7 @@ title = "Google Cloud Monitoring"
description = "Guide for using Google Cloud Monitoring in Grafana"
keywords = ["grafana", "stackdriver", "google", "guide", "cloud", "monitoring"]
aliases = ["/docs/grafana/latest/features/datasources/stackdriver", "/docs/grafana/next/datasources/cloudmonitoring/", "/docs/grafana/next/features/datasources/cloudmonitoring/"]
weight = 200
weight = 350
+++
# Using Google Cloud Monitoring in Grafana
@@ -21,10 +21,11 @@ datasources:
access: proxy
database: site
user: grafana
password: grafana
url: http://localhost:8086
jsonData:
httpMode: GET
secureJsonData:
password: grafana
```
## InfluxDB 2.x for Flux example
@@ -285,9 +285,9 @@ func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataReque
If your data source uses the same OAuth provider as Grafana itself, for example using [Generic OAuth Authentication]({{< relref "../../auth/generic-oauth.md" >}}), your data source plugin can reuse the access token for the logged-in Grafana user.
To allow Grafana to pass the access token to the plugin, the user needs to enable **Forward OAuth Identity** on the data source configuration page. This tells Grafana to pass the token to the plugin in a Authorization header. Note that your plugin needs to use the [DataSourceHttpSettings](https://developers.grafana.com/ui/latest/index.html?path=/story/data-source-datasourcehttpsettings--basic) component in the configuration editor to expose the setting for your data source plugin.
To allow Grafana to pass the access token to the plugin, update the data source configuration and set the` jsonData.oauthPassThru` property to `true`. The [DataSourceHttpSettings](https://developers.grafana.com/ui/latest/index.html?path=/story/data-source-datasourcehttpsettings--basic) provides a toggle, the **Forward OAuth Identity** option, for this. You can also build an appropriate toggle to set `jsonData.oauthPassThru` in your data source configuration page UI.
The Authorization header is available on the `DataQuery` object on the query data request in your backend data source.
When configured, Grafana will pass the user's token to the plugin in an Authorization header, available on the `QueryDataRequest` object on the `QueryData` request in your backend data source.
```go
func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
@@ -39,7 +39,7 @@ The following list contains fine-grained access control actions.
| `reports:send` | `reports:*` | Send a report email. |
| `reports.settings:write` | n/a | Update report settings. |
| `reports.settings:read` | n/a | Read report settings. |
| `provisioning:reload` | `service:accesscontrol` | Reload provisioning files. |
| `provisioning:reload` | `services:accesscontrol` | Reload provisioning files. |
| `users:read` | `global:users:*` | Read or search user profiles. |
| `users:write` | `global:users:*` | Update a user’s profile. |
| `users.teams:read` | `global:users:*` | Read a user’s teams. |
@@ -62,7 +62,7 @@ The following list contains fine-grained access control actions.
| `ldap.user:sync` | n/a | Sync a user via LDAP. |
| `ldap.status:read` | n/a | Verify the LDAP servers’ availability. |
| `ldap.config:reload` | n/a | Reload the LDAP configuration. |
| `status:accesscontrol` | `service:accesscontrol` | Get access-control enabled status. |
| `status:accesscontrol` | `services:accesscontrol` | Get access-control enabled status. |
| `settings:read` | `settings:**`<br>`settings:auth.saml:*`<br>`settings:auth.saml:enabled` (property level) | Read settings |
| `settings:write` | `settings:**`<br>`settings:auth.saml:*`<br>`settings:auth.saml:enabled` (property level) | Update settings |
| `server.stats:read` | n/a | Read server stats |
@@ -72,12 +72,12 @@ The following list contains fine-grained access control actions.
The following list contains fine-grained access control scopes.
| Scopes | Descriptions |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `roles:*` | Restrict an action to a set of roles. For example, `roles:*` matches any role, `roles:randomuid` matches only the role with UID `randomuid` and `roles:custom:reports:{editor,viewer}` matches both `custom:reports:editor` and `custom:reports:viewer` roles. |
| `permissions:delegate` | The scope is only applicable for roles associated with the Access Control itself and indicates that you can delegate your permissions only, or a subset of it, by creating a new role or making an assignment. |
| `reports:*` | Restrict an action to a set of reports. For example, `reports:*` matches any report and `reports:1` matches the report with id `1`. |
| `service:accesscontrol` | Restrict an action to target only the fine-grained access control service. For example, you can use this in conjunction with the `provisioning:reload` or the `status:accesscontrol` actions. |
| `global:users:*` | Restrict an action to a set of global users. |
| `users:*` | Restrict an action to a set of users from an organization. |
| `settings:**` | Restrict an action to a subset of settings. For example, `settings:**` matches all settings, `settings:auth.saml:*` matches all SAML settings, and `settings:auth.saml:enabled` matches the enable property on the SAML settings. |
| Scopes | Descriptions |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `roles:*` | Restrict an action to a set of roles. For example, `roles:*` matches any role, `roles:randomuid` matches only the role with UID `randomuid` and `roles:custom:reports:{editor,viewer}` matches both `custom:reports:editor` and `custom:reports:viewer` roles. |
| `permissions:delegate` | The scope is only applicable for roles associated with the Access Control itself and indicates that you can delegate your permissions only, or a subset of it, by creating a new role or making an assignment. |
| `reports:*` | Restrict an action to a set of reports. For example, `reports:*` matches any report and `reports:1` matches the report with id `1`. |
| `services:accesscontrol` | Restrict an action to target only the fine-grained access control service. For example, you can use this in conjunction with the `provisioning:reload` or the `status:accesscontrol` actions. |
| `global:users:*` | Restrict an action to a set of global users. |
| `users:*` | Restrict an action to a set of users from an organization. |
| `settings:**` | Restrict an action to a subset of settings. For example, `settings:**` matches all settings, `settings:auth.saml:*` matches all SAML settings, and `settings:auth.saml:enabled` matches the enable property on the SAML settings. |
+30 -22
View File
@@ -26,10 +26,11 @@ Returns an indicator to check if fine-grained access control is enabled or not.
| -------------------- | ---------------------- |
| status:accesscontrol | services:accesscontrol |
#### Example request
```http
GET /api/access-control/check
GET /api/access-control/status
Accept: application/json
Content-Type: application/json
```
@@ -61,6 +62,7 @@ Content-Type: application/json; charset=UTF-8
`GET /api/access-control/roles`
Gets all existing roles. The response contains all global and organization local roles, for the organization which user is signed in.
Refer to the [Role scopes]({{< relref "../enterprise/access-control/roles.md#built-in-role-assignments" >}}) for more information.
#### Required permissions
@@ -254,6 +256,7 @@ Content-Type: application/json; charset=UTF-8
#### Status codes
| Code | Description |
| ---- | ---------------------------------------------------------------------------------- |
| 200 | Role is updated. |
@@ -276,6 +279,7 @@ For example, if a user does not have required permissions for creating users, th
| ----------- | -------------------- |
| roles:write | permissions:delegate |
#### Example request
```http
@@ -284,16 +288,17 @@ Accept: application/json
Content-Type: application/json
{
"version": 2,
"name": "custom:delete:create:roles",
"description": "My custom role which gives users permissions to delete and create roles",
"version": 3,
"name": "custom:delete:write:roles",
"description": "My custom role which gives users permissions to delete and write roles",
"global": true,
"permissions": [
{
"action": "roles:delete",
"scope": "permissions:delegate"
},
{
"action": "roles:create",
{
"action": "roles:write",
"scope": "permissions:delegate"
}
]
@@ -323,25 +328,27 @@ HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
"version": 3,
"name": "custom:delete:create:roles",
"description": "My custom role which gives users permissions to delete and create roles",
"permissions": [
"version":3,
"uid":"jZrmlLCGka",
"name":"custom:delete:write:roles",
"description":"My custom role which gives users permissions to delete and write roles",
"permissions":[
{
"action": "roles:delete",
"scope": "permissions:delegate",
"updated": "2021-05-13T23:19:46.546146+02:00",
"created": "2021-05-13T23:19:46.546146+02:00"
"action":"roles:delete",
"scope":"permissions:delegate",
"updated":"2021-08-06T18:27:40+02:00",
"created":"2021-08-06T18:27:40+02:00"
},
{
"action": "roles:create",
"scope": "permissions:delegate",
"updated": "2021-05-13T23:19:46.546146+02:00",
"created": "2021-05-13T23:19:46.546146+02:00"
{
"action":"roles:write",
"scope":"permissions:delegate",
"updated":"2021-08-06T18:27:41+02:00",
"created":"2021-08-06T18:27:41+02:00"
}
],
"updated": "2021-05-13T23:19:46.540987+02:00",
"created": "2021-05-13T23:19:46.540986+02:00"
"updated":"2021-08-06T18:27:41+02:00",
"created":"2021-08-06T18:27:40+02:00",
"global":true
}
```
@@ -370,10 +377,11 @@ For example, if a user does not have required permissions for creating users, th
| ------------ | -------------------- |
| roles:delete | permissions:delegate |
#### Example request
```http
DELETE /api/access-control/roles/jZrmlLCGka?force=true
DELETE /api/access-control/roles/jZrmlLCGka?force=true&global=true
Accept: application/json
```
+3 -3
View File
@@ -612,9 +612,9 @@ Only works with Basic Authentication (username and password). See [introduction]
See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation.
| Action | Scope | Provision entity |
| ------------------- | --------------------- | ---------------- |
| provisioning:reload | service:accesscontrol | accesscontrol |
| Action | Scope | Provision entity |
| ------------------- | ---------------------- | ---------------- |
| provisioning:reload | services:accesscontrol | accesscontrol |
**Example Request**:
+1
View File
@@ -8,6 +8,7 @@ weight = 10000
Here you can find detailed release notes that list everything that is included in every release as well as notices
about deprecations, breaking changes as well as changes that relate to plugin development.
- [Release notes for 8.1.1]({{< relref "release-notes-8-1-1" >}})
- [Release notes for 8.1.0]({{< relref "release-notes-8-1-0" >}})
- [Release notes for 8.1.0-beta3]({{< relref "release-notes-8-1-0-beta3" >}})
- [Release notes for 8.1.0-beta2]({{< relref "release-notes-8-1-0-beta2" >}})
@@ -0,0 +1,14 @@
+++
title = "Release notes for Grafana 8.1.1"
[_build]
list = false
+++
<!-- Auto generated by update changelog github action -->
# Release notes for Grafana 8.1.1
### Bug fixes
* **CloudWatch Logs:** Fix crash when no region is selected. [#37639](https://github.com/grafana/grafana/pull/37639), [@aocenas](https://github.com/aocenas)
+1 -1
View File
@@ -62,7 +62,7 @@ If you created a snapshot by mistake, click **delete snapshot** to remove the sn
You can embed a panel using an iframe on another web site. A viewer must be signed into Grafana to view the graph.
**> Note:** As of Grafana 8.0, anonymous access permission is no available for Grafana Cloud.
**> Note:** As of Grafana 8.0, anonymous access permission is no longer available for Grafana Cloud.
![Panel share embed](/static/img/docs/sharing/share-panel-embedded-link-8-0.png)
+2 -2
View File
@@ -1,4 +1,4 @@
{
"stable": "8.1.0",
"testing": "8.1.0"
"stable": "8.1.1",
"testing": "8.1.1"
}
@@ -6,7 +6,7 @@ import { Field, FieldType } from '../types/dataFrame';
import { DisplayProcessor, DisplayValue } from '../types/displayValue';
import { getValueFormat, isBooleanUnit } from '../valueFormats/valueFormats';
import { getValueMappingResult } from '../utils/valueMappings';
import { dateTime } from '../datetime';
import { dateTime, dateTimeParse } from '../datetime';
import { KeyValue, TimeZone } from '../types';
import { getScaleCalculator } from './scale';
import { GrafanaTheme2 } from '../themes/types';
@@ -45,10 +45,23 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
let unit = config.unit;
let hasDateUnit = unit && (timeFormats[unit] || unit.startsWith('time:'));
let showMs = false;
if (field.type === FieldType.time && !hasDateUnit) {
unit = `dateTimeAsSystem`;
hasDateUnit = true;
if (field.values && field.values.length > 1) {
let start = field.values.get(0);
let end = field.values.get(field.values.length - 1);
if (typeof start === 'string') {
start = dateTimeParse(start).unix();
end = dateTimeParse(end).unix();
} else {
start /= 1e3;
end /= 1e3;
}
showMs = end - start < 60; //show ms when minute or less
}
} else if (field.type === FieldType.boolean) {
if (!isBooleanUnit(unit)) {
unit = 'bool';
@@ -93,7 +106,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
if (!isNaN(numeric)) {
if (shouldFormat && !isBoolean(value)) {
const v = formatFunc(numeric, config.decimals, null, options.timeZone);
const v = formatFunc(numeric, config.decimals, null, options.timeZone, showMs);
text = v.text;
suffix = v.suffix;
prefix = v.prefix;
@@ -4,28 +4,25 @@ import { toDataFrame } from '../dataframe';
import { createTheme } from '../themes';
describe('getFieldDisplayValuesProxy', () => {
const data = applyFieldOverrides({
data: [
toDataFrame({
fields: [
{ name: 'Time', values: [1, 2, 3] },
{
name: 'power',
values: [100, 200, 300],
labels: {
name: 'POWAH!',
},
config: {
displayName: 'The Power',
},
},
{
name: 'Last',
values: ['a', 'b', 'c'],
},
],
}),
],
const shortTimeField = [{ name: 'Time', values: [1, 2, 3] }];
const longTimeField = [{ name: 'Time', values: [1000, 2000, 61000] }];
const dataFields = [
{
name: 'power',
values: [100, 200, 300],
labels: {
name: 'POWAH!',
},
config: {
displayName: 'The Power',
},
},
{ name: 'Last', values: ['a', 'b', 'c'] },
];
const overrides = {
fieldConfig: {
defaults: {},
overrides: [],
@@ -33,29 +30,45 @@ describe('getFieldDisplayValuesProxy', () => {
replaceVariables: (val: string) => val,
timeZone: 'utc',
theme: createTheme(),
};
const dataShortTimeRange = applyFieldOverrides({
...{ data: [toDataFrame({ fields: [...shortTimeField, ...dataFields] })] },
...overrides,
})[0];
const dataLongTimeRange = applyFieldOverrides({
...{ data: [toDataFrame({ fields: [...longTimeField, ...dataFields] })] },
...overrides,
})[0];
it('should define all display functions', () => {
// Field display should be set
for (const field of data.fields) {
for (const field of dataShortTimeRange.fields) {
expect(field.display).toBeDefined();
}
});
it('should format the time values in UTC', () => {
it('should format the time values in UTC with ms when time range is minute or less', () => {
// Test Proxies in general
const p = getFieldDisplayValuesProxy({ frame: data, rowIndex: 0 });
const p = getFieldDisplayValuesProxy({ frame: dataShortTimeRange, rowIndex: 0 });
const time = p.Time;
expect(time.numeric).toEqual(1);
expect(time.text).toEqual('1970-01-01 00:00:00');
expect(time.text).toEqual('1970-01-01 00:00:00.001');
// Should get to the same values by name or index
const time2 = p[0];
expect(time2.toString()).toEqual(time.toString());
});
it('should format the time values in UTC without ms when time range is over a minute', () => {
const p = getFieldDisplayValuesProxy({ frame: dataLongTimeRange, rowIndex: 0 });
const time = p.Time;
expect(time.text).toEqual('1970-01-01 00:00:01');
});
it('Lookup by name, index, or displayName', () => {
const p = getFieldDisplayValuesProxy({ frame: data, rowIndex: 2 });
const p = getFieldDisplayValuesProxy({ frame: dataShortTimeRange, rowIndex: 2 });
expect(p.power.numeric).toEqual(300);
expect(p['power'].numeric).toEqual(300);
expect(p['POWAH!'].numeric).toEqual(300);
@@ -64,7 +77,7 @@ describe('getFieldDisplayValuesProxy', () => {
});
it('should return undefined when missing', () => {
const p = getFieldDisplayValuesProxy({ frame: data, rowIndex: 0 });
const p = getFieldDisplayValuesProxy({ frame: dataShortTimeRange, rowIndex: 0 });
expect(p.xyz).toBeUndefined();
expect(p[100]).toBeUndefined();
});
@@ -1,14 +1,16 @@
import { Observable } from 'rxjs';
import { ComponentType } from 'react';
import { DataQuery, QueryEditorProps } from './datasource';
import { QueryEditorProps } from './datasource';
import { DataFrame } from './dataFrame';
import { DataQuery, DatasourceRef } from './query';
/**
* This JSON object is stored in the dashboard json model.
*/
export interface AnnotationQuery<TQuery extends DataQuery = DataQuery> {
datasource?: string | null;
datasource?: DatasourceRef | string | null;
enable: boolean;
name: string;
iconColor: string;
@@ -1,5 +1,40 @@
import { FieldConfigSource } from './fieldOverrides';
import { DataQuery, DatasourceRef } from './query';
export enum DashboardCursorSync {
Off,
Crosshair,
Tooltip,
}
/**
* @public
*/
export interface PanelModel<TOptions = any, TCustomFieldConfig extends object = any> {
/** ID of the panel within the current dashboard */
id: number;
/** Panel title */
title?: string;
/** Description */
description?: string;
/** Panel options */
options: TOptions;
/** Field options configuration */
fieldConfig: FieldConfigSource<TCustomFieldConfig>;
/** Version of the panel plugin */
pluginVersion?: string;
/** The datasource used in all targets */
datasource?: DatasourceRef | null;
/** The queries in a panel */
targets?: DataQuery[];
/** alerting v1 object */
alert?: any;
}
+1 -4
View File
@@ -2,6 +2,7 @@ import { FieldConfig } from './dataFrame';
import { DataTransformerConfig } from './transformations';
import { ApplyFieldOverrideOptions } from './fieldOverrides';
import { PanelPluginDataSupport } from '.';
import { DataTopic } from './query';
export type KeyValue<T = any> = Record<string, T>;
@@ -17,10 +18,6 @@ export enum LoadingState {
Error = 'Error',
}
export enum DataTopic {
Annotations = 'annotations',
}
// Should be kept in sync with grafana-plugin-sdk-go/data/frame_meta.go
export type PreferredVisualisationType = 'graph' | 'table' | 'logs' | 'trace' | 'nodeGraph';
+1 -1
View File
@@ -1,4 +1,4 @@
import { DataQuery } from './datasource';
import { DataQuery } from './query';
import { InterpolateFunction } from './panel';
/**
+5 -39
View File
@@ -4,7 +4,7 @@ import { GrafanaPlugin, PluginMeta } from './plugin';
import { PanelData } from './panel';
import { LogRowModel } from './logs';
import { AnnotationEvent, AnnotationQuery, AnnotationSupport } from './annotations';
import { DataTopic, KeyValue, LoadingState, TableData, TimeSeries } from './data';
import { KeyValue, LoadingState, TableData, TimeSeries } from './data';
import { DataFrame, DataFrameDTO } from './dataFrame';
import { RawTimeRange, TimeRange } from './time';
import { ScopedVars } from './ScopedVars';
@@ -12,6 +12,7 @@ import { CoreApp } from './app';
import { LiveChannelSupport } from './live';
import { CustomVariableSupport, DataSourceVariableSupport, StandardVariableSupport } from './variables';
import { makeClassES5Compatible } from '../utils/makeClassES5Compatible';
import { DataQuery } from './query';
export interface DataSourcePluginOptionsEditorProps<JSONData = DataSourceJsonData, SecureJSONData = {}> {
options: DataSourceSettings<JSONData, SecureJSONData>;
@@ -205,6 +206,9 @@ abstract class DataSourceApi<
this.type = instanceSettings.type;
this.meta = {} as DataSourcePluginMeta;
this.uid = instanceSettings.uid;
if (!this.uid) {
this.uid = this.name; // Internal datasources do not have a UID (-- Grafana --)
}
}
/**
@@ -430,44 +434,6 @@ export interface DataQueryResponse {
state?: LoadingState;
}
/**
* These are the common properties available to all queries in all datasources
* Specific implementations will extend this interface adding the required properties
* for the given context
*/
export interface DataQuery {
/**
* A - Z
*/
refId: string;
/**
* true if query is disabled (ie should not be returned to the dashboard)
*/
hide?: boolean;
/**
* Unique, guid like, string used in explore mode
*/
key?: string;
/**
* Specify the query flavor
*/
queryType?: string;
/**
* The data topic results should be attached to
*/
dataTopic?: DataTopic;
/**
* For mixed data sources the selected datasource is on the query level.
* For non mixed scenarios this is undefined.
*/
datasource?: string | null;
}
export enum DataQueryErrorType {
Cancelled = 'cancelled',
Timeout = 'timeout',
+1
View File
@@ -2,6 +2,7 @@ export * from './data';
export * from './dataFrame';
export * from './dataLink';
export * from './dashboard';
export * from './query';
export * from './annotations';
export * from './logs';
export * from './navModel';
+1 -1
View File
@@ -1,7 +1,7 @@
import { Labels } from './data';
import { DataFrame } from './dataFrame';
import { DataQuery } from './query';
import { AbsoluteTimeRange } from './time';
import { DataQuery } from './datasource';
/**
* Mapping of log level abbreviation to canonical log level.
+1 -13
View File
@@ -11,6 +11,7 @@ import { StandardEditorProps } from '../field';
import { OptionsEditorItem } from './OptionsUIRegistryBuilder';
import { OptionEditorConfig } from './options';
import { AlertStateInfo } from './alerts';
import { PanelModel } from './dashboard';
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
@@ -121,19 +122,6 @@ export interface PanelEditorProps<T = any> {
data?: PanelData;
}
export interface PanelModel<TOptions = any> {
/** ID of the panel within the current dashboard */
id: number;
alert?: any;
/** Panel options */
options: TOptions;
/** Field options configuration */
fieldConfig: FieldConfigSource;
/** Version of the panel plugin */
pluginVersion?: string;
scopedVars?: ScopedVars;
}
/**
* Called when a panel is first loaded with current panel model
*/
+50
View File
@@ -0,0 +1,50 @@
/**
* Attached to query results (not persisted)
*
* @public
*/
export enum DataTopic {
Annotations = 'annotations',
}
/**
* In 8.2, this will become an interface
*
* @public
*/
export type DatasourceRef = string;
/**
* These are the common properties available to all queries in all datasources
* Specific implementations will *extend* this interface adding the required properties
* for the given context
*
* @public
*/
export interface DataQuery {
/**
* A - Z
*/
refId: string;
/**
* true if query is disabled (ie should not be returned to the dashboard)
*/
hide?: boolean;
/**
* Unique, guid like, string used in explore mode
*/
key?: string;
/**
* Specify the query flavor
*/
queryType?: string;
/**
* For mixed data sources the selected datasource is on the query level.
* For non mixed scenarios this is undefined.
*/
datasource?: DatasourceRef;
}
@@ -1,5 +1,6 @@
import { Observable } from 'rxjs';
import { DataQuery, DataSourceApi } from './datasource';
import { DataQuery, DatasourceRef } from './query';
import { DataSourceApi } from './datasource';
import { PanelData } from './panel';
import { ScopedVars } from './ScopedVars';
import { TimeRange, TimeZone } from './time';
@@ -10,7 +11,7 @@ import { TimeRange, TimeZone } from './time';
* @internal
*/
export interface QueryRunnerOptions {
datasource: string | DataSourceApi | null;
datasource: DatasourceRef | DataSourceApi | null;
queries: DataQuery[];
panelId?: number;
dashboardId?: number;
+1 -1
View File
@@ -1,8 +1,8 @@
import { ComponentType } from 'react';
import { Observable } from 'rxjs';
import { DataQuery } from './query';
import {
DataQuery,
DataQueryRequest,
DataQueryResponse,
DataSourceApi,
@@ -405,9 +405,15 @@ export function dateTimeSystemFormatter(
value: number,
decimals: DecimalCount,
scaledDecimals: DecimalCount,
timeZone?: TimeZone
timeZone?: TimeZone,
showMs?: boolean
): FormattedValue {
return { text: dateTimeFormat(value, { format: systemDateFormats.fullDate, timeZone }) };
return {
text: dateTimeFormat(value, {
format: showMs ? systemDateFormats.fullDateMS : systemDateFormats.fullDate,
timeZone,
}),
};
}
export function dateTimeFromNow(
@@ -18,7 +18,8 @@ export type ValueFormatter = (
value: number,
decimals?: DecimalCount,
scaledDecimals?: DecimalCount,
timeZone?: TimeZone
timeZone?: TimeZone,
showMs?: boolean
) => FormattedValue;
export interface ValueFormat {
@@ -213,9 +213,9 @@ export const Components = {
items: 'Search items',
},
DashboardLinks: {
container: 'Dashboard link container',
dropDown: 'Dashboard link dropdown',
link: 'Dashboard link',
container: 'data-testid Dashboard link container',
dropDown: 'data-testid Dashboard link dropdown',
link: 'data-testid Dashboard link',
},
LoadingIndicator: {
icon: 'Loading indicator',
@@ -50,7 +50,7 @@ export const jestConfig = (baseDir: string = process.cwd()) => {
const setupFile = getSetupFile(setupFilePath);
const shimsFile = getSetupFile(shimsFilePath);
const setupFiles = [setupFile, shimsFile, 'jest-canvas-mock'].filter((f) => f);
const setupFiles = [setupFile, shimsFile, `${__dirname}/matchMedia.js`, 'jest-canvas-mock'].filter((f) => f);
const defaultJestConfig = {
preset: 'ts-jest',
verbose: false,
@@ -0,0 +1,14 @@
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(global, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
@@ -52,7 +52,7 @@ export const TimeRangeOption = memo<Props>(({ value, onSelect, selected = false,
const id = uuidv4();
return (
<li className={cx(styles.container, selected && styles.selected)}>
<li onClick={() => onSelect(value)} className={cx(styles.container, selected && styles.selected)}>
<input
className={styles.radio}
checked={selected}
@@ -21,3 +21,7 @@ export default {
export const Basic: Story<FileDropzoneProps> = (args) => {
return <FileDropzone {...args} />;
};
export const WithCustomFileList: Story<FileDropzoneProps> = () => {
return <FileDropzone fileListRenderer={(file) => <div>Custom rendered item {file.file.name}</div>} />;
};
@@ -107,6 +107,17 @@ describe('The FileDropzone component', () => {
screen.getByText('Custom dropzone text');
});
it('should handle file list overwrite when fileListRenderer is passed', async () => {
render(<FileDropzone fileListRenderer={() => null} />);
dispatchEvt(screen.getByTestId('dropzone'), 'drop', mockData([file({})]));
// need to await this in order to have the drop finished
await screen.findByTestId('dropzone');
expect(screen.queryByText('ping.json')).not.toBeInTheDocument();
});
});
function dispatchEvt(node: HTMLElement, type: string, data: any) {
@@ -30,6 +30,11 @@ export interface FileDropzoneProps {
* Use the onLoad function to get the result from FileReader.
*/
onLoad?: (result: string | ArrayBuffer | null) => void;
/**
* The fileListRenderer property can be used to overwrite the list of files. To not to show
* any list return null in the function.
*/
fileListRenderer?: (file: DropzoneFile, removeFile: (file: DropzoneFile) => void) => ReactNode;
}
export interface DropzoneFile {
@@ -41,7 +46,7 @@ export interface DropzoneFile {
retryUpload?: () => void;
}
export function FileDropzone({ options, children, readAs, onLoad }: FileDropzoneProps) {
export function FileDropzone({ options, children, readAs, onLoad, fileListRenderer }: FileDropzoneProps) {
const [files, setFiles] = useState<DropzoneFile[]>([]);
const setFileProperty = useCallback(
@@ -133,7 +138,12 @@ export function FileDropzone({ options, children, readAs, onLoad }: FileDropzone
const { getRootProps, getInputProps, isDragActive } = useDropzone({ ...options, onDrop });
const theme = useTheme2();
const styles = getStyles(theme, isDragActive);
const fileList = files.map((file) => <FileListItem key={file.id} file={file} removeFile={removeFile} />);
const fileList = files.map((file) => {
if (fileListRenderer) {
return fileListRenderer(file, removeFile);
}
return <FileListItem key={file.id} file={file} removeFile={removeFile} />;
});
return (
<div className={styles.container}>
@@ -30,7 +30,7 @@ describe('The FileListItem component', () => {
it('should show a progressbar when the progress prop has a value', () => {
render(<FileListItem file={{ file: file({}), id: '1', error: null, progress: 6 }} />);
expect(screen.queryByText('Cancel')).not.toBeInTheDocument();
expect(screen.queryByText('Cancel upload')).not.toBeInTheDocument();
expect(screen.getByText('46%')).toBeInTheDocument();
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
@@ -45,7 +45,7 @@ describe('The FileListItem component', () => {
const abortUpload = jest.fn();
render(<FileListItem file={{ file: file({}), id: '1', error: null, progress: 6, abortUpload }} />);
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
expect(abortUpload).toBeCalledTimes(1);
});
@@ -24,11 +24,19 @@ export function FileListItem({ file: customFile, removeFile }: FileListItemProps
<>
<span className={styles.error}>{error.message}</span>
{retryUpload && (
<IconButton aria-label="Retry" name="sync" tooltip="Retry" tooltipPlacement="top" onClick={retryUpload} />
<IconButton
type="button"
aria-label="Retry"
name="sync"
tooltip="Retry"
tooltipPlacement="top"
onClick={retryUpload}
/>
)}
{removeFile && (
<IconButton
className={retryUpload ? styles.marginLeft : ''}
type="button"
name="trash-alt"
onClick={() => removeFile(customFile)}
tooltip={REMOVE_FILE}
@@ -46,7 +54,7 @@ export function FileListItem({ file: customFile, removeFile }: FileListItemProps
<span className={styles.paddingLeft}>{Math.round((progress / file.size) * 100)}%</span>
{abortUpload && (
<Button variant="secondary" type="button" fill="text" onClick={abortUpload}>
Cancel
Cancel upload
</Button>
)}
</>
@@ -59,6 +67,7 @@ export function FileListItem({ file: customFile, removeFile }: FileListItemProps
onClick={() => removeFile(customFile)}
tooltip={REMOVE_FILE}
aria-label={REMOVE_FILE}
type="button"
tooltipPlacement="top"
/>
)
@@ -70,7 +79,7 @@ export function FileListItem({ file: customFile, removeFile }: FileListItemProps
return (
<div className={styles.fileListContainer}>
<span className={styles.fileNameWrapper}>
<Icon name="file-blank" size="lg" />
<Icon name="file-blank" size="lg" aria-hidden={true} />
<span className={styles.padding}>{trimFileName(file.name)}</span>
<span>{formattedValueToString(valueFormat)}</span>
</span>
@@ -45,11 +45,11 @@ function passThroughFieldOverrides(frame: DataFrame) {
const aSeries = passThroughFieldOverrides(
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [10000, 20000, 30000] },
{ name: 'time', type: FieldType.time, values: [10000, 20000, 30000, 80000] },
{
name: 'value',
type: FieldType.number,
values: [10, 20, 10],
values: [10, 20, 10, 25],
config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'red' } },
},
],
@@ -59,11 +59,11 @@ const aSeries = passThroughFieldOverrides(
const bSeries = passThroughFieldOverrides(
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [10000, 20000, 30000] },
{ name: 'time', type: FieldType.time, values: [10000, 20000, 30000, 80000] },
{
name: 'value',
type: FieldType.number,
values: [30, 60, 30],
values: [30, 60, 30, 40],
config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'blue' } },
},
],
@@ -74,11 +74,11 @@ const bSeries = passThroughFieldOverrides(
const cSeries = passThroughFieldOverrides(
toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [10000, 30000] },
{ name: 'time', type: FieldType.time, values: [10000, 30000, 80000] },
{
name: 'value',
type: FieldType.number,
values: [30, 30],
values: [30, 30, 30],
config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'yellow' } },
},
],
@@ -4,7 +4,7 @@ import { css, cx } from '@emotion/css';
import { Alert } from '../Alert/Alert';
import { LogRowContextRows, LogRowContextQueryErrors, HasMoreContextRows } from './LogRowContextProvider';
import { useStyles } from '../../themes/ThemeContext';
import { useStyles, useTheme } from '../../themes/ThemeContext';
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
import { List } from '../List/List';
import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper';
@@ -13,18 +13,41 @@ import { LogMessageAnsi } from './LogMessageAnsi';
interface LogRowContextProps {
row: LogRowModel;
context: LogRowContextRows;
wrapLogMessage: boolean;
errors?: LogRowContextQueryErrors;
hasMoreContextRows?: HasMoreContextRows;
onOutsideClick: () => void;
onLoadMoreContext: () => void;
}
const getLogRowContextStyles = (theme: GrafanaTheme) => {
const getLogRowContextStyles = (theme: GrafanaTheme, wrapLogMessage?: boolean) => {
/**
* This is workaround for displaying uncropped context when we have unwrapping log messages.
* We are using margins to correctly position context. Because non-wrapped logs have always 1 line of log
* and 1 line of Show/Hide context switch. Therefore correct position can be reliably achieved by margins.
* We also adjust width to 75%.
*/
const afterContext = wrapLogMessage
? css`
top: -250px;
`
: css`
margin-top: -250px;
width: 75%;
`;
const beforeContext = wrapLogMessage
? css`
top: 100%;
`
: css`
margin-top: 40px;
width: 75%;
`;
return {
commonStyles: css`
position: absolute;
width: calc(100% + 20px);
left: -13px;
height: 250px;
z-index: ${theme.zIndex.dropdown};
overflow: hidden;
@@ -32,6 +55,7 @@ const getLogRowContextStyles = (theme: GrafanaTheme) => {
box-shadow: 0 0 10px ${theme.colors.dropdownShadow};
border: 1px solid ${theme.colors.bg2};
border-radius: ${theme.border.radius.md};
width: 100%;
`,
header: css`
height: 30px;
@@ -44,6 +68,8 @@ const getLogRowContextStyles = (theme: GrafanaTheme) => {
height: 220px;
padding: 10px;
`,
afterContext,
beforeContext,
};
};
@@ -56,7 +82,7 @@ interface LogRowContextGroupHeaderProps {
}
interface LogRowContextGroupProps extends LogRowContextGroupHeaderProps {
rows: Array<string | DataQueryError>;
className: string;
className?: string;
error?: string;
}
@@ -122,11 +148,11 @@ export const LogRowContextGroup: React.FunctionComponent<LogRowContextGroupProps
};
return (
<div className={cx(className, commonStyles)}>
<div className={cx(commonStyles, className)}>
{/* When displaying "after" context */}
{shouldScrollToBottom && !error && <LogRowContextGroupHeader {...headerProps} />}
<div className={logs}>
<CustomScrollbar autoHide scrollTop={scrollTop}>
<CustomScrollbar autoHide scrollTop={scrollTop} autoHeightMin={'210px'}>
<div ref={listContainerRef}>
{!error && (
<List
@@ -161,6 +187,7 @@ export const LogRowContext: React.FunctionComponent<LogRowContextProps> = ({
onOutsideClick,
onLoadMoreContext,
hasMoreContextRows,
wrapLogMessage,
}) => {
useEffect(() => {
const handleEscKeyDown = (e: KeyboardEvent): void => {
@@ -173,6 +200,8 @@ export const LogRowContext: React.FunctionComponent<LogRowContextProps> = ({
document.removeEventListener('keydown', handleEscKeyDown, false);
};
}, [onOutsideClick]);
const theme = useTheme();
const { afterContext, beforeContext } = getLogRowContextStyles(theme, wrapLogMessage);
return (
<ClickOutsideWrapper onClick={onOutsideClick}>
@@ -184,9 +213,7 @@ export const LogRowContext: React.FunctionComponent<LogRowContextProps> = ({
rows={context.after}
error={errors && errors.after}
row={row}
className={css`
top: -250px;
`}
className={afterContext}
shouldScrollToBottom
canLoadMoreRows={hasMoreContextRows ? hasMoreContextRows.after : false}
onLoadMoreContext={onLoadMoreContext}
@@ -200,9 +227,7 @@ export const LogRowContext: React.FunctionComponent<LogRowContextProps> = ({
row={row}
rows={context.before}
error={errors && errors.before}
className={css`
top: 100%;
`}
className={beforeContext}
/>
)}
</div>
@@ -51,6 +51,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
label: verticalScroll;
white-space: pre;
`,
contextNewline: css`
display: block;
margin-left: 0px;
`,
};
});
@@ -124,12 +128,15 @@ class UnThemedLogRowMessage extends PureComponent<Props> {
return (
<td className={style.logsRowMessage}>
<div className={cx(styles.positionRelative, { [styles.horizontalScroll]: !wrapLogMessage })}>
<div
className={cx({ [styles.positionRelative]: wrapLogMessage }, { [styles.horizontalScroll]: !wrapLogMessage })}
>
{contextIsOpen && context && (
<LogRowContext
row={row}
context={context}
errors={errors}
wrapLogMessage={wrapLogMessage}
hasMoreContextRows={hasMoreContextRows}
onOutsideClick={onToggleContext}
onLoadMoreContext={() => {
@@ -143,7 +150,10 @@ class UnThemedLogRowMessage extends PureComponent<Props> {
{renderLogMessage(hasAnsi, restructuredEntry, highlights, highlightClassName)}
</span>
{showContextToggle?.(row) && (
<span onClick={this.onContextToggle} className={cx('log-row-context', style.context)}>
<span
onClick={this.onContextToggle}
className={cx('log-row-context', style.context, { [styles.contextNewline]: !wrapLogMessage })}
>
{contextIsOpen ? 'Hide' : 'Show'} context
</span>
)}
@@ -48,13 +48,13 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
font-family: ${theme.typography.fontFamily.monospace};
font-size: ${theme.typography.size.sm};
width: 100%;
overflow: hidden;
`,
context: css`
label: context;
visibility: hidden;
white-space: nowrap;
position: relative;
margin-left: 10px;
`,
logsRow: css`
label: logs-row;
@@ -66,7 +66,6 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
.log-row-context {
visibility: visible;
z-index: 1;
margin-left: 10px;
text-decoration: underline;
&:hover {
color: ${theme.palette.yellow};
@@ -50,9 +50,12 @@ AxisConfig: {
HideSeriesConfig: {
tooltip: bool
legend: bool
graph: bool
viz: bool
} @cuetsy(targetType="interface")
// TODO This is the same composition as what's used in the timeseries panel's
// PanelFieldConfig. If that's the only place it's used, it probably shouldn't
// be assembled here, too
GraphFieldConfig: LineConfig & FillConfig & PointsConfig & AxisConfig & {
drawStyle?: DrawStyle
gradientMode?: GraphGradientMode
+1 -1
View File
@@ -118,7 +118,7 @@ func (a *CacheServer) Handler(ctx *models.ReqContext) {
if err := avatar.Encode(ctx.Resp); err != nil {
log.Warnf("avatar encode error: %v", err)
ctx.WriteHeader(500)
ctx.Resp.WriteHeader(500)
}
}
+1 -4
View File
@@ -217,10 +217,7 @@ func setupScenarioContext(t *testing.T, url string) *scenarioContext {
require.Truef(t, exists, "Views should be in %q", viewsPath)
sc.m = macaron.New()
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.UseMiddleware(macaron.Renderer(viewsPath, "[[", "]]"))
sc.m.Use(getContextHandler(t, cfg).Middleware)
return sc
+1 -5
View File
@@ -58,11 +58,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*macaron.Macaron, *HT
m := macaron.New()
m.Use(getContextHandler(t, cfg).Middleware)
m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: filepath.Join(setting.StaticRootPath, "views"),
IndentJSON: true,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
m.UseMiddleware(macaron.Renderer(filepath.Join(setting.StaticRootPath, "views"), "[[", "]]"))
m.Get("/api/frontend/settings/", hs.GetFrontendSettings)
return m, hs
+4 -8
View File
@@ -335,7 +335,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m.Use(middleware.Logger(hs.Cfg))
if hs.Cfg.EnableGzip {
m.Use(middleware.Gziper())
m.UseMiddleware(middleware.Gziper())
}
m.Use(middleware.Recovery(hs.Cfg))
@@ -354,11 +354,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m.SetURLPrefix(hs.Cfg.AppSubURL)
}
m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: filepath.Join(hs.Cfg.StaticRootPath, "views"),
IndentJSON: macaron.Env != macaron.PROD,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
m.UseMiddleware(macaron.Renderer(filepath.Join(hs.Cfg.StaticRootPath, "views"), "[[", "]]"))
// These endpoints are used for monitoring the Grafana instance
// and should not be redirected or rejected.
@@ -375,7 +371,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
}
m.Use(middleware.HandleNoCacheHeader)
m.Use(middleware.AddCSPHeader(hs.Cfg, hs.log))
m.UseMiddleware(middleware.AddCSPHeader(hs.Cfg, hs.log))
for _, mw := range hs.middlewares {
m.Use(mw)
@@ -408,7 +404,7 @@ func (hs *HTTPServer) healthzHandler(ctx *macaron.Context) {
return
}
ctx.WriteHeader(200)
ctx.Resp.WriteHeader(200)
_, err := ctx.Resp.Write([]byte("Ok"))
if err != nil {
hs.log.Error("could not write to response", "err", err)
+18 -9
View File
@@ -21,6 +21,14 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
var permittedFileExts = []string{
".html", ".xhtml", ".css", ".js", ".json", ".jsonld", ".map", ".mjs",
".jpeg", ".jpg", ".png", ".gif", ".svg", ".webp", ".ico",
".woff", ".woff2", ".eot", ".ttf", ".otf",
".wav", ".mp3",
".md", ".pdf", ".txt",
}
func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
typeFilter := c.Query("type")
enabledFilter := c.Query("enabled")
@@ -292,9 +300,9 @@ func (hs *HTTPServer) GetPluginAssets(c *models.ReqContext) {
return
}
if shouldExclude(fi) {
if accessForbidden(fi.Name()) {
c.JsonApiErr(403, "Plugin file access forbidden",
fmt.Errorf("access is forbidden to executable plugin file %s", pluginFilePath))
fmt.Errorf("access is forbidden to plugin file %s", pluginFilePath))
return
}
@@ -441,12 +449,13 @@ func translatePluginRequestErrorToAPIError(err error) response.Response {
return response.Error(500, "Plugin request failed", err)
}
func shouldExclude(fi os.FileInfo) bool {
normalizedFilename := strings.ToLower(fi.Name())
func accessForbidden(pluginFilename string) bool {
ext := filepath.Ext(pluginFilename)
isUnixExecutable := fi.Mode()&0111 == 0111
isWindowsExecutable := strings.HasSuffix(normalizedFilename, ".exe")
isScript := strings.HasSuffix(normalizedFilename, ".sh")
return isUnixExecutable || isWindowsExecutable || isScript
for _, permittedExt := range permittedFileExts {
if strings.EqualFold(permittedExt, ext) {
return false
}
}
return true
}
+89
View File
@@ -0,0 +1,89 @@
package api
import (
"testing"
)
func Test_accessForbidden(t *testing.T) {
type testCase struct {
filename string
}
tests := []struct {
name string
t testCase
accessForbidden bool
}{
{
name: ".exe files are forbidden",
t: testCase{
filename: "test.exe",
},
accessForbidden: true,
},
{
name: ".sh files are forbidden",
t: testCase{
filename: "test.sh",
},
accessForbidden: true,
},
{
name: "js is not forbidden",
t: testCase{
filename: "module.js",
},
accessForbidden: false,
},
{
name: "logos are not forbidden",
t: testCase{
filename: "logo.svg",
},
accessForbidden: false,
},
{
name: "JPGs are not forbidden",
t: testCase{
filename: "img/test.jpg",
},
accessForbidden: false,
},
{
name: "JPEGs are not forbidden",
t: testCase{
filename: "img/test.jpeg",
},
accessForbidden: false,
},
{
name: "ext case is ignored",
t: testCase{
filename: "scripts/runThis.SH",
},
accessForbidden: true,
},
{
name: "no file ext is forbidden",
t: testCase{
filename: "scripts/runThis",
},
accessForbidden: true,
},
{
name: "empty file ext is forbidden",
t: testCase{
filename: "scripts/runThis.",
},
accessForbidden: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := accessForbidden(tt.t.filename); got != tt.accessForbidden {
t.Errorf("accessForbidden() = %v, accessForbidden %v", got, tt.accessForbidden)
}
})
}
}
+2 -2
View File
@@ -186,7 +186,7 @@ func (s *Service) buildDSNode(dp *simple.DirectedGraph, rn *rawNode, req *Reques
}
var floatIntervalMS float64
if rawIntervalMS := rn.Query["intervalMs"]; ok {
if rawIntervalMS, ok := rn.Query["intervalMs"]; ok {
if floatIntervalMS, ok = rawIntervalMS.(float64); !ok {
return nil, fmt.Errorf("expected intervalMs to be an float64, got type %T for refId %v", rawIntervalMS, rn.RefID)
}
@@ -194,7 +194,7 @@ func (s *Service) buildDSNode(dp *simple.DirectedGraph, rn *rawNode, req *Reques
}
var floatMaxDP float64
if rawMaxDP := rn.Query["maxDataPoints"]; ok {
if rawMaxDP, ok := rn.Query["maxDataPoints"]; ok {
if floatMaxDP, ok = rawMaxDP.(float64); !ok {
return nil, fmt.Errorf("expected maxDataPoints to be an float64, got type %T for refId %v", rawMaxDP, rn.RefID)
}
+30 -16
View File
@@ -15,6 +15,8 @@
package macaron
import (
"encoding/json"
"html/template"
"net/http"
"net/url"
"reflect"
@@ -44,11 +46,11 @@ type Context struct {
index int
*Router
Req Request
Resp ResponseWriter
params Params
Render
Data map[string]interface{}
Req Request
Resp ResponseWriter
params Params
template *template.Template
Data map[string]interface{}
}
func (ctx *Context) handler() Handler {
@@ -108,19 +110,31 @@ func (ctx *Context) RemoteAddr() string {
return addr
}
func (ctx *Context) renderHTML(status int, setName, tplName string, data ...interface{}) {
if len(data) <= 0 {
ctx.Render.HTMLSet(status, setName, tplName, ctx.Data)
} else if len(data) == 1 {
ctx.Render.HTMLSet(status, setName, tplName, data[0])
} else {
ctx.Render.HTMLSet(status, setName, tplName, data[0], data[1].(HTMLOptions))
const (
headerContentType = "Content-Type"
contentTypeJSON = "application/json; charset=UTF-8"
contentTypeHTML = "text/html; charset=UTF-8"
)
// HTML renders the HTML with default template set.
func (ctx *Context) HTML(status int, name string, data interface{}) {
ctx.Resp.Header().Set(headerContentType, contentTypeHTML)
ctx.Resp.WriteHeader(status)
if err := ctx.template.ExecuteTemplate(ctx.Resp, name, data); err != nil {
panic("Context.HTML:" + err.Error())
}
}
// HTML renders the HTML with default template set.
func (ctx *Context) HTML(status int, name string, data ...interface{}) {
ctx.renderHTML(status, DEFAULT_TPL_SET_NAME, name, data...)
func (ctx *Context) JSON(status int, data interface{}) {
ctx.Resp.Header().Set(headerContentType, contentTypeJSON)
ctx.Resp.WriteHeader(status)
enc := json.NewEncoder(ctx.Resp)
if Env != PROD {
enc.SetIndent("", " ")
}
if err := enc.Encode(data); err != nil {
panic("Context.JSON: " + err.Error())
}
}
// Redirect sends a redirect response
@@ -142,7 +156,7 @@ func (ctx *Context) parseForm() {
return
}
contentType := ctx.Req.Header.Get(_CONTENT_TYPE)
contentType := ctx.Req.Header.Get(headerContentType)
if (ctx.Req.Method == "POST" || ctx.Req.Method == "PUT") &&
len(contentType) > 0 && strings.Contains(contentType, "multipart/form-data") {
_ = ctx.Req.ParseMultipartForm(MaxMemory)
-1
View File
@@ -182,7 +182,6 @@ func (m *Macaron) createContext(rw http.ResponseWriter, req *http.Request) *Cont
index: 0,
Router: m.Router,
Resp: NewResponseWriter(req.Method, rw),
Render: &DummyRender{rw},
Data: make(map[string]interface{}),
}
req = req.WithContext(context.WithValue(req.Context(), macaronContextKey{}, c))
+38 -689
View File
@@ -16,708 +16,57 @@
package macaron
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"html/template"
"io"
"io/ioutil"
"io/fs"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
)
const (
_CONTENT_TYPE = "Content-Type"
_CONTENT_BINARY = "application/octet-stream"
_CONTENT_JSON = "application/json"
_CONTENT_HTML = "text/html"
_CONTENT_PLAIN = "text/plain"
_CONTENT_XHTML = "application/xhtml+xml"
_CONTENT_XML = "text/xml"
_DEFAULT_CHARSET = "UTF-8"
)
var (
// Provides a temporary buffer to execute templates into and catch errors.
bufpool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
// Renderer is a Middleware that injects a template renderer into the macaron context, enabling ctx.HTML calls in the handlers.
// If MACARON_ENV is set to "development" then templates will be recompiled on every request. For more performance, set the
// MACARON_ENV environment variable to "production".
func Renderer(dir, leftDelim, rightDelim string) func(http.Handler) http.Handler {
fs := os.DirFS(dir)
t, err := compileTemplates(fs, leftDelim, rightDelim)
if err != nil {
panic("Renderer: " + err.Error())
}
// Included helper functions for use when rendering html
helperFuncs = template.FuncMap{
"yield": func() (string, error) {
return "", fmt.Errorf("yield called with no layout defined")
},
"current": func() (string, error) {
return "", nil
},
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := FromContext(req.Context())
if Env == DEV {
if t, err = compileTemplates(fs, leftDelim, rightDelim); err != nil {
panic("Context.HTML:" + err.Error())
}
}
ctx.template = t
next.ServeHTTP(rw, req)
})
}
)
type (
// TemplateFile represents a interface of template file that has name and can be read.
TemplateFile interface {
Name() string
Data() []byte
Ext() string
}
// TemplateFileSystem represents a interface of template file system that able to list all files.
TemplateFileSystem interface {
ListFiles() []TemplateFile
Get(string) (io.Reader, error)
}
// Delims represents a set of Left and Right delimiters for HTML template rendering
Delims struct {
// Left delimiter, defaults to {{
Left string
// Right delimiter, defaults to }}
Right string
}
// RenderOptions represents a struct for specifying configuration options for the Render middleware.
RenderOptions struct {
// Directory to load templates. Default is "templates".
Directory string
// Addtional directories to overwite templates.
AppendDirectories []string
// Layout template name. Will not render a layout if "". Default is to "".
Layout string
// Extensions to parse template files from. Defaults are [".tmpl", ".html"].
Extensions []string
// Funcs is a slice of FuncMaps to apply to the template upon compilation. This is useful for helper functions. Default is [].
Funcs []template.FuncMap
// Delims sets the action delimiters to the specified strings in the Delims struct.
Delims Delims
// Appends the given charset to the Content-Type header. Default is "UTF-8".
Charset string
// Outputs human readable JSON.
IndentJSON bool
// Outputs human readable XML.
IndentXML bool
// Prefixes the JSON output with the given bytes.
PrefixJSON []byte
// Prefixes the XML output with the given bytes.
PrefixXML []byte
// Allows changing of output to XHTML instead of HTML. Default is "text/html"
HTMLContentType string
// TemplateFileSystem is the interface for supporting any implmentation of template file system.
TemplateFileSystem
}
// HTMLOptions is a struct for overriding some rendering Options for specific HTML call
HTMLOptions struct {
// Layout template name. Overrides Options.Layout.
Layout string
}
Render interface {
http.ResponseWriter
SetResponseWriter(http.ResponseWriter)
JSON(int, interface{})
JSONString(interface{}) (string, error)
RawData(int, []byte) // Serve content as binary
PlainText(int, []byte) // Serve content as plain text
HTML(int, string, interface{}, ...HTMLOptions)
HTMLSet(int, string, string, interface{}, ...HTMLOptions)
HTMLSetString(string, string, interface{}, ...HTMLOptions) (string, error)
HTMLString(string, interface{}, ...HTMLOptions) (string, error)
HTMLSetBytes(string, string, interface{}, ...HTMLOptions) ([]byte, error)
HTMLBytes(string, interface{}, ...HTMLOptions) ([]byte, error)
XML(int, interface{})
Error(int, ...string)
Status(int)
SetTemplatePath(string, string)
HasTemplateSet(string) bool
}
)
// TplFile implements TemplateFile interface.
type TplFile struct {
name string
data []byte
ext string
}
// NewTplFile cerates new template file with given name and data.
func NewTplFile(name string, data []byte, ext string) *TplFile {
return &TplFile{name, data, ext}
}
func (f *TplFile) Name() string {
return f.name
}
func (f *TplFile) Data() []byte {
return f.data
}
func (f *TplFile) Ext() string {
return f.ext
}
// TplFileSystem implements TemplateFileSystem interface.
type TplFileSystem struct {
files []TemplateFile
}
// NewTemplateFileSystem creates new template file system with given options.
func NewTemplateFileSystem(opt RenderOptions, omitData bool) TplFileSystem {
fs := TplFileSystem{}
fs.files = make([]TemplateFile, 0, 10)
// Directories are composed in reverse order because later one overwrites previous ones,
// so once found, we can directly jump out of the loop.
dirs := make([]string, 0, len(opt.AppendDirectories)+1)
for i := len(opt.AppendDirectories) - 1; i >= 0; i-- {
dirs = append(dirs, opt.AppendDirectories[i])
}
dirs = append(dirs, opt.Directory)
var err error
for i := range dirs {
// Skip ones that does not exists for symlink test,
// but allow non-symlink ones added after start.
if _, err := os.Stat(dirs[i]); err != nil && os.IsNotExist(err) {
continue
func compileTemplates(filesystem fs.FS, leftDelim, rightDelim string) (*template.Template, error) {
t := template.New("")
t.Delims(leftDelim, rightDelim)
err := fs.WalkDir(filesystem, ".", func(path string, d fs.DirEntry, e error) error {
if e != nil {
return nil // skip unreadable or erroneous filesystem items
}
dirs[i], err = filepath.EvalSymlinks(dirs[i])
if err != nil {
panic("EvalSymlinks(" + dirs[i] + "): " + err.Error())
if d.IsDir() {
return nil
}
}
lastDir := dirs[len(dirs)-1]
// We still walk the last (original) directory because it's non-sense we load templates not exist in original directory.
if err = filepath.Walk(lastDir, func(path string, info os.FileInfo, _ error) error {
r, err := filepath.Rel(lastDir, path)
ext := filepath.Ext(path)
if ext != ".html" && ext != ".tmpl" {
return nil
}
data, err := fs.ReadFile(filesystem, path)
if err != nil {
return err
}
ext := GetExt(r)
for _, extension := range opt.Extensions {
if ext != extension {
continue
}
var data []byte
if !omitData {
// Loop over candidates of directory, break out once found.
// The file always exists because it's inside the walk function,
// and read original file is the worst case.
for i := range dirs {
path = filepath.Join(dirs[i], r)
if f, err := os.Stat(path); err != nil || f.IsDir() {
continue
}
data, err = ioutil.ReadFile(path)
if err != nil {
return err
}
break
}
}
name := filepath.ToSlash((r[0 : len(r)-len(ext)]))
fs.files = append(fs.files, NewTplFile(name, data, ext))
}
return nil
}); err != nil {
panic("NewTemplateFileSystem: " + err.Error())
}
return fs
}
func (fs TplFileSystem) ListFiles() []TemplateFile {
return fs.files
}
func (fs TplFileSystem) Get(name string) (io.Reader, error) {
for i := range fs.files {
if fs.files[i].Name()+fs.files[i].Ext() == name {
return bytes.NewReader(fs.files[i].Data()), nil
}
}
return nil, fmt.Errorf("file '%s' not found", name)
}
func PrepareCharset(charset string) string {
if len(charset) != 0 {
return "; charset=" + charset
}
return "; charset=" + _DEFAULT_CHARSET
}
func GetExt(s string) string {
index := strings.Index(s, ".")
if index == -1 {
return ""
}
return s[index:]
}
func compile(opt RenderOptions) *template.Template {
t := template.New(opt.Directory)
t.Delims(opt.Delims.Left, opt.Delims.Right)
// Parse an initial template in case we don't have any.
template.Must(t.Parse("Macaron"))
if opt.TemplateFileSystem == nil {
opt.TemplateFileSystem = NewTemplateFileSystem(opt, false)
}
for _, f := range opt.TemplateFileSystem.ListFiles() {
tmpl := t.New(f.Name())
for _, funcs := range opt.Funcs {
tmpl.Funcs(funcs)
}
// Bomb out if parse fails. We don't want any silent server starts.
template.Must(tmpl.Funcs(helperFuncs).Parse(string(f.Data())))
}
return t
}
const (
DEFAULT_TPL_SET_NAME = "DEFAULT"
)
// TemplateSet represents a template set of type *template.Template.
type TemplateSet struct {
lock sync.RWMutex
sets map[string]*template.Template
dirs map[string]string
}
// NewTemplateSet initializes a new empty template set.
func NewTemplateSet() *TemplateSet {
return &TemplateSet{
sets: make(map[string]*template.Template),
dirs: make(map[string]string),
}
}
func (ts *TemplateSet) Set(name string, opt *RenderOptions) *template.Template {
t := compile(*opt)
ts.lock.Lock()
defer ts.lock.Unlock()
ts.sets[name] = t
ts.dirs[name] = opt.Directory
return t
}
func (ts *TemplateSet) Get(name string) *template.Template {
ts.lock.RLock()
defer ts.lock.RUnlock()
return ts.sets[name]
}
func (ts *TemplateSet) GetDir(name string) string {
ts.lock.RLock()
defer ts.lock.RUnlock()
return ts.dirs[name]
}
func prepareRenderOptions(options []RenderOptions) RenderOptions {
var opt RenderOptions
if len(options) > 0 {
opt = options[0]
}
// Defaults.
if len(opt.Directory) == 0 {
opt.Directory = "templates"
}
if len(opt.Extensions) == 0 {
opt.Extensions = []string{".tmpl", ".html"}
}
if len(opt.HTMLContentType) == 0 {
opt.HTMLContentType = _CONTENT_HTML
}
return opt
}
func ParseTplSet(tplSet string) (tplName string, tplDir string) {
tplSet = strings.TrimSpace(tplSet)
if len(tplSet) == 0 {
panic("empty template set argument")
}
infos := strings.Split(tplSet, ":")
if len(infos) == 1 {
tplDir = infos[0]
tplName = path.Base(tplDir)
} else {
tplName = infos[0]
tplDir = infos[1]
}
dir, err := os.Stat(tplDir)
if err != nil || !dir.IsDir() {
panic("template set path does not exist or is not a directory")
}
return tplName, tplDir
}
func renderHandler(opt RenderOptions, tplSets []string) Handler {
cs := PrepareCharset(opt.Charset)
ts := NewTemplateSet()
ts.Set(DEFAULT_TPL_SET_NAME, &opt)
var tmpOpt RenderOptions
for _, tplSet := range tplSets {
tplName, tplDir := ParseTplSet(tplSet)
tmpOpt = opt
tmpOpt.Directory = tplDir
ts.Set(tplName, &tmpOpt)
}
return func(ctx *Context) {
r := &TplRender{
ResponseWriter: ctx.Resp,
TemplateSet: ts,
Opt: &opt,
CompiledCharset: cs,
}
ctx.Data["TmplLoadTimes"] = func() string {
if r.startTime.IsZero() {
return ""
}
return fmt.Sprint(time.Since(r.startTime).Nanoseconds()/1e6) + "ms"
}
ctx.Render = r
ctx.MapTo(r, (*Render)(nil))
}
}
// Renderer is a Middleware that maps a macaron.Render service into the Macaron handler chain.
// An single variadic macaron.RenderOptions struct can be optionally provided to configure
// HTML rendering. The default directory for templates is "templates" and the default
// file extension is ".tmpl" and ".html".
//
// If MACARON_ENV is set to "" or "development" then templates will be recompiled on every request. For more performance, set the
// MACARON_ENV environment variable to "production".
func Renderer(options ...RenderOptions) Handler {
return renderHandler(prepareRenderOptions(options), []string{})
}
func Renderers(options RenderOptions, tplSets ...string) Handler {
return renderHandler(prepareRenderOptions([]RenderOptions{options}), tplSets)
}
type TplRender struct {
http.ResponseWriter
*TemplateSet
Opt *RenderOptions
CompiledCharset string
startTime time.Time
}
func (r *TplRender) SetResponseWriter(rw http.ResponseWriter) {
r.ResponseWriter = rw
}
func (r *TplRender) JSON(status int, v interface{}) {
var (
result []byte
err error
)
if r.Opt.IndentJSON {
result, err = json.MarshalIndent(v, "", " ")
} else {
result, err = json.Marshal(v)
}
if err != nil {
http.Error(r, err.Error(), 500)
return
}
// json rendered fine, write out the result
r.Header().Set(_CONTENT_TYPE, _CONTENT_JSON+r.CompiledCharset)
r.WriteHeader(status)
if len(r.Opt.PrefixJSON) > 0 {
_, _ = r.Write(r.Opt.PrefixJSON)
}
_, _ = r.Write(result)
}
func (r *TplRender) JSONString(v interface{}) (string, error) {
var result []byte
var err error
if r.Opt.IndentJSON {
result, err = json.MarshalIndent(v, "", " ")
} else {
result, err = json.Marshal(v)
}
if err != nil {
return "", err
}
return string(result), nil
}
func (r *TplRender) XML(status int, v interface{}) {
var result []byte
var err error
if r.Opt.IndentXML {
result, err = xml.MarshalIndent(v, "", " ")
} else {
result, err = xml.Marshal(v)
}
if err != nil {
http.Error(r, err.Error(), 500)
return
}
// XML rendered fine, write out the result
r.Header().Set(_CONTENT_TYPE, _CONTENT_XML+r.CompiledCharset)
r.WriteHeader(status)
if len(r.Opt.PrefixXML) > 0 {
_, _ = r.Write(r.Opt.PrefixXML)
}
_, _ = r.Write(result)
}
func (r *TplRender) data(status int, contentType string, v []byte) {
if r.Header().Get(_CONTENT_TYPE) == "" {
r.Header().Set(_CONTENT_TYPE, contentType)
}
r.WriteHeader(status)
_, _ = r.Write(v)
}
func (r *TplRender) RawData(status int, v []byte) {
r.data(status, _CONTENT_BINARY, v)
}
func (r *TplRender) PlainText(status int, v []byte) {
r.data(status, _CONTENT_PLAIN, v)
}
func (r *TplRender) execute(t *template.Template, name string, data interface{}) (*bytes.Buffer, error) {
buf := bufpool.Get().(*bytes.Buffer)
return buf, t.ExecuteTemplate(buf, name, data)
}
func (r *TplRender) addYield(t *template.Template, tplName string, data interface{}) {
funcs := template.FuncMap{
"yield": func() (template.HTML, error) {
buf, err := r.execute(t, tplName, data)
// return safe html here since we are rendering our own template
return template.HTML(buf.String()), err
},
"current": func() (string, error) {
return tplName, nil
},
}
t.Funcs(funcs)
}
func (r *TplRender) renderBytes(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) (*bytes.Buffer, error) {
t := r.TemplateSet.Get(setName)
if Env == DEV {
opt := *r.Opt
opt.Directory = r.TemplateSet.GetDir(setName)
t = r.TemplateSet.Set(setName, &opt)
}
if t == nil {
return nil, fmt.Errorf("html/template: template \"%s\" is undefined", tplName)
}
opt := r.prepareHTMLOptions(htmlOpt)
if len(opt.Layout) > 0 {
r.addYield(t, tplName, data)
tplName = opt.Layout
}
out, err := r.execute(t, tplName, data)
if err != nil {
return nil, err
}
return out, nil
}
func (r *TplRender) renderHTML(status int, setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) {
r.startTime = time.Now()
out, err := r.renderBytes(setName, tplName, data, htmlOpt...)
if err != nil {
http.Error(r, err.Error(), http.StatusInternalServerError)
return
}
r.Header().Set(_CONTENT_TYPE, r.Opt.HTMLContentType+r.CompiledCharset)
r.WriteHeader(status)
if _, err := out.WriteTo(r); err != nil {
out.Reset()
}
bufpool.Put(out)
}
func (r *TplRender) HTML(status int, name string, data interface{}, htmlOpt ...HTMLOptions) {
r.renderHTML(status, DEFAULT_TPL_SET_NAME, name, data, htmlOpt...)
}
func (r *TplRender) HTMLSet(status int, setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) {
r.renderHTML(status, setName, tplName, data, htmlOpt...)
}
func (r *TplRender) HTMLSetBytes(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) ([]byte, error) {
out, err := r.renderBytes(setName, tplName, data, htmlOpt...)
if err != nil {
return []byte(""), err
}
return out.Bytes(), nil
}
func (r *TplRender) HTMLBytes(name string, data interface{}, htmlOpt ...HTMLOptions) ([]byte, error) {
return r.HTMLSetBytes(DEFAULT_TPL_SET_NAME, name, data, htmlOpt...)
}
func (r *TplRender) HTMLSetString(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) (string, error) {
p, err := r.HTMLSetBytes(setName, tplName, data, htmlOpt...)
return string(p), err
}
func (r *TplRender) HTMLString(name string, data interface{}, htmlOpt ...HTMLOptions) (string, error) {
p, err := r.HTMLBytes(name, data, htmlOpt...)
return string(p), err
}
// Error writes the given HTTP status to the current ResponseWriter
func (r *TplRender) Error(status int, message ...string) {
r.WriteHeader(status)
if len(message) > 0 {
_, _ = r.Write([]byte(message[0]))
}
}
func (r *TplRender) Status(status int) {
r.WriteHeader(status)
}
func (r *TplRender) prepareHTMLOptions(htmlOpt []HTMLOptions) HTMLOptions {
if len(htmlOpt) > 0 {
return htmlOpt[0]
}
return HTMLOptions{
Layout: r.Opt.Layout,
}
}
func (r *TplRender) SetTemplatePath(setName, dir string) {
if len(setName) == 0 {
setName = DEFAULT_TPL_SET_NAME
}
opt := *r.Opt
opt.Directory = dir
r.TemplateSet.Set(setName, &opt)
}
func (r *TplRender) HasTemplateSet(name string) bool {
return r.TemplateSet.Get(name) != nil
}
// DummyRender is used when user does not choose any real render to use.
// This way, we can print out friendly message which asks them to register one,
// instead of ugly and confusing 'nil pointer' panic.
type DummyRender struct {
http.ResponseWriter
}
func renderNotRegistered() {
panic("middleware render hasn't been registered")
}
func (r *DummyRender) SetResponseWriter(http.ResponseWriter) {
renderNotRegistered()
}
func (r *DummyRender) JSON(int, interface{}) {
renderNotRegistered()
}
func (r *DummyRender) JSONString(interface{}) (string, error) {
renderNotRegistered()
return "", nil
}
func (r *DummyRender) RawData(int, []byte) {
renderNotRegistered()
}
func (r *DummyRender) PlainText(int, []byte) {
renderNotRegistered()
}
func (r *DummyRender) HTML(int, string, interface{}, ...HTMLOptions) {
renderNotRegistered()
}
func (r *DummyRender) HTMLSet(int, string, string, interface{}, ...HTMLOptions) {
renderNotRegistered()
}
func (r *DummyRender) HTMLSetString(string, string, interface{}, ...HTMLOptions) (string, error) {
renderNotRegistered()
return "", nil
}
func (r *DummyRender) HTMLString(string, interface{}, ...HTMLOptions) (string, error) {
renderNotRegistered()
return "", nil
}
func (r *DummyRender) HTMLSetBytes(string, string, interface{}, ...HTMLOptions) ([]byte, error) {
renderNotRegistered()
return nil, nil
}
func (r *DummyRender) HTMLBytes(string, interface{}, ...HTMLOptions) ([]byte, error) {
renderNotRegistered()
return nil, nil
}
func (r *DummyRender) XML(int, interface{}) {
renderNotRegistered()
}
func (r *DummyRender) Error(int, ...string) {
renderNotRegistered()
}
func (r *DummyRender) Status(int) {
renderNotRegistered()
}
func (r *DummyRender) SetTemplatePath(string, string) {
renderNotRegistered()
}
func (r *DummyRender) HasTemplateSet(string) bool {
renderNotRegistered()
return false
basename := path[:len(path)-len(ext)]
_, err = t.New(basename).Parse(string(data))
return err
})
return t, err
}
+30 -31
View File
@@ -10,45 +10,44 @@ import (
"strings"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/setting"
macaron "gopkg.in/macaron.v1"
)
// AddCSPHeader adds the Content Security Policy header.
func AddCSPHeader(cfg *setting.Cfg, logger log.Logger) macaron.Handler {
return func(w http.ResponseWriter, req *http.Request, c *macaron.Context) {
if !cfg.CSPEnabled {
return
}
func AddCSPHeader(cfg *setting.Cfg, logger log.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if !cfg.CSPEnabled {
next.ServeHTTP(rw, req)
return
}
logger.Debug("Adding CSP header to response", "cfg", fmt.Sprintf("%p", cfg))
logger.Debug("Adding CSP header to response", "cfg", fmt.Sprintf("%p", cfg))
ctx, ok := c.Data["ctx"].(*models.ReqContext)
if !ok {
panic("Failed to convert context into models.ReqContext")
}
ctx := contexthandler.FromContext(req.Context())
if cfg.CSPTemplate == "" {
logger.Debug("CSP template not configured, so returning 500")
ctx.JsonApiErr(500, "CSP template has to be configured", nil)
return
}
if cfg.CSPTemplate == "" {
logger.Debug("CSP template not configured, so returning 500")
ctx.JsonApiErr(500, "CSP template has to be configured", nil)
return
}
var buf [16]byte
if _, err := io.ReadFull(rand.Reader, buf[:]); err != nil {
logger.Error("Failed to generate CSP nonce", "err", err)
ctx.JsonApiErr(500, "Failed to generate CSP nonce", err)
}
var buf [16]byte
if _, err := io.ReadFull(rand.Reader, buf[:]); err != nil {
logger.Error("Failed to generate CSP nonce", "err", err)
ctx.JsonApiErr(500, "Failed to generate CSP nonce", err)
}
nonce := base64.RawStdEncoding.EncodeToString(buf[:])
val := strings.ReplaceAll(cfg.CSPTemplate, "$NONCE", fmt.Sprintf("'nonce-%s'", nonce))
nonce := base64.RawStdEncoding.EncodeToString(buf[:])
val := strings.ReplaceAll(cfg.CSPTemplate, "$NONCE", fmt.Sprintf("'nonce-%s'", nonce))
re := regexp.MustCompile(`^\w+:(//)?`)
rootPath := re.ReplaceAllString(cfg.AppURL, "")
val = strings.ReplaceAll(val, "$ROOT_PATH", rootPath)
w.Header().Set("Content-Security-Policy", val)
ctx.RequestNonce = nonce
logger.Debug("Successfully generated CSP nonce", "nonce", nonce)
re := regexp.MustCompile(`^\w+:(//)?`)
rootPath := re.ReplaceAllString(cfg.AppURL, "")
val = strings.ReplaceAll(val, "$ROOT_PATH", rootPath)
rw.Header().Set("Content-Security-Policy", val)
ctx.RequestNonce = nonce
logger.Debug("Successfully generated CSP nonce", "nonce", nonce)
next.ServeHTTP(rw, req)
})
}
}
+64 -26
View File
@@ -1,43 +1,81 @@
package middleware
import (
"bufio"
"compress/gzip"
"fmt"
"net"
"net/http"
"strings"
"github.com/go-macaron/gzip"
"github.com/grafana/grafana/pkg/infra/log"
"gopkg.in/macaron.v1"
macaron "gopkg.in/macaron.v1"
)
const resourcesPath = "/resources"
var gzipIgnoredPathPrefixes = []string{
"/api/datasources/proxy", // Ignore datasource proxy requests.
"/api/plugin-proxy/",
"/metrics",
"/api/live/ws", // WebSocket does not support gzip compression.
"/api/live/push", // WebSocket does not support gzip compression.
type gzipResponseWriter struct {
w *gzip.Writer
macaron.ResponseWriter
}
func Gziper() macaron.Handler {
gziperLogger := log.New("gziper")
gziper := gzip.Gziper()
func (grw *gzipResponseWriter) WriteHeader(c int) {
grw.Header().Del("Content-Length")
grw.ResponseWriter.WriteHeader(c)
}
return func(ctx *macaron.Context) {
requestPath := ctx.Req.URL.RequestURI()
func (grw gzipResponseWriter) Write(p []byte) (int, error) {
if grw.Header().Get("Content-Type") == "" {
grw.Header().Set("Content-Type", http.DetectContentType(p))
}
grw.Header().Del("Content-Length")
return grw.w.Write(p)
}
for _, pathPrefix := range gzipIgnoredPathPrefixes {
if strings.HasPrefix(requestPath, pathPrefix) {
func (grw gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hijacker, ok := grw.ResponseWriter.(http.Hijacker); ok {
return hijacker.Hijack()
}
return nil, nil, fmt.Errorf("GZIP ResponseWriter doesn't implement the Hijacker interface")
}
type matcher func(s string) bool
func prefix(p string) matcher { return func(s string) bool { return strings.HasPrefix(s, p) } }
func substr(p string) matcher { return func(s string) bool { return strings.Contains(s, p) } }
var gzipIgnoredPaths = []matcher{
prefix("/api/datasources"),
prefix("/api/plugins"),
prefix("/api/plugin-proxy/"),
prefix("/metrics"),
prefix("/api/live/ws"), // WebSocket does not support gzip compression.
prefix("/api/live/push"), // WebSocket does not support gzip compression.
substr("/resources"),
}
func Gziper() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
requestPath := req.URL.RequestURI()
for _, pathMatcher := range gzipIgnoredPaths {
if pathMatcher(requestPath) {
fmt.Println("skip path", requestPath)
next.ServeHTTP(rw, req)
return
}
}
if !strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(rw, req)
return
}
}
// ignore resources
if (strings.HasPrefix(requestPath, "/api/datasources/") || strings.HasPrefix(requestPath, "/api/plugins/")) && strings.Contains(requestPath, resourcesPath) {
return
}
grw := &gzipResponseWriter{gzip.NewWriter(rw), rw.(macaron.ResponseWriter)}
grw.Header().Set("Content-Encoding", "gzip")
grw.Header().Set("Vary", "Accept-Encoding")
if _, err := ctx.Invoke(gziper); err != nil {
gziperLogger.Error("Invoking gzip handler failed", "err", err)
}
next.ServeHTTP(grw, req)
// We can't really handle close errors at this point and we can't report them to the caller
_ = grw.w.Close()
})
}
}
+3 -6
View File
@@ -111,7 +111,7 @@ func TestMiddlewareContext(t *testing.T) {
Settings: map[string]interface{}{},
NavTree: []*dtos.NavLink{},
}
t.Log("Calling HTML", "data", data, "render", c.Render)
t.Log("Calling HTML", "data", data)
c.HTML(200, "index-template", data)
t.Log("Returned HTML with code 200")
}
@@ -632,11 +632,8 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func(
sc.m = macaron.New()
sc.m.Use(AddDefaultResponseHeaders(cfg))
sc.m.Use(AddCSPHeader(cfg, logger))
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.UseMiddleware(AddCSPHeader(cfg, logger))
sc.m.UseMiddleware(macaron.Renderer(viewsPath, "[[", "]]"))
ctxHdlr := getContextHandler(t, cfg)
sc.sqlStore = ctxHdlr.SQLStore
+1 -1
View File
@@ -37,7 +37,7 @@ func OrgRedirect(cfg *setting.Cfg) macaron.Handler {
if ctx.IsApiRequest() {
ctx.JsonApiErr(404, "Not found", nil)
} else {
ctx.Error(404, "Not found")
http.Error(ctx.Resp, "Not found", http.StatusNotFound)
}
return
+1 -4
View File
@@ -32,10 +32,7 @@ func rateLimiterScenario(t *testing.T, desc string, rps int, burst int, fn rateL
cfg := setting.NewCfg()
m := macaron.New()
m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: "",
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
m.UseMiddleware(macaron.Renderer("../../public/views", "[[", "]]"))
m.Use(getContextHandler(t, cfg).Middleware)
m.Get("/foo", RateLimit(rps, burst, func() time.Time { return currentTime }), defaultHandler)
+1 -1
View File
@@ -158,7 +158,7 @@ func Recovery(cfg *setting.Cfg) macaron.Handler {
c.JSON(500, resp)
} else {
c.HTML(500, cfg.ErrTemplateName)
c.HTML(500, cfg.ErrTemplateName, c.Data)
}
}
}()
+1 -4
View File
@@ -65,10 +65,7 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) {
sc.m.Use(Recovery(cfg))
sc.m.Use(AddDefaultResponseHeaders(cfg))
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.UseMiddleware(macaron.Renderer(viewsPath, "[[", "]]"))
sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
sc.remoteCacheService = remotecache.NewFakeStore(t)
+1 -1
View File
@@ -36,7 +36,7 @@ func (ctx *ReqContext) Handle(cfg *setting.Cfg, status int, title string, err er
ctx.Data["AppSubUrl"] = cfg.AppSubURL
ctx.Data["Theme"] = "dark"
ctx.HTML(status, cfg.ErrTemplateName)
ctx.HTML(status, cfg.ErrTemplateName, ctx.Data)
}
func (ctx *ReqContext) IsApiRequest() bool {
@@ -475,7 +475,7 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableAp
n, err = channels.NewDiscordNotifier(cfg, tmpl)
case "googlechat":
n, err = channels.NewGoogleChatNotifier(cfg, tmpl)
case "line":
case "LINE":
n, err = channels.NewLineNotifier(cfg, tmpl)
case "threema":
n, err = channels.NewThreemaNotifier(cfg, tmpl)
@@ -723,7 +723,7 @@ const alertmanagerConfig = `
"grafana_managed_receiver_configs": [
{
"name": "line_test",
"type": "line",
"type": "LINE",
"settings": {},
"secureSettings": {
"token": "mysecrettoken"
@@ -1217,7 +1217,7 @@ var expAlertmanagerConfigFromAPI = `
{
"uid": "",
"name": "line_test",
"type": "line",
"type": "LINE",
"disableResolveMessage": false,
"settings": {},
"secureFields": {
+17 -17
View File
@@ -3,23 +3,23 @@
Frame[0]
Name:
Dimensions: 7 Fields by 10 Rows
+-------------------------------+----------------------+----------------------+----------------------+----------------------+----------------------+----------------------+
| Name: Time | Name: foo | Name: foo | Name: foo | Name: bar | Name: bar | Name: bar |
| Labels: | Labels: state=CA | Labels: state=OR | Labels: state=NV | Labels: state=CA | Labels: state=OR | Labels: state=NV |
| Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []float64 | Type: []float64 | Type: []float64 |
+-------------------------------+----------------------+----------------------+----------------------+----------------------+----------------------+----------------------+
| 2020-01-10 23:00:00 +0000 UTC | 0.7591864109375384 | 0.9882682601465907 | 0 | 0.6508732545187114 | -0.1527280131044415 | 1 |
| 2020-01-10 23:00:01 +0000 UTC | 0.9913874643906261 | 0.7446917214252602 | 0.5458434494486994 | 0.13096142733310906 | -0.6674086005144695 | 0.8378871813639024 |
| 2020-01-10 23:00:02 +0000 UTC | 0.9021552854179972 | 0.25966703475349645 | 0.9147104586490411 | -0.4314114520876468 | -0.9656982091017495 | 0.40410985738788996 |
| 2020-01-10 23:00:03 +0000 UTC | 0.520421234112239 | -0.309548361739801 | 0.9870048864743552 | -0.8539096785287623 | -0.9508837004303972 | -0.16069024262768705 |
| 2020-01-10 23:00:04 +0000 UTC | -0.03004672347354234 | -0.7784002433814474 | 0.7392870259917509 | -0.9995484952759943 | -0.6277683180152559 | -0.6733904463238785 |
| 2020-01-10 23:00:05 +0000 UTC | -0.5707727629931729 | -0.994874810059912 | 0.2518733583799049 | -0.8211080641579032 | -0.10111435263232425 | -0.9677602034277024 |
| 2020-01-10 23:00:06 +0000 UTC | -0.9264396396937303 | -0.8887854574406474 | -0.31720410936455373 | -0.37644334766887727 | 0.4583234781701883 | -0.9483572918485101 |
| 2020-01-10 23:00:07 +0000 UTC | -0.981731033820365 | -0.4945290734844293 | -0.7834358726049309 | 0.19027395311496903 | 0.8691610871861625 | -0.6214726329580013 |
| 2020-01-10 23:00:08 +0000 UTC | -0.7187200578767007 | 0.060066314471906294 | -0.9956576407880757 | 0.6952995601938146 | 0.998194388817009 | -0.09309061359945539 |
| 2020-01-10 23:00:09 +0000 UTC | -0.22268161314765392 | 0.5951866633399958 | -0.8850616758817763 | 0.9748912242737435 | 0.8035874786121313 | 0.46547376927743367 |
+-------------------------------+----------------------+----------------------+----------------------+----------------------+----------------------+----------------------+
+-------------------------------+------------------+------------------+------------------+------------------+------------------+------------------+
| Name: Time | Name: foo | Name: foo | Name: foo | Name: bar | Name: bar | Name: bar |
| Labels: | Labels: state=CA | Labels: state=OR | Labels: state=NV | Labels: state=CA | Labels: state=OR | Labels: state=NV |
| Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []float64 | Type: []float64 | Type: []float64 |
+-------------------------------+------------------+------------------+------------------+------------------+------------------+------------------+
| 2020-01-10 23:00:00 +0000 UTC | 0.7591864109 | 0.9882682601 | 0 | 0.6508732545 | -0.1527280131 | 1 |
| 2020-01-10 23:00:01 +0000 UTC | 0.9913874643 | 0.7446917214 | 0.5458434494 | 0.1309614273 | -0.6674086005 | 0.8378871813 |
| 2020-01-10 23:00:02 +0000 UTC | 0.9021552854 | 0.2596670347 | 0.9147104586 | -0.431411452 | -0.9656982091 | 0.4041098573 |
| 2020-01-10 23:00:03 +0000 UTC | 0.5204212341 | -0.3095483617 | 0.9870048864 | -0.8539096785 | -0.9508837004 | -0.1606902426 |
| 2020-01-10 23:00:04 +0000 UTC | -0.0300467234 | -0.7784002433 | 0.7392870259 | -0.9995484952 | -0.627768318 | -0.6733904463 |
| 2020-01-10 23:00:05 +0000 UTC | -0.5707727629 | -0.99487481 | 0.2518733583 | -0.8211080641 | -0.1011143526 | -0.9677602034 |
| 2020-01-10 23:00:06 +0000 UTC | -0.9264396396 | -0.8887854574 | -0.3172041093 | -0.3764433476 | 0.4583234781 | -0.9483572918 |
| 2020-01-10 23:00:07 +0000 UTC | -0.9817310338 | -0.4945290734 | -0.7834358726 | 0.1902739531 | 0.8691610871 | -0.6214726329 |
| 2020-01-10 23:00:08 +0000 UTC | -0.7187200578 | 0.0600663144 | -0.9956576407 | 0.6952995601 | 0.9981943888 | -0.0930906135 |
| 2020-01-10 23:00:09 +0000 UTC | -0.2226816131 | 0.5951866633 | -0.8850616758 | 0.9748912242 | 0.8035874786 | 0.4654737692 |
+-------------------------------+------------------+------------------+------------------+------------------+------------------+------------------+
====== TEST DATA RESPONSE (arrow base64) ======
FRAME=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
FRAME=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
+102 -102
View File
@@ -3,138 +3,138 @@
Frame[0]
Name: foo
Dimensions: 2 Fields by 10 Rows
+-------------------------------+----------------------+
| Name: Time | Name: Value |
| Labels: | Labels: state=CA |
| Type: []time.Time | Type: []float64 |
+-------------------------------+----------------------+
| 2020-01-10 23:00:00 +0000 UTC | 0.7591864109375384 |
| 2020-01-10 23:00:01 +0000 UTC | 0.9913874643906261 |
| 2020-01-10 23:00:02 +0000 UTC | 0.9021552854179972 |
| 2020-01-10 23:00:03 +0000 UTC | 0.520421234112239 |
| 2020-01-10 23:00:04 +0000 UTC | -0.03004672347354234 |
| 2020-01-10 23:00:05 +0000 UTC | -0.5707727629931729 |
| 2020-01-10 23:00:06 +0000 UTC | -0.9264396396937303 |
| 2020-01-10 23:00:07 +0000 UTC | -0.981731033820365 |
| 2020-01-10 23:00:08 +0000 UTC | -0.7187200578767007 |
| 2020-01-10 23:00:09 +0000 UTC | -0.22268161314765392 |
+-------------------------------+----------------------+
+-------------------------------+------------------+
| Name: Time | Name: Value |
| Labels: | Labels: state=CA |
| Type: []time.Time | Type: []float64 |
+-------------------------------+------------------+
| 2020-01-10 23:00:00 +0000 UTC | 0.7591864109 |
| 2020-01-10 23:00:01 +0000 UTC | 0.9913874643 |
| 2020-01-10 23:00:02 +0000 UTC | 0.9021552854 |
| 2020-01-10 23:00:03 +0000 UTC | 0.5204212341 |
| 2020-01-10 23:00:04 +0000 UTC | -0.0300467234 |
| 2020-01-10 23:00:05 +0000 UTC | -0.5707727629 |
| 2020-01-10 23:00:06 +0000 UTC | -0.9264396396 |
| 2020-01-10 23:00:07 +0000 UTC | -0.9817310338 |
| 2020-01-10 23:00:08 +0000 UTC | -0.7187200578 |
| 2020-01-10 23:00:09 +0000 UTC | -0.2226816131 |
+-------------------------------+------------------+
Frame[1]
Name: foo
Dimensions: 2 Fields by 10 Rows
+-------------------------------+----------------------+
| Name: Time | Name: Value |
| Labels: | Labels: state=OR |
| Type: []time.Time | Type: []float64 |
+-------------------------------+----------------------+
| 2020-01-10 23:00:00 +0000 UTC | 0.9882682601465907 |
| 2020-01-10 23:00:01 +0000 UTC | 0.7446917214252602 |
| 2020-01-10 23:00:02 +0000 UTC | 0.25966703475349645 |
| 2020-01-10 23:00:03 +0000 UTC | -0.309548361739801 |
| 2020-01-10 23:00:04 +0000 UTC | -0.7784002433814474 |
| 2020-01-10 23:00:05 +0000 UTC | -0.994874810059912 |
| 2020-01-10 23:00:06 +0000 UTC | -0.8887854574406474 |
| 2020-01-10 23:00:07 +0000 UTC | -0.4945290734844293 |
| 2020-01-10 23:00:08 +0000 UTC | 0.060066314471906294 |
| 2020-01-10 23:00:09 +0000 UTC | 0.5951866633399958 |
+-------------------------------+----------------------+
+-------------------------------+------------------+
| Name: Time | Name: Value |
| Labels: | Labels: state=OR |
| Type: []time.Time | Type: []float64 |
+-------------------------------+------------------+
| 2020-01-10 23:00:00 +0000 UTC | 0.9882682601 |
| 2020-01-10 23:00:01 +0000 UTC | 0.7446917214 |
| 2020-01-10 23:00:02 +0000 UTC | 0.2596670347 |
| 2020-01-10 23:00:03 +0000 UTC | -0.3095483617 |
| 2020-01-10 23:00:04 +0000 UTC | -0.7784002433 |
| 2020-01-10 23:00:05 +0000 UTC | -0.99487481 |
| 2020-01-10 23:00:06 +0000 UTC | -0.8887854574 |
| 2020-01-10 23:00:07 +0000 UTC | -0.4945290734 |
| 2020-01-10 23:00:08 +0000 UTC | 0.0600663144 |
| 2020-01-10 23:00:09 +0000 UTC | 0.5951866633 |
+-------------------------------+------------------+
Frame[2]
Name: foo
Dimensions: 2 Fields by 10 Rows
+-------------------------------+----------------------+
| Name: Time | Name: Value |
| Labels: | Labels: state=NV |
| Type: []time.Time | Type: []float64 |
+-------------------------------+----------------------+
| 2020-01-10 23:00:00 +0000 UTC | 0 |
| 2020-01-10 23:00:01 +0000 UTC | 0.5458434494486994 |
| 2020-01-10 23:00:02 +0000 UTC | 0.9147104586490411 |
| 2020-01-10 23:00:03 +0000 UTC | 0.9870048864743552 |
| 2020-01-10 23:00:04 +0000 UTC | 0.7392870259917509 |
| 2020-01-10 23:00:05 +0000 UTC | 0.2518733583799049 |
| 2020-01-10 23:00:06 +0000 UTC | -0.31720410936455373 |
| 2020-01-10 23:00:07 +0000 UTC | -0.7834358726049309 |
| 2020-01-10 23:00:08 +0000 UTC | -0.9956576407880757 |
| 2020-01-10 23:00:09 +0000 UTC | -0.8850616758817763 |
+-------------------------------+----------------------+
+-------------------------------+------------------+
| Name: Time | Name: Value |
| Labels: | Labels: state=NV |
| Type: []time.Time | Type: []float64 |
+-------------------------------+------------------+
| 2020-01-10 23:00:00 +0000 UTC | 0 |
| 2020-01-10 23:00:01 +0000 UTC | 0.5458434494 |
| 2020-01-10 23:00:02 +0000 UTC | 0.9147104586 |
| 2020-01-10 23:00:03 +0000 UTC | 0.9870048864 |
| 2020-01-10 23:00:04 +0000 UTC | 0.7392870259 |
| 2020-01-10 23:00:05 +0000 UTC | 0.2518733583 |
| 2020-01-10 23:00:06 +0000 UTC | -0.3172041093 |
| 2020-01-10 23:00:07 +0000 UTC | -0.7834358726 |
| 2020-01-10 23:00:08 +0000 UTC | -0.9956576407 |
| 2020-01-10 23:00:09 +0000 UTC | -0.8850616758 |
+-------------------------------+------------------+
Frame[3]
Name: bar
Dimensions: 2 Fields by 10 Rows
+-------------------------------+----------------------+
| Name: Time | Name: Value |
| Labels: | Labels: state=CA |
| Type: []time.Time | Type: []float64 |
+-------------------------------+----------------------+
| 2020-01-10 23:00:00 +0000 UTC | 0.6508732545187114 |
| 2020-01-10 23:00:01 +0000 UTC | 0.13096142733310906 |
| 2020-01-10 23:00:02 +0000 UTC | -0.4314114520876468 |
| 2020-01-10 23:00:03 +0000 UTC | -0.8539096785287623 |
| 2020-01-10 23:00:04 +0000 UTC | -0.9995484952759943 |
| 2020-01-10 23:00:05 +0000 UTC | -0.8211080641579032 |
| 2020-01-10 23:00:06 +0000 UTC | -0.37644334766887727 |
| 2020-01-10 23:00:07 +0000 UTC | 0.19027395311496903 |
| 2020-01-10 23:00:08 +0000 UTC | 0.6952995601938146 |
| 2020-01-10 23:00:09 +0000 UTC | 0.9748912242737435 |
+-------------------------------+----------------------+
+-------------------------------+------------------+
| Name: Time | Name: Value |
| Labels: | Labels: state=CA |
| Type: []time.Time | Type: []float64 |
+-------------------------------+------------------+
| 2020-01-10 23:00:00 +0000 UTC | 0.6508732545 |
| 2020-01-10 23:00:01 +0000 UTC | 0.1309614273 |
| 2020-01-10 23:00:02 +0000 UTC | -0.431411452 |
| 2020-01-10 23:00:03 +0000 UTC | -0.8539096785 |
| 2020-01-10 23:00:04 +0000 UTC | -0.9995484952 |
| 2020-01-10 23:00:05 +0000 UTC | -0.8211080641 |
| 2020-01-10 23:00:06 +0000 UTC | -0.3764433476 |
| 2020-01-10 23:00:07 +0000 UTC | 0.1902739531 |
| 2020-01-10 23:00:08 +0000 UTC | 0.6952995601 |
| 2020-01-10 23:00:09 +0000 UTC | 0.9748912242 |
+-------------------------------+------------------+
Frame[4]
Name: bar
Dimensions: 2 Fields by 10 Rows
+-------------------------------+----------------------+
| Name: Time | Name: Value |
| Labels: | Labels: state=OR |
| Type: []time.Time | Type: []float64 |
+-------------------------------+----------------------+
| 2020-01-10 23:00:00 +0000 UTC | -0.1527280131044415 |
| 2020-01-10 23:00:01 +0000 UTC | -0.6674086005144695 |
| 2020-01-10 23:00:02 +0000 UTC | -0.9656982091017495 |
| 2020-01-10 23:00:03 +0000 UTC | -0.9508837004303972 |
| 2020-01-10 23:00:04 +0000 UTC | -0.6277683180152559 |
| 2020-01-10 23:00:05 +0000 UTC | -0.10111435263232425 |
| 2020-01-10 23:00:06 +0000 UTC | 0.4583234781701883 |
| 2020-01-10 23:00:07 +0000 UTC | 0.8691610871861625 |
| 2020-01-10 23:00:08 +0000 UTC | 0.998194388817009 |
| 2020-01-10 23:00:09 +0000 UTC | 0.8035874786121313 |
+-------------------------------+----------------------+
+-------------------------------+------------------+
| Name: Time | Name: Value |
| Labels: | Labels: state=OR |
| Type: []time.Time | Type: []float64 |
+-------------------------------+------------------+
| 2020-01-10 23:00:00 +0000 UTC | -0.1527280131 |
| 2020-01-10 23:00:01 +0000 UTC | -0.6674086005 |
| 2020-01-10 23:00:02 +0000 UTC | -0.9656982091 |
| 2020-01-10 23:00:03 +0000 UTC | -0.9508837004 |
| 2020-01-10 23:00:04 +0000 UTC | -0.627768318 |
| 2020-01-10 23:00:05 +0000 UTC | -0.1011143526 |
| 2020-01-10 23:00:06 +0000 UTC | 0.4583234781 |
| 2020-01-10 23:00:07 +0000 UTC | 0.8691610871 |
| 2020-01-10 23:00:08 +0000 UTC | 0.9981943888 |
| 2020-01-10 23:00:09 +0000 UTC | 0.8035874786 |
+-------------------------------+------------------+
Frame[5]
Name: bar
Dimensions: 2 Fields by 10 Rows
+-------------------------------+----------------------+
| Name: Time | Name: Value |
| Labels: | Labels: state=NV |
| Type: []time.Time | Type: []float64 |
+-------------------------------+----------------------+
| 2020-01-10 23:00:00 +0000 UTC | 1 |
| 2020-01-10 23:00:01 +0000 UTC | 0.8378871813639024 |
| 2020-01-10 23:00:02 +0000 UTC | 0.40410985738788996 |
| 2020-01-10 23:00:03 +0000 UTC | -0.16069024262768705 |
| 2020-01-10 23:00:04 +0000 UTC | -0.6733904463238785 |
| 2020-01-10 23:00:05 +0000 UTC | -0.9677602034277024 |
| 2020-01-10 23:00:06 +0000 UTC | -0.9483572918485101 |
| 2020-01-10 23:00:07 +0000 UTC | -0.6214726329580013 |
| 2020-01-10 23:00:08 +0000 UTC | -0.09309061359945539 |
| 2020-01-10 23:00:09 +0000 UTC | 0.46547376927743367 |
+-------------------------------+----------------------+
+-------------------------------+------------------+
| Name: Time | Name: Value |
| Labels: | Labels: state=NV |
| Type: []time.Time | Type: []float64 |
+-------------------------------+------------------+
| 2020-01-10 23:00:00 +0000 UTC | 1 |
| 2020-01-10 23:00:01 +0000 UTC | 0.8378871813 |
| 2020-01-10 23:00:02 +0000 UTC | 0.4041098573 |
| 2020-01-10 23:00:03 +0000 UTC | -0.1606902426 |
| 2020-01-10 23:00:04 +0000 UTC | -0.6733904463 |
| 2020-01-10 23:00:05 +0000 UTC | -0.9677602034 |
| 2020-01-10 23:00:06 +0000 UTC | -0.9483572918 |
| 2020-01-10 23:00:07 +0000 UTC | -0.6214726329 |
| 2020-01-10 23:00:08 +0000 UTC | -0.0930906135 |
| 2020-01-10 23:00:09 +0000 UTC | 0.4654737692 |
+-------------------------------+------------------+
====== TEST DATA RESPONSE (arrow base64) ======
FRAME=QVJST1cxAAD/////mAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJDQSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAoAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAoAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAIAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAABg5zW9qOgVACqCcb2o6BUA9BytvajoFQC+t+i9qOgVAIhSJL6o6BUAUu1fvqjoFQAciJu+qOgVAOYi176o6BUAsL0Sv6joFQB6WE6/qOgVRGvRTEFL6D95Gyc0crnvPz8V2cJ03uw/Kv6Ubkqn4D9o6656kcSev4Qa0D3FQ+K/qXtGvmSl7b9vQXczV2rvv7Xo8TTB/+a/meHxwtSAzL8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAAAwABAAAAqAEAAAAAAADAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJDQSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAwAEAAEFSUk9XMQ==
FRAME=QVJST1cxAAD/////mAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJPUiJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAoAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAoAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAIAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAABg5zW9qOgVACqCcb2o6BUA9BytvajoFQC+t+i9qOgVAIhSJL6o6BUAUu1fvqjoFQAciJu+qOgVAOYi176o6BUAsL0Sv6joFQB6WE6/qOgVRSQgwuSf7z8u86O7g9TnP2Flh3tintA/DfuM7qPP0788spCgp+jovwFKmrID1u+/f8YbM+5w7L8CW2JFXabfv4cFfxADwa4/zPDB5sQL4z8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAAAwABAAAAqAEAAAAAAADAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJPUiJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAwAEAAEFSUk9XMQ==
FRAME=QVJST1cxAAD/////mAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJOViJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAoAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAoAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAIAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAABg5zW9qOgVACqCcb2o6BUA9BytvajoFQC+t+i9qOgVAIhSJL6o6BUAUu1fvqjoFQAciJu+qOgVAOYi176o6BUAsL0Sv6joFQB6WE6/qOgVAAAAAAAAAACaxoOujHfhPwqeJt5ORe0/GsmMRYuV7z/Hu99DPajnP5xsPm+xHtA/CSr4dhJN1L+sPmsb6BHpv0pQpmlt3O+/pV0b3WxS7L8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAAAwABAAAAqAEAAAAAAADAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJOViJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAwAEAAEFSUk9XMQ==
FRAME=QVJST1cxAAD/////mAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJDQSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAoAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAoAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAIAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAABg5zW9qOgVACqCcb2o6BUA9BytvajoFQC+t+i9qOgVAIhSJL6o6BUAUu1fvqjoFQAciJu+qOgVAOYi176o6BUAsL0Sv6joFQB6WE6/qOgVZfe/JfTT5D+ScrcTWMPAPyuGdcc+nNu/5JrgYzpT67+4Pj8gTfzvvzRIQWuERuq/pzbC1qUX2L8uaPSa5VrIP2qU/tzkP+Y/XDitFE8y7z8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAAAwABAAAAqAEAAAAAAADAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJDQSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAwAEAAEFSUk9XMQ==
FRAME=QVJST1cxAAD/////mAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJPUiJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAoAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAoAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAIAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAABg5zW9qOgVACqCcb2o6BUA9BytvajoFQC+t+i9qOgVAIhSJL6o6BUAUu1fvqjoFQAciJu+qOgVAOYi176o6BUAsL0Sv6joFQB6WE6/qOgVuLq7bpeMw7+66whIaVvlv6W6PO7/5u6/jrx0p6Nt7r/75GqVrRbkv4dItlWh4rm/J7du/ytV3T+xc43pKtDrP+Oh4Fs18e8/jqeDFv226T8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAAAwABAAAAqAEAAAAAAADAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJPUiJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAwAEAAEFSUk9XMQ==
FRAME=QVJST1cxAAD/////mAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJOViJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAoAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAoAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAIAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAABg5zW9qOgVACqCcb2o6BUA9BytvajoFQC+t+i9qOgVAIhSJL6o6BUAUu1fvqjoFQAciJu+qOgVAOYi176o6BUAsL0Sv6joFQB6WE6/qOgVAAAAAAAA8D80QjbH+M/qP7Y4Xpfv3Nk/8KRvdH+RxL/+ywwfaozlvwj0Aj/k9+6/BjMtZPFY7r9vPD2TGuPjv2Jn+VTJ1Le/ulAMflLK3T8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAAAwABAAAAqAEAAAAAAADAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJOViJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAwAEAAEFSUk9XMQ==
FRAME=QVJST1cxAAD/////mAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJDQSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAoAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAoAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAIAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAABg5zW9qOgVACqCcb2o6BUA9BytvajoFQC+t+i9qOgVAIhSJL6o6BUAUu1fvqjoFQAciJu+qOgVAOYi176o6BUAsL0Sv6joFQB6WE6/qOgVgULMTEFL6D/apho0crnvPwec1sJ03uw/i0+Tbkqn4D8Xemt5kcSev0lMwz3FQ+K/0Zk5vmSl7b/odHQzV2rvvwte5zTB/+a/6K7XwtSAzL8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAAAwABAAAAqAEAAAAAAADAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJDQSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAwAEAAEFSUk9XMQ==
FRAME=QVJST1cxAAD/////mAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJPUiJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAoAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAoAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAIAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAABg5zW9qOgVACqCcb2o6BUA9BytvajoFQC+t+i9qOgVAIhSJL6o6BUAUu1fvqjoFQAciJu+qOgVAOYi176o6BUAsL0Sv6joFQB6WE6/qOgVAb0ZwuSf7z9qeqC7g9TnP+eweHtintA/TgqC7qPP07+PgIWgp+jovwoOkrID1u+/WDAWM+5w7L/TJUtFXabfv+Ll4A8Dwa4/knG85sQL4z8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAAAwABAAAAqAEAAAAAAADAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJPUiJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAwAEAAEFSUk9XMQ==
FRAME=QVJST1cxAAD/////mAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJOViJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAoAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAoAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAIAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAABg5zW9qOgVACqCcb2o6BUA9BytvajoFQC+t+i9qOgVAIhSJL6o6BUAUu1fvqjoFQAciJu+qOgVAOYi176o6BUAsL0Sv6joFQB6WE6/qOgVAAAAAAAAAAAkFX2ujHfhP4/gH95ORe0/9ZCCRYuV7z+UH9NDPajnP851KG+xHtA/eGvmdhJN1L8vkWob6BHpv2Y1mmlt3O+/ZSAQ3WxS7L8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAAAwABAAAAqAEAAAAAAADAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJOViJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAwAEAAEFSUk9XMQ==
FRAME=QVJST1cxAAD/////mAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJDQSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAoAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAoAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAIAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAABg5zW9qOgVACqCcb2o6BUA9BytvajoFQC+t+i9qOgVAIhSJL6o6BUAUu1fvqjoFQAciJu+qOgVAOYi176o6BUAsL0Sv6joFQB6WE6/qOgVDGW9JfTT5D/iPqUTWMPAP5JuXcc+nNu/6KbcYzpT67/ozDQgTfzvv+tSOWuERuq/2Eev1qUX2L96Leya5VrIP5yv8dzkP+Y/vRWjFE8y7z8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAAAwABAAAAqAEAAAAAAADAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJDQSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAwAEAAEFSUk9XMQ==
FRAME=QVJST1cxAAD/////mAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJPUiJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAoAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAoAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAIAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAABg5zW9qOgVACqCcb2o6BUA9BytvajoFQC+t+i9qOgVAIhSJL6o6BUAUu1fvqjoFQAciJu+qOgVAOYi176o6BUAsL0Sv6joFQB6WE6/qOgVokm5bpeMw7+g7gZIaVvlvxd9PO7/5u6/DY9wp6Nt7r82zGiVrRbkvw++klWh4rm/F2xb/ytV3T8enIHpKtDrP3BL3ls18e8/ufyBFv226T8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAAAwABAAAAqAEAAAAAAADAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJPUiJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAwAEAAEFSUk9XMQ==
FRAME=QVJST1cxAAD/////mAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJOViJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAoAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAoAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAAAAAAIAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAABg5zW9qOgVACqCcb2o6BUA9BytvajoFQC+t+i9qOgVAIhSJL6o6BUAUu1fvqjoFQAciJu+qOgVAOYi176o6BUAsL0Sv6joFQB6WE6/qOgVAAAAAAAA8D/WeS3H+M/qPwEQRpfv3Nk/VWxgdH+RxL/Ygwkfaozlv1cl/z7k9+6/OYgmZPFY7r+yQzWTGuPjvy4NjFTJ1Le/0Qf3fVLK3T8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAAAwABAAAAqAEAAAAAAADAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAAD4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj///8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAgAAAKQAAAAEAAAAdv///xQAAABsAAAAbAAAAAAAAANsAAAAAgAAACwAAAAEAAAAaP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAjP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJOViJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAwAEAAEFSUk9XMQ==
+16 -16
View File
@@ -3,28 +3,28 @@
Frame[0]
Name: foo
Dimensions: 3 Fields by 1 Rows
+--------------------+--------------------+----------------------+
| Name: CA | Name: OR | Name: NV |
| Labels: | Labels: | Labels: |
| Type: []float64 | Type: []float64 | Type: []float64 |
+--------------------+--------------------+----------------------+
| 0.3455559195129913 | 0.9373322369907638 | -0.48750602508751073 |
+--------------------+--------------------+----------------------+
+-----------------+-----------------+-----------------+
| Name: CA | Name: OR | Name: NV |
| Labels: | Labels: | Labels: |
| Type: []float64 | Type: []float64 | Type: []float64 |
+-----------------+-----------------+-----------------+
| 0.3455559195 | 0.9373322369 | -0.487506025 |
+-----------------+-----------------+-----------------+
Frame[1]
Name: bar
Dimensions: 3 Fields by 1 Rows
+--------------------+--------------------+-------------------+
| Name: CA | Name: OR | Name: NV |
| Labels: | Labels: | Labels: |
| Type: []float64 | Type: []float64 | Type: []float64 |
+--------------------+--------------------+-------------------+
| 0.9383981598924473 | 0.3484369060502785 | 0.873119622676856 |
+--------------------+--------------------+-------------------+
+-----------------+-----------------+-----------------+
| Name: CA | Name: OR | Name: NV |
| Labels: | Labels: | Labels: |
| Type: []float64 | Type: []float64 | Type: []float64 |
+-----------------+-----------------+-----------------+
| 0.9383981598 | 0.348436906 | 0.8731196226 |
+-----------------+-----------------+-----------------+
====== TEST DATA RESPONSE (arrow base64) ======
FRAME=QVJST1cxAAD/////sAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAADU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAPT+//8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAwAAAMgAAABcAAAABAAAAFb///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAARP///wgAAAAMAAAAAgAAAE5WAAAEAAAAbmFtZQAAAAAAAAAAOv///wAAAgACAAAATlYAAKr///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAAmP///wgAAAAMAAAAAgAAAE9SAAAEAAAAbmFtZQAAAAAAAAAAjv///wAAAgACAAAAT1IAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABAAAAASAAAAAAAAANIAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAADAAAAAIAAABDQQAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAgACAAAAQ0EAAP/////oAAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAGAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAeAAAAAEAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAAAAAAAwAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAACPXT5OWHdY/S5TrLKD+7T/kpJZ4TDPfvxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA8AAAAAAADAAEAAADAAQAAAAAAAPAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAADU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAPT+//8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAwAAAMgAAABcAAAABAAAAFb///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAARP///wgAAAAMAAAAAgAAAE5WAAAEAAAAbmFtZQAAAAAAAAAAOv///wAAAgACAAAATlYAAKr///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAAmP///wgAAAAMAAAAAgAAAE9SAAAEAAAAbmFtZQAAAAAAAAAAjv///wAAAgACAAAAT1IAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABAAAAASAAAAAAAAANIAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAADAAAAAIAAABDQQAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAgACAAAAQ0EAAOABAABBUlJPVzE=
FRAME=QVJST1cxAAD/////sAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAADU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAPT+//8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAwAAAMgAAABcAAAABAAAAFb///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAARP///wgAAAAMAAAAAgAAAE5WAAAEAAAAbmFtZQAAAAAAAAAAOv///wAAAgACAAAATlYAAKr///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAAmP///wgAAAAMAAAAAgAAAE9SAAAEAAAAbmFtZQAAAAAAAAAAjv///wAAAgACAAAAT1IAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABAAAAASAAAAAAAAANIAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAADAAAAAIAAABDQQAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAgACAAAAQ0EAAP/////oAAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAGAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAeAAAAAEAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAAAAAAAwAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAACKr65NbB+4/yyQNT8pM1j8akxyQmPDrPxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA8AAAAAAADAAEAAADAAQAAAAAAAPAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAADU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAPT+//8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAwAAAMgAAABcAAAABAAAAFb///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAARP///wgAAAAMAAAAAgAAAE5WAAAEAAAAbmFtZQAAAAAAAAAAOv///wAAAgACAAAATlYAAKr///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAAmP///wgAAAAMAAAAAgAAAE9SAAAEAAAAbmFtZQAAAAAAAAAAjv///wAAAgACAAAAT1IAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABAAAAASAAAAAAAAANIAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAADAAAAAIAAABDQQAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAgACAAAAQ0EAAOABAABBUlJPVzE=
FRAME=QVJST1cxAAD/////sAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAADU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAPT+//8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAwAAAMgAAABcAAAABAAAAFb///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAARP///wgAAAAMAAAAAgAAAE5WAAAEAAAAbmFtZQAAAAAAAAAAOv///wAAAgACAAAATlYAAKr///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAAmP///wgAAAAMAAAAAgAAAE9SAAAEAAAAbmFtZQAAAAAAAAAAjv///wAAAgACAAAAT1IAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABAAAAASAAAAAAAAANIAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAADAAAAAIAAABDQQAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAgACAAAAQ0EAAP/////oAAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAGAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAeAAAAAEAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAAAAAAAwAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAPRETJOWHdY/0xrfLKD+7T/fln54TDPfvxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA8AAAAAAADAAEAAADAAQAAAAAAAPAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAADU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAPT+//8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAwAAAMgAAABcAAAABAAAAFb///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAARP///wgAAAAMAAAAAgAAAE5WAAAEAAAAbmFtZQAAAAAAAAAAOv///wAAAgACAAAATlYAAKr///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAAmP///wgAAAAMAAAAAgAAAE9SAAAEAAAAbmFtZQAAAAAAAAAAjv///wAAAgACAAAAT1IAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABAAAAASAAAAAAAAANIAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAADAAAAAIAAABDQQAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAgACAAAAQ0EAAOABAABBUlJPVzE=
FRAME=QVJST1cxAAD/////sAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAADU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAPT+//8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAwAAAMgAAABcAAAABAAAAFb///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAARP///wgAAAAMAAAAAgAAAE5WAAAEAAAAbmFtZQAAAAAAAAAAOv///wAAAgACAAAATlYAAKr///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAAmP///wgAAAAMAAAAAgAAAE9SAAAEAAAAbmFtZQAAAAAAAAAAjv///wAAAgACAAAAT1IAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABAAAAASAAAAAAAAANIAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAADAAAAAIAAABDQQAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAgACAAAAQ0EAAP/////oAAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAGAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAeAAAAAEAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAAAAAAAwAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAG723pNbB+4/wlL/TspM1j/4AhKQmPDrPxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA8AAAAAAADAAEAAADAAQAAAAAAAPAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAADU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAPT+//8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAwAAAMgAAABcAAAABAAAAFb///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAARP///wgAAAAMAAAAAgAAAE5WAAAEAAAAbmFtZQAAAAAAAAAAOv///wAAAgACAAAATlYAAKr///8UAAAAOAAAADgAAAAAAAADOAAAAAEAAAAEAAAAmP///wgAAAAMAAAAAgAAAE9SAAAEAAAAbmFtZQAAAAAAAAAAjv///wAAAgACAAAAT1IAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABAAAAASAAAAAAAAANIAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAADAAAAAIAAABDQQAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAgACAAAAQ0EAAOABAABBUlJPVzE=
@@ -3,28 +3,28 @@
Frame[0]
Name: foo
Dimensions: 3 Fields by 1 Rows
+--------------------+--------------------+----------------------+
| Name: | Name: | Name: |
| Labels: state=CA | Labels: state=OR | Labels: state=NV |
| Type: []float64 | Type: []float64 | Type: []float64 |
+--------------------+--------------------+----------------------+
| 0.3455559195129913 | 0.9373322369907638 | -0.48750602508751073 |
+--------------------+--------------------+----------------------+
+------------------+------------------+------------------+
| Name: | Name: | Name: |
| Labels: state=CA | Labels: state=OR | Labels: state=NV |
| Type: []float64 | Type: []float64 | Type: []float64 |
+------------------+------------------+------------------+
| 0.3455559195 | 0.9373322369 | -0.487506025 |
+------------------+------------------+------------------+
Frame[1]
Name: bar
Dimensions: 3 Fields by 1 Rows
+--------------------+--------------------+-------------------+
| Name: | Name: | Name: |
| Labels: state=CA | Labels: state=OR | Labels: state=NV |
| Type: []float64 | Type: []float64 | Type: []float64 |
+--------------------+--------------------+-------------------+
| 0.9383981598924473 | 0.3484369060502785 | 0.873119622676856 |
+--------------------+--------------------+-------------------+
+------------------+------------------+------------------+
| Name: | Name: | Name: |
| Labels: state=CA | Labels: state=OR | Labels: state=NV |
| Type: []float64 | Type: []float64 | Type: []float64 |
+------------------+------------------+------------------+
| 0.9383981598 | 0.348436906 | 0.8731196226 |
+------------------+------------------+------------------+
====== TEST DATA RESPONSE (arrow base64) ======
FRAME=QVJST1cxAAD/////QAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAABU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAHT+//8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAwAAACQBAACMAAAABAAAAPr+//8UAAAAaAAAAGgAAAAAAAADaAAAAAIAAAAoAAAABAAAAMj+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA6P7//wgAAAAYAAAADgAAAHsic3RhdGUiOiJOViJ9AAAGAAAAbGFiZWxzAAAAAAAA3v7//wAAAgAAAAAAAAAAAH7///8UAAAAaAAAAGgAAAAAAAADaAAAAAIAAAAoAAAABAAAAEz///8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAbP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJPUiJ9AAAGAAAAbGFiZWxzAAAAAAAAYv///wAAAgAAAAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAHAAAAB4AAAAAAAAA3gAAAACAAAAMAAAAAQAAADg////CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAgADAAIAAQACAAAAAgAAAAYAAAADgAAAHsic3RhdGUiOiJDQSJ9AAAGAAAAbGFiZWxzAAAAAAAAAAAGAAgABgAGAAAAAAACAAAAAAAAAAAAAAAAAP/////oAAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAGAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAeAAAAAEAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAAAAAAAwAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAACPXT5OWHdY/S5TrLKD+7T/kpJZ4TDPfvxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAADAAEAAABQAgAAAAAAAPAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAUAAAAAIAAAAoAAAABAAAAFT+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAdP7//wgAAAAMAAAAAwAAAGZvbwAEAAAAbmFtZQAAAAADAAAAJAEAAIwAAAAEAAAA+v7//xQAAABoAAAAaAAAAAAAAANoAAAAAgAAACgAAAAEAAAAyP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADo/v//CAAAABgAAAAOAAAAeyJzdGF0ZSI6Ik5WIn0AAAYAAABsYWJlbHMAAAAAAADe/v//AAACAAAAAAAAAAAAfv///xQAAABoAAAAaAAAAAAAAANoAAAAAgAAACgAAAAEAAAATP///wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAABs////CAAAABgAAAAOAAAAeyJzdGF0ZSI6Ik9SIn0AAAYAAABsYWJlbHMAAAAAAABi////AAACAAAAAAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAAcAAAAHgAAAAAAAADeAAAAAIAAAAwAAAABAAAAOD///8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAACAAMAAgABAAIAAAACAAAABgAAAAOAAAAeyJzdGF0ZSI6IkNBIn0AAAYAAABsYWJlbHMAAAAAAAAAAAYACAAGAAYAAAAAAAIAAAAAAAAAAABoAgAAQVJST1cx
FRAME=QVJST1cxAAD/////QAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAABU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAHT+//8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAwAAACQBAACMAAAABAAAAPr+//8UAAAAaAAAAGgAAAAAAAADaAAAAAIAAAAoAAAABAAAAMj+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA6P7//wgAAAAYAAAADgAAAHsic3RhdGUiOiJOViJ9AAAGAAAAbGFiZWxzAAAAAAAA3v7//wAAAgAAAAAAAAAAAH7///8UAAAAaAAAAGgAAAAAAAADaAAAAAIAAAAoAAAABAAAAEz///8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAbP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJPUiJ9AAAGAAAAbGFiZWxzAAAAAAAAYv///wAAAgAAAAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAHAAAAB4AAAAAAAAA3gAAAACAAAAMAAAAAQAAADg////CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAgADAAIAAQACAAAAAgAAAAYAAAADgAAAHsic3RhdGUiOiJDQSJ9AAAGAAAAbGFiZWxzAAAAAAAAAAAGAAgABgAGAAAAAAACAAAAAAAAAAAAAAAAAP/////oAAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAGAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAeAAAAAEAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAAAAAAAwAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAACKr65NbB+4/yyQNT8pM1j8akxyQmPDrPxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAADAAEAAABQAgAAAAAAAPAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAUAAAAAIAAAAoAAAABAAAAFT+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAdP7//wgAAAAMAAAAAwAAAGJhcgAEAAAAbmFtZQAAAAADAAAAJAEAAIwAAAAEAAAA+v7//xQAAABoAAAAaAAAAAAAAANoAAAAAgAAACgAAAAEAAAAyP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADo/v//CAAAABgAAAAOAAAAeyJzdGF0ZSI6Ik5WIn0AAAYAAABsYWJlbHMAAAAAAADe/v//AAACAAAAAAAAAAAAfv///xQAAABoAAAAaAAAAAAAAANoAAAAAgAAACgAAAAEAAAATP///wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAABs////CAAAABgAAAAOAAAAeyJzdGF0ZSI6Ik9SIn0AAAYAAABsYWJlbHMAAAAAAABi////AAACAAAAAAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAAcAAAAHgAAAAAAAADeAAAAAIAAAAwAAAABAAAAOD///8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAACAAMAAgABAAIAAAACAAAABgAAAAOAAAAeyJzdGF0ZSI6IkNBIn0AAAYAAABsYWJlbHMAAAAAAAAAAAYACAAGAAYAAAAAAAIAAAAAAAAAAABoAgAAQVJST1cx
FRAME=QVJST1cxAAD/////QAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAABU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAHT+//8IAAAADAAAAAMAAABmb28ABAAAAG5hbWUAAAAAAwAAACQBAACMAAAABAAAAPr+//8UAAAAaAAAAGgAAAAAAAADaAAAAAIAAAAoAAAABAAAAMj+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA6P7//wgAAAAYAAAADgAAAHsic3RhdGUiOiJOViJ9AAAGAAAAbGFiZWxzAAAAAAAA3v7//wAAAgAAAAAAAAAAAH7///8UAAAAaAAAAGgAAAAAAAADaAAAAAIAAAAoAAAABAAAAEz///8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAbP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJPUiJ9AAAGAAAAbGFiZWxzAAAAAAAAYv///wAAAgAAAAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAHAAAAB4AAAAAAAAA3gAAAACAAAAMAAAAAQAAADg////CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAgADAAIAAQACAAAAAgAAAAYAAAADgAAAHsic3RhdGUiOiJDQSJ9AAAGAAAAbGFiZWxzAAAAAAAAAAAGAAgABgAGAAAAAAACAAAAAAAAAAAAAAAAAP/////oAAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAGAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAeAAAAAEAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAAAAAAAwAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAPRETJOWHdY/0xrfLKD+7T/fln54TDPfvxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAADAAEAAABQAgAAAAAAAPAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAUAAAAAIAAAAoAAAABAAAAFT+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAdP7//wgAAAAMAAAAAwAAAGZvbwAEAAAAbmFtZQAAAAADAAAAJAEAAIwAAAAEAAAA+v7//xQAAABoAAAAaAAAAAAAAANoAAAAAgAAACgAAAAEAAAAyP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADo/v//CAAAABgAAAAOAAAAeyJzdGF0ZSI6Ik5WIn0AAAYAAABsYWJlbHMAAAAAAADe/v//AAACAAAAAAAAAAAAfv///xQAAABoAAAAaAAAAAAAAANoAAAAAgAAACgAAAAEAAAATP///wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAABs////CAAAABgAAAAOAAAAeyJzdGF0ZSI6Ik9SIn0AAAYAAABsYWJlbHMAAAAAAABi////AAACAAAAAAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAAcAAAAHgAAAAAAAADeAAAAAIAAAAwAAAABAAAAOD///8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAACAAMAAgABAAIAAAACAAAABgAAAAOAAAAeyJzdGF0ZSI6IkNBIn0AAAYAAABsYWJlbHMAAAAAAAAAAAYACAAGAAYAAAAAAAIAAAAAAAAAAABoAgAAQVJST1cx
FRAME=QVJST1cxAAD/////QAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAABU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAHT+//8IAAAADAAAAAMAAABiYXIABAAAAG5hbWUAAAAAAwAAACQBAACMAAAABAAAAPr+//8UAAAAaAAAAGgAAAAAAAADaAAAAAIAAAAoAAAABAAAAMj+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA6P7//wgAAAAYAAAADgAAAHsic3RhdGUiOiJOViJ9AAAGAAAAbGFiZWxzAAAAAAAA3v7//wAAAgAAAAAAAAAAAH7///8UAAAAaAAAAGgAAAAAAAADaAAAAAIAAAAoAAAABAAAAEz///8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAbP///wgAAAAYAAAADgAAAHsic3RhdGUiOiJPUiJ9AAAGAAAAbGFiZWxzAAAAAAAAYv///wAAAgAAAAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAHAAAAB4AAAAAAAAA3gAAAACAAAAMAAAAAQAAADg////CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAgADAAIAAQACAAAAAgAAAAYAAAADgAAAHsic3RhdGUiOiJDQSJ9AAAGAAAAbGFiZWxzAAAAAAAAAAAGAAgABgAGAAAAAAACAAAAAAAAAAAAAAAAAP/////oAAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAGAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAeAAAAAEAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAAAAAAAwAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAG723pNbB+4/wlL/TspM1j/4AhKQmPDrPxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAADAAEAAABQAgAAAAAAAPAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAUAAAAAIAAAAoAAAABAAAAFT+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAdP7//wgAAAAMAAAAAwAAAGJhcgAEAAAAbmFtZQAAAAADAAAAJAEAAIwAAAAEAAAA+v7//xQAAABoAAAAaAAAAAAAAANoAAAAAgAAACgAAAAEAAAAyP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADo/v//CAAAABgAAAAOAAAAeyJzdGF0ZSI6Ik5WIn0AAAYAAABsYWJlbHMAAAAAAADe/v//AAACAAAAAAAAAAAAfv///xQAAABoAAAAaAAAAAAAAANoAAAAAgAAACgAAAAEAAAATP///wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAABs////CAAAABgAAAAOAAAAeyJzdGF0ZSI6Ik9SIn0AAAYAAABsYWJlbHMAAAAAAABi////AAACAAAAAAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAAcAAAAHgAAAAAAAADeAAAAAIAAAAwAAAABAAAAOD///8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAACAAMAAgABAAIAAAACAAAABgAAAAOAAAAeyJzdGF0ZSI6IkNBIn0AAAYAAABsYWJlbHMAAAAAAAAAAAYACAAGAAYAAAAAAAIAAAAAAAAAAABoAgAAQVJST1cx
+17 -17
View File
@@ -3,23 +3,23 @@
Frame[0]
Name:
Dimensions: 4 Fields by 51 Rows
+----------------+----------------------+----------------------+---------------------+
| Name: state | Name: foo | Name: bar | Name: baz |
| Labels: | Labels: | Labels: | Labels: |
| Type: []string | Type: []float64 | Type: []float64 | Type: []float64 |
+----------------+----------------------+----------------------+---------------------+
| AL | 0.6779842417778559 | 0.7350764367743711 | 0.9223316213929472 |
| AK | 0.9096030926893556 | -0.41547829518515106 | -2.1892914822036755 |
| AZ | -0.11168671678088725 | -0.9937434665418967 | 0.11238988787472795 |
| AR | -0.9791366197808846 | -0.2032030506760749 | 4.818513386109159 |
| CA | -0.4979007493043646 | 0.8672340190756773 | -0.5741250208738828 |
| CO | 0.6691553056325918 | 0.74312258540819 | 0.9004642286104536 |
| CT | 0.914501128595007 | -0.4045833483949364 | -2.260352859856979 |
| DE | -0.09980837850296802 | -0.9950066771537809 | 0.10030925499763493 |
| DC | -0.976639481217665 | -0.21488444272885437 | 4.544952016140177 |
| ... | ... | ... | ... |
+----------------+----------------------+----------------------+---------------------+
+----------------+-----------------+-----------------+-----------------+
| Name: state | Name: foo | Name: bar | Name: baz |
| Labels: | Labels: | Labels: | Labels: |
| Type: []string | Type: []float64 | Type: []float64 | Type: []float64 |
+----------------+-----------------+-----------------+-----------------+
| AL | 0.6779842417 | 0.7350764367 | 0.9223316213 |
| AK | 0.9096030926 | -0.4154782951 | -2.1892914822 |
| AZ | -0.1116867167 | -0.9937434665 | 0.1123898878 |
| AR | -0.9791366197 | -0.2032030506 | 4.8185133861 |
| CA | -0.4979007493 | 0.867234019 | -0.5741250208 |
| CO | 0.6691553056 | 0.7431225854 | 0.9004642286 |
| CT | 0.9145011285 | -0.4045833483 | -2.2603528598 |
| DE | -0.0998083785 | -0.9950066771 | 0.1003092549 |
| DC | -0.9766394812 | -0.2148844427 | 4.5449520161 |
| ... | ... | ... | ... |
+----------------+-----------------+-----------------+-----------------+
====== TEST DATA RESPONSE (arrow base64) ======
FRAME=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
FRAME=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
+4 -1
View File
@@ -192,7 +192,10 @@ func getStateValues(t time.Time, field string, query usaQuery) []float64 {
values := make([]float64, len(query.States))
for i := range query.States {
tv += incr
values[i] = fn(float64(int64(tv) % int64(pn)))
value := fn(float64(int64(tv) % int64(pn)))
// We shall truncate float64 to a certain precision, because otherwise we might
// get different cos/tan results across different CPU architectures and compilers.
values[i] = float64(int(value*1e10)) / 1e10
}
return values
}
@@ -15,20 +15,6 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import userEvent from '@testing-library/user-event';
import { selectOptionInTest } from '@grafana/ui';
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
jest.mock('./api/alertmanager');
jest.mock('./utils/config');
@@ -0,0 +1,273 @@
import React from 'react';
import { locationService, setDataSourceSrv } from '@grafana/runtime';
import { configureStore } from 'app/store/configureStore';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { render } from '@testing-library/react';
import { PanelAlertTabContent } from './PanelAlertTabContent';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import {
mockDataSource,
MockDataSourceSrv,
mockPromAlertingRule,
mockPromRuleGroup,
mockPromRuleNamespace,
mockRulerGrafanaRule,
} from './mocks';
import { DataSourceType } from './utils/datasource';
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
import { getAllDataSources } from './utils/config';
import { fetchRules } from './api/prometheus';
import { fetchRulerRules } from './api/ruler';
import { Annotation } from './utils/constants';
import { byTestId } from 'testing-library-selector';
import { PrometheusDatasource } from 'app/plugins/datasource/prometheus/datasource';
import { DataSourceApi } from '@grafana/data';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
jest.mock('./api/prometheus');
jest.mock('./api/ruler');
jest.mock('./utils/config');
const dataSources = {
prometheus: mockDataSource({
name: 'Prometheus',
type: DataSourceType.Prometheus,
}),
};
dataSources.prometheus.meta.alerting = true;
const mocks = {
getAllDataSources: typeAsJestMock(getAllDataSources),
api: {
fetchRules: typeAsJestMock(fetchRules),
fetchRulerRules: typeAsJestMock(fetchRulerRules),
},
};
const renderAlertTabContent = (dashboard: DashboardModel, panel: PanelModel) => {
const store = configureStore();
return render(
<Provider store={store}>
<Router history={locationService.getHistory()}>
<PanelAlertTabContent dashboard={dashboard} panel={panel} />
</Router>
</Provider>
);
};
const rules = [
mockPromRuleNamespace({
name: 'default',
groups: [
mockPromRuleGroup({
name: 'mygroup',
rules: [
mockPromAlertingRule({
name: 'dashboardrule1',
annotations: {
[Annotation.dashboardUID]: '12',
[Annotation.panelID]: '34',
},
}),
],
}),
mockPromRuleGroup({
name: 'othergroup',
rules: [
mockPromAlertingRule({
name: 'dashboardrule2',
annotations: {
[Annotation.dashboardUID]: '121',
[Annotation.panelID]: '341',
},
}),
],
}),
],
}),
];
const rulerRules = {
default: [
{
name: 'mygroup',
rules: [
mockRulerGrafanaRule(
{
annotations: {
[Annotation.dashboardUID]: '12',
[Annotation.panelID]: '34',
},
},
{
title: 'dashboardrule1',
}
),
],
},
{
name: 'othergroup',
rules: [
mockRulerGrafanaRule(
{
annotations: {
[Annotation.dashboardUID]: '121',
[Annotation.panelID]: '341',
},
},
{
title: 'dashboardrule2',
}
),
],
},
],
};
const dashboard = {
uid: '12',
time: {
from: 'now-6h',
to: 'now',
},
meta: {
canSave: true,
folderId: 1,
folderTitle: 'super folder',
},
} as DashboardModel;
const panel = ({
datasource: dataSources.prometheus.uid,
title: 'mypanel',
editSourceId: 34,
targets: [
{
expr: 'sum(some_metric [$__interval])) by (app)',
refId: 'A',
},
],
} as any) as PanelModel;
const ui = {
row: byTestId('row'),
createButton: byTestId<HTMLAnchorElement>('create-alert-rule-button'),
};
describe('PanelAlertTabContent', () => {
beforeEach(() => {
jest.resetAllMocks();
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
const dsService = new MockDataSourceSrv(dataSources);
dsService.datasources[dataSources.prometheus.name] = new PrometheusDatasource(
dataSources.prometheus
) as DataSourceApi<any, any>;
setDataSourceSrv(dsService);
});
it('Will take into account panel maxDataPoints', async () => {
await renderAlertTabContent(dashboard, ({
...panel,
maxDataPoints: 100,
interval: '10s',
} as any) as PanelModel);
const button = await ui.createButton.find();
const href = button.href;
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
expect(match).toHaveLength(2);
const defaults = JSON.parse(decodeURIComponent(match![1]));
expect(defaults.queries[0].model).toEqual({
expr: 'sum(some_metric [5m])) by (app)',
refId: 'A',
datasource: 'Prometheus',
interval: '',
intervalMs: 300000,
maxDataPoints: 100,
});
});
it('Will take into account datasource minInterval', async () => {
((getDatasourceSrv() as any) as MockDataSourceSrv).datasources[dataSources.prometheus.name].interval = '7m';
await renderAlertTabContent(dashboard, ({
...panel,
maxDataPoints: 100,
} as any) as PanelModel);
const button = await ui.createButton.find();
const href = button.href;
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
expect(match).toHaveLength(2);
const defaults = JSON.parse(decodeURIComponent(match![1]));
expect(defaults.queries[0].model).toEqual({
expr: 'sum(some_metric [7m])) by (app)',
refId: 'A',
datasource: 'Prometheus',
interval: '',
intervalMs: 420000,
maxDataPoints: 100,
});
});
it('Will render alerts belonging to panel and a button to create alert from panel queries', async () => {
mocks.api.fetchRules.mockResolvedValue(rules);
mocks.api.fetchRulerRules.mockResolvedValue(rulerRules);
await renderAlertTabContent(dashboard, panel);
const rows = await ui.row.findAll();
expect(rows).toHaveLength(1);
expect(rows[0]).toHaveTextContent(/dashboardrule1/);
expect(rows[0]).not.toHaveTextContent(/dashboardrule2/);
const button = await ui.createButton.find();
const href = button.href;
const match = href.match(/alerting\/new\?defaults=(.*)&returnTo=/);
expect(match).toHaveLength(2);
const defaults = JSON.parse(decodeURIComponent(match![1]));
expect(defaults).toEqual({
type: 'grafana',
folder: { id: 1, title: 'super folder' },
queries: [
{
refId: 'A',
queryType: '',
relativeTimeRange: { from: 21600, to: 0 },
datasourceUid: 'mock-ds-2',
model: {
expr: 'sum(some_metric [15s])) by (app)',
refId: 'A',
datasource: 'Prometheus',
interval: '',
intervalMs: 15000,
},
},
{
refId: 'B',
datasourceUid: '-100',
queryType: '',
model: {
refId: 'B',
hide: false,
type: 'classic_conditions',
datasource: '__expr__',
conditions: [
{
type: 'query',
evaluator: { params: [3], type: 'gt' },
operator: { type: 'and' },
query: { params: ['A'] },
reducer: { params: [], type: 'last' },
},
],
},
},
],
name: 'mypanel',
condition: 'B',
annotations: [
{ key: '__dashboardUid__', value: '12' },
{ key: '__panelId__', value: '34' },
],
});
});
});
@@ -1,9 +1,10 @@
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import React, { FC } from 'react';
import { Alert, LinkButton } from '@grafana/ui';
import { Alert, LinkButton, Button } from '@grafana/ui';
import { panelToRuleFormValues } from '../../utils/rule-form';
import { useLocation } from 'react-router-dom';
import { urlUtil } from '@grafana/data';
import { useAsync } from 'react-use';
interface Props {
panel: PanelModel;
@@ -12,8 +13,11 @@ interface Props {
}
export const NewRuleFromPanelButton: FC<Props> = ({ dashboard, panel, className }) => {
const formValues = panelToRuleFormValues(panel, dashboard);
const { loading, value: formValues } = useAsync(() => panelToRuleFormValues(panel, dashboard), [panel, dashboard]);
const location = useLocation();
if (loading) {
return <Button disabled={true}>Create alert rule from this panel</Button>;
}
if (!formValues) {
return (
@@ -29,7 +33,7 @@ export const NewRuleFromPanelButton: FC<Props> = ({ dashboard, panel, className
});
return (
<LinkButton icon="bell" href={ruleFormUrl} className={className}>
<LinkButton icon="bell" href={ruleFormUrl} className={className} data-testid="create-alert-rule-button">
Create alert rule from this panel
</LinkButton>
);
+42 -2
View File
@@ -1,5 +1,11 @@
import { DataSourceApi, DataSourceInstanceSettings, DataSourcePluginMeta, ScopedVars } from '@grafana/data';
import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto';
import {
GrafanaAlertStateDecision,
GrafanaRuleDefinition,
PromAlertingRuleState,
PromRuleType,
RulerGrafanaRuleDTO,
} from 'app/types/unified-alerting-dto';
import { AlertingRule, Alert, RecordingRule, RuleGroup, RuleNamespace } from 'app/types/unified-alerting';
import DatasourceSrv from 'app/features/plugins/datasource_srv';
import { DataSourceSrv, GetDataSourceListFilters } from '@grafana/runtime';
@@ -17,7 +23,7 @@ let nextDataSourceId = 1;
export const mockDataSource = (
partial: Partial<DataSourceInstanceSettings> = {},
meta: Partial<DataSourcePluginMeta> = {}
): DataSourceInstanceSettings => {
): DataSourceInstanceSettings<any> => {
const id = partial.id ?? nextDataSourceId++;
return {
@@ -54,6 +60,40 @@ export const mockPromAlert = (partial: Partial<Alert> = {}): Alert => ({
...partial,
});
export const mockRulerGrafanaRule = (
partial: Partial<RulerGrafanaRuleDTO> = {},
partialDef: Partial<GrafanaRuleDefinition> = {}
): RulerGrafanaRuleDTO => {
return {
for: '1m',
grafana_alert: {
uid: '123',
title: 'myalert',
namespace_uid: '123',
namespace_id: 1,
condition: 'A',
no_data_state: GrafanaAlertStateDecision.Alerting,
exec_err_state: GrafanaAlertStateDecision.Alerting,
data: [
{
datasourceUid: '123',
refId: 'A',
queryType: 'huh',
model: {} as any,
},
],
...partialDef,
},
annotations: {
message: 'alert with severity "{{.warning}}}"',
},
labels: {
severity: 'warning',
},
...partial,
};
};
export const mockPromAlertingRule = (partial: Partial<AlertingRule> = {}): AlertingRule => {
return {
type: PromRuleType.Alerting,
@@ -1,11 +1,18 @@
import { DataQuery, rangeUtil, RelativeTimeRange } from '@grafana/data';
import {
DataQuery,
rangeUtil,
RelativeTimeRange,
ScopedVars,
getDefaultRelativeTimeRange,
TimeRange,
IntervalValues,
} from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { contextSrv } from 'app/core/services/context_srv';
import { getNextRefIdChar } from 'app/core/utils/query';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { ExpressionDatasourceID, ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource';
import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { RuleWithLocation } from 'app/types/unified-alerting';
import {
Annotations,
@@ -22,7 +29,6 @@ import { isGrafanaRulesSource } from './datasource';
import { arrayToRecord, recordToArray } from './misc';
import { isAlertingRulerRule, isGrafanaRulerRule } from './rules';
import { parseInterval } from './time';
import { getDefaultRelativeTimeRange } from '../../../../../../packages/grafana-data';
export const getDefaultFormValues = (): RuleFormValues =>
Object.freeze({
@@ -193,60 +199,81 @@ const getDefaultExpression = (refId: string): AlertQuery => {
};
};
const dataQueriesToGrafanaQueries = (
const dataQueriesToGrafanaQueries = async (
queries: DataQuery[],
relativeTimeRange: RelativeTimeRange,
datasourceName?: string
): AlertQuery[] => {
return queries.reduce<AlertQuery[]>((queries, target) => {
scopedVars: ScopedVars | {},
datasourceName?: string,
maxDataPoints?: number,
minInterval?: string
): Promise<AlertQuery[]> => {
const result: AlertQuery[] = [];
for (const target of queries) {
const dsName = target.datasource || datasourceName;
const datasource = await getDataSourceSrv().get(dsName);
const range = rangeUtil.relativeToTimeRange(relativeTimeRange);
const { interval, intervalMs } = getIntervals(range, minInterval ?? datasource.interval, maxDataPoints);
const queryVariables = {
__interval: { text: interval, value: interval },
__interval_ms: { text: intervalMs, value: intervalMs },
...scopedVars,
};
const interpolatedTarget = datasource.interpolateVariablesInQueries
? await datasource.interpolateVariablesInQueries([target], queryVariables)[0]
: target;
if (dsName) {
// expressions
if (dsName === ExpressionDatasourceID) {
const newQuery: AlertQuery = {
refId: target.refId,
refId: interpolatedTarget.refId,
queryType: '',
relativeTimeRange,
datasourceUid: ExpressionDatasourceUID,
model: target,
model: interpolatedTarget,
};
return [...queries, newQuery];
result.push(newQuery);
// queries
} else {
const datasource = getDataSourceSrv().getInstanceSettings(target.datasource || datasourceName);
if (datasource && datasource.meta.alerting) {
const datasourceSettings = getDataSourceSrv().getInstanceSettings(dsName);
if (datasourceSettings && datasourceSettings.meta.alerting) {
const newQuery: AlertQuery = {
refId: target.refId,
queryType: target.queryType ?? '',
refId: interpolatedTarget.refId,
queryType: interpolatedTarget.queryType ?? '',
relativeTimeRange,
datasourceUid: datasource.uid,
model: target,
datasourceUid: datasourceSettings.uid,
model: {
...interpolatedTarget,
maxDataPoints,
intervalMs,
},
};
return [...queries, newQuery];
result.push(newQuery);
}
}
}
return queries;
}, []);
}
return result;
};
export const panelToRuleFormValues = (
export const panelToRuleFormValues = async (
panel: PanelModel,
dashboard: DashboardModel
): Partial<RuleFormValues> | undefined => {
): Promise<Partial<RuleFormValues> | undefined> => {
const { targets } = panel;
// it seems if default datasource is selected, datasource=null, hah
const datasourceName =
panel.datasource === null ? getDatasourceSrv().getInstanceSettings('default')?.name : panel.datasource;
if (!panel.editSourceId || !dashboard.uid) {
return undefined;
}
const relativeTimeRange = rangeUtil.timeRangeToRelative(rangeUtil.convertRawToRange(dashboard.time));
const queries = dataQueriesToGrafanaQueries(targets, relativeTimeRange, datasourceName);
const queries = await dataQueriesToGrafanaQueries(
targets,
relativeTimeRange,
panel.scopedVars || {},
panel.datasource ?? undefined,
panel.maxDataPoints ?? undefined,
panel.interval ?? undefined
);
// if no alerting capable queries are found, can't create a rule
if (!queries.length || !queries.find((query) => query.datasourceUid !== ExpressionDatasourceUID)) {
return undefined;
@@ -269,6 +296,7 @@ export const panelToRuleFormValues = (
: undefined,
queries,
name: panel.title,
condition: queries[queries.length - 1].refId,
annotations: [
{
key: Annotation.dashboardUID,
@@ -282,3 +310,17 @@ export const panelToRuleFormValues = (
};
return formValues;
};
export function getIntervals(range: TimeRange, lowLimit?: string, resolution?: number): IntervalValues {
if (!resolution) {
if (lowLimit && rangeUtil.intervalToMs(lowLimit) > 1000) {
return {
interval: lowLimit,
intervalMs: rangeUtil.intervalToMs(lowLimit),
};
}
return { interval: '1s', intervalMs: 1000 };
}
return rangeUtil.calculateInterval(range, resolution, lowLimit);
}
@@ -1,5 +1,5 @@
import React, { useRef, useState, useLayoutEffect } from 'react';
import { Icon, Tooltip } from '@grafana/ui';
import { Icon, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui';
import { sanitize, sanitizeUrl } from '@grafana/data/src/text/sanitize';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { getLinkSrv } from '../../../panel/panellinks/link_srv';
@@ -7,6 +7,7 @@ import { DashboardLink } from '../../state/DashboardModel';
import { DashboardSearchHit } from 'app/features/search/types';
import { selectors } from '@grafana/e2e-selectors';
import { useAsync } from 'react-use';
import { css, cx } from '@emotion/css';
interface Props {
link: DashboardLink;
@@ -21,33 +22,45 @@ export const DashboardLinksDashboard: React.FC<Props> = (props) => {
const [opened, setOpened] = useState(0);
const resolvedLinks = useResolvedLinks(props, opened);
const buttonStyle = useStyles2(
(theme) =>
css`
color: ${theme.colors.text.primary};
`
);
useLayoutEffect(() => {
setDropdownCssClass(getDropdownLocationCssClass(listRef.current));
}, [resolvedLinks]);
if (link.asDropdown) {
return (
<LinkElement link={link} key="dashlinks-dropdown" aria-label={selectors.components.DashboardLinks.dropDown}>
<LinkElement link={link} key="dashlinks-dropdown" data-testid={selectors.components.DashboardLinks.dropDown}>
<>
<a
<ToolbarButton
onClick={() => setOpened(Date.now())}
className="gf-form-label gf-form-label--dashlink"
className={cx('gf-form-label gf-form-label--dashlink', buttonStyle)}
data-placement="bottom"
data-toggle="dropdown"
aria-expanded={!!opened}
aria-controls="dropdown-list"
aria-haspopup="menu"
>
<Icon name="bars" style={{ marginRight: '4px' }} />
<Icon aria-hidden name="bars" style={{ marginRight: '4px' }} />
<span>{linkInfo.title}</span>
</a>
<ul className={`dropdown-menu ${dropdownCssClass}`} role="menu" ref={listRef}>
</ToolbarButton>
<ul id="dropdown-list" className={`dropdown-menu ${dropdownCssClass}`} role="menu" ref={listRef}>
{resolvedLinks.length > 0 &&
resolvedLinks.map((resolvedLink, index) => {
return (
<li key={`dashlinks-dropdown-item-${resolvedLink.id}-${index}`}>
<li role="none" key={`dashlinks-dropdown-item-${resolvedLink.id}-${index}`}>
<a
role="menuitem"
href={resolvedLink.url}
target={link.targetBlank ? '_blank' : undefined}
rel="noreferrer"
aria-label={selectors.components.DashboardLinks.link}
data-testid={selectors.components.DashboardLinks.link}
aria-label={`${resolvedLink.title} dashboard`}
>
{resolvedLink.title}
</a>
@@ -68,16 +81,17 @@ export const DashboardLinksDashboard: React.FC<Props> = (props) => {
<LinkElement
link={link}
key={`dashlinks-list-item-${resolvedLink.id}-${index}`}
aria-label={selectors.components.DashboardLinks.container}
data-testid={selectors.components.DashboardLinks.container}
>
<a
className="gf-form-label gf-form-label--dashlink"
href={resolvedLink.url}
target={link.targetBlank ? '_blank' : undefined}
rel="noreferrer"
aria-label={selectors.components.DashboardLinks.link}
data-testid={selectors.components.DashboardLinks.link}
aria-label={`${resolvedLink.title} dashboard`}
>
<Icon name="apps" style={{ marginRight: '4px' }} />
<Icon aria-hidden name="apps" style={{ marginRight: '4px' }} />
<span>{resolvedLink.title}</span>
</a>
</LinkElement>
@@ -89,7 +103,6 @@ export const DashboardLinksDashboard: React.FC<Props> = (props) => {
interface LinkElementProps {
link: DashboardLink;
'aria-label': string;
key: string;
children: JSX.Element;
}
@@ -17,6 +17,8 @@ import {
PanelPluginDataSupport,
ScopedVars,
urlUtil,
PanelModel as IPanelModel,
DatasourceRef,
} from '@grafana/data';
import { EDIT_PANEL_ID } from 'app/core/constants';
import config from 'app/core/config';
@@ -124,7 +126,7 @@ const defaults: any = {
title: '',
};
export class PanelModel implements DataConfigSource {
export class PanelModel implements DataConfigSource, IPanelModel {
/* persisted id, used in URL to identify a panel */
id!: number;
editSourceId?: number;
@@ -144,7 +146,7 @@ export class PanelModel implements DataConfigSource {
panels?: any;
declare targets: DataQuery[];
transformations?: DataTransformerConfig[];
datasource: string | null = null;
datasource: DatasourceRef | null = null;
thresholds?: any;
pluginVersion?: string;
+4 -3
View File
@@ -283,7 +283,7 @@ export class UnthemedLogs extends PureComponent<Props, State> {
forceEscape,
} = this.state;
const styles = getStyles(theme);
const styles = getStyles(theme, wrapLogMessage);
const hasData = logRows && logRows.length > 0;
const hasUnescapedContent = this.checkUnescapedContent(logRows);
@@ -420,7 +420,7 @@ export class UnthemedLogs extends PureComponent<Props, State> {
export const Logs = withTheme2(UnthemedLogs);
const getStyles = (theme: GrafanaTheme2) => {
const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean) => {
return {
noData: css`
> * {
@@ -450,7 +450,8 @@ const getStyles = (theme: GrafanaTheme2) => {
justify-content: space-between;
`,
logRows: css`
overflow-x: scroll;
overflow-x: ${wrapLogMessage ? 'unset' : 'scroll'};
overflow-y: visible;
width: 100%;
`,
infoText: css`
+15 -15
View File
@@ -1,35 +1,35 @@
import React, { memo } from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme } from '@grafana/data';
import { useStyles } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
const getStyles = (theme: GrafanaTheme) => ({
const getStyles = (theme: GrafanaTheme2) => ({
metaContainer: css`
flex: 1;
color: ${theme.colors.textWeak};
margin-bottom: ${theme.spacing.d};
color: ${theme.colors.text.secondary};
margin-bottom: ${theme.spacing(2)};
min-width: 30%;
display: flex;
flex-wrap: wrap;
`,
metaItem: css`
margin-right: ${theme.spacing.d};
margin-top: ${theme.spacing.xs};
margin-right: ${theme.spacing(2)};
margin-top: ${theme.spacing(0.5)};
display: flex;
align-items: baseline;
.logs-meta-item__error {
color: ${theme.palette.red};
color: ${theme.colors.error.text};
}
`,
metaLabel: css`
margin-right: calc(${theme.spacing.d} / 2);
font-size: ${theme.typography.size.sm};
font-weight: ${theme.typography.weight.semibold};
margin-right: calc(${theme.spacing(2)} / 2);
font-size: ${theme.typography.bodySmall.fontSize};
font-weight: ${theme.typography.fontWeightMedium};
`,
metaValue: css`
font-family: ${theme.typography.fontFamily.monospace};
font-size: ${theme.typography.size.sm};
font-family: ${theme.typography.fontFamilyMonospace};
font-size: ${theme.typography.bodySmall.fontSize};
`,
});
@@ -39,7 +39,7 @@ export interface MetaItemProps {
}
export const MetaInfoItem = memo(function MetaInfoItem(props: MetaItemProps) {
const style = useStyles(getStyles);
const style = useStyles2(getStyles);
const { label, value } = props;
return (
@@ -55,7 +55,7 @@ export interface MetaInfoTextProps {
}
export const MetaInfoText = memo(function MetaInfoText(props: MetaInfoTextProps) {
const style = useStyles(getStyles);
const style = useStyles2(getStyles);
const { metaItems } = props;
return (
@@ -8,6 +8,7 @@ import {
DataQueryResponse,
DataSourceApi,
DataSourceInstanceSettings,
DatasourceRef,
isValidLiveChannelAddress,
parseLiveChannelAddress,
StreamingFrameOptions,
@@ -17,6 +18,7 @@ import {
import { GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQuery, GrafanaQueryType } from './types';
import AnnotationQueryEditor from './components/AnnotationQueryEditor';
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
import { isString } from 'lodash';
let counter = 100;
@@ -37,7 +39,11 @@ export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
return json;
},
prepareQuery(anno: AnnotationQuery<GrafanaAnnotationQuery>): GrafanaQuery {
return { ...anno, refId: anno.name, queryType: GrafanaQueryType.Annotations };
let datasource: DatasourceRef | undefined | null = undefined;
if (isString(anno.datasource)) {
datasource = anno.datasource as DatasourceRef;
}
return { ...anno, refId: anno.name, queryType: GrafanaQueryType.Annotations, datasource };
},
};
}
@@ -182,10 +182,12 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
return true;
}
/**
* Only applied on flux queries
*/
applyTemplateVariables(query: InfluxQuery, scopedVars: ScopedVars): Record<string, any> {
// this only works in flux-mode, it should not be called in non-flux-mode
if (!this.isFlux) {
throw new Error('applyTemplateVariables called in influxql-mode. this should never happen');
}
return {
...query,
query: this.templateSrv.replace(query.query ?? '', scopedVars), // The raw query text
@@ -344,7 +346,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
policy: this.templateSrv.replace(query.policy ?? '', scopedVars, 'regex'),
};
if (query.rawQuery) {
if (query.rawQuery || this.isFlux) {
expandedQuery.query = this.templateSrv.replace(query.query ?? '', scopedVars, 'regex');
}
@@ -1,4 +1,4 @@
import { PanelModel, FieldConfigSource } from '@grafana/data';
import { PanelModel, FieldConfigSource, DataQuery } from '@grafana/data';
import { graphPanelMigrationHandler } from './GraphMigrations';
describe('Graph Panel Migrations', () => {
@@ -52,7 +52,7 @@ describe('Graph Panel Migrations', () => {
spaceLength: 10,
stack: false,
steppedLine: false,
targets: [
targets: ([
{
alias: 'Foo datacenter',
labels: 'datacenter=foo,region=us-east-1',
@@ -71,7 +71,7 @@ describe('Graph Panel Migrations', () => {
refId: 'C',
scenarioId: 'random_walk',
},
],
] as unknown) as DataQuery[],
thresholds: [],
timeFrom: null,
timeRegions: [],
@@ -1,6 +1,6 @@
import React, { ComponentProps } from 'react';
import { render, screen } from '@testing-library/react';
import { LoadingState, MutableDataFrame, FieldType } from '@grafana/data';
import { LoadingState, MutableDataFrame, FieldType, LogsSortOrder } from '@grafana/data';
import { LogsPanel } from './LogsPanel';
type LogsPanelProps = ComponentProps<typeof LogsPanel>;
@@ -38,6 +38,22 @@ describe('LogsPanel', () => {
expect(screen.getByText(/common_app/i)).toBeInTheDocument();
expect(screen.getByText(/common_job/i)).toBeInTheDocument();
});
it('shows common labels on top when descending sort order', () => {
const { container } = setup({
data: { series: seriesWithCommonLabels },
options: { showCommonLabels: true, sortOrder: LogsSortOrder.Descending },
});
expect(container.firstChild?.childNodes[0].textContent).toMatch(/^Common labels:common_appcommon_job/);
});
it('shows common labels on bottom when ascending sort order', () => {
const { container } = setup({
data: { series: seriesWithCommonLabels },
options: { showCommonLabels: true, sortOrder: LogsSortOrder.Ascending },
});
expect(container.firstChild?.childNodes[0].textContent).toMatch(/Common labels:common_appcommon_job$/);
});
it('does not show common labels when showCommonLabels is set to false', () => {
setup({ data: { series: seriesWithCommonLabels }, options: { showCommonLabels: false } });
+28 -13
View File
@@ -1,7 +1,7 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useRef, useLayoutEffect, useState } from 'react';
import { css } from '@emotion/css';
import { LogRows, CustomScrollbar, LogLabels, useStyles2 } from '@grafana/ui';
import { PanelProps, Field, Labels, GrafanaTheme2 } from '@grafana/data';
import { PanelProps, Field, Labels, GrafanaTheme2, LogsSortOrder } from '@grafana/data';
import { Options } from './types';
import { dataFrameToLogsModel, dedupLogRows } from 'app/core/logs_model';
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
@@ -24,7 +24,10 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
},
title,
}) => {
const style = useStyles2(getStyles(title));
const isAscending = sortOrder === LogsSortOrder.Ascending;
const style = useStyles2(getStyles(title, isAscending));
const [scrollTop, setScrollTop] = useState(0);
const logsContainerRef = useRef<HTMLDivElement>(null);
// Important to memoize stuff here, as panel rerenders a lot for example when resizing.
const [logRows, deduplicatedRows, commonLabels] = useMemo(() => {
@@ -35,6 +38,14 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
return [logRows, deduplicatedRows, commonLabels];
}, [data, dedupStrategy]);
useLayoutEffect(() => {
if (isAscending && logsContainerRef.current) {
setScrollTop(logsContainerRef.current.offsetHeight);
} else {
setScrollTop(0);
}
}, [isAscending, logRows]);
const getFieldLinks = useCallback(
(field: Field, rowIndex: number) => {
return getFieldLinksForExplore({ field, rowIndex, range: data.timeRange });
@@ -50,15 +61,17 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
);
}
const renderCommonLabels = () => (
<div className={style.labelContainer}>
<span className={style.label}>Common labels:</span>
<LogLabels labels={commonLabels ? (commonLabels.value as Labels) : { labels: '(no common labels)' }} />
</div>
);
return (
<CustomScrollbar autoHide>
<div className={style.container}>
{showCommonLabels && (
<div className={style.labelContainer}>
<span className={style.label}>Common labels:</span>
<LogLabels labels={commonLabels ? (commonLabels.value as Labels) : { labels: '(no common labels)' }} />
</div>
)}
<CustomScrollbar autoHide scrollTop={scrollTop}>
<div className={style.container} ref={logsContainerRef}>
{showCommonLabels && !isAscending && renderCommonLabels()}
<LogRows
logRows={logRows}
deduplicatedRows={deduplicatedRows}
@@ -71,20 +84,22 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
getFieldLinks={getFieldLinks}
logsSortOrder={sortOrder}
enableLogDetails={enableLogDetails}
previewLimit={isAscending ? logRows.length : undefined}
/>
{showCommonLabels && isAscending && renderCommonLabels()}
</div>
</CustomScrollbar>
);
};
const getStyles = (title: string) => (theme: GrafanaTheme2) => ({
const getStyles = (title: string, isAscending: boolean) => (theme: GrafanaTheme2) => ({
container: css`
margin-bottom: ${theme.spacing(1.5)};
//We can remove this hot-fix when we fix panel menu with no title overflowing top of all panels
margin-top: ${theme.spacing(!title ? 2.5 : 0)};
`,
labelContainer: css`
margin: ${theme.spacing(0, 0, 0.5, 0.5)};
margin: ${isAscending ? theme.spacing(0.5, 0, 0.5, 0) : theme.spacing(0, 0, 0.5, 0.5)};
display: flex;
align-items: center;
`,
@@ -489,6 +489,10 @@ Object {
"fill": "dash",
},
"lineWidth": 1,
"scaleDistribution": Object {
"log": 10,
"type": "log",
},
"showPoints": "never",
"spanNulls": true,
},

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