mirror of
https://github.com/grafana/grafana.git
synced 2025-12-20 19:44:55 +08:00
Compare commits
63 Commits
docs/add-t
...
v6.2.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a30e47aa4 | ||
|
|
7c176cfa22 | ||
|
|
053f5f5dcc | ||
|
|
c533ec7dea | ||
|
|
22a991ff66 | ||
|
|
91feb80187 | ||
|
|
9a93382348 | ||
|
|
07540dfdce | ||
|
|
8b86d5d1ed | ||
|
|
f26831c8a9 | ||
|
|
6a32c29b92 | ||
|
|
80475fa13a | ||
|
|
2b400ac38e | ||
|
|
c3e5e69e73 | ||
|
|
9e40b07fd7 | ||
|
|
99472f9465 | ||
|
|
5169765db3 | ||
|
|
7b0efb787b | ||
|
|
efd14ac7db | ||
|
|
b0647a4537 | ||
|
|
1f0e94b633 | ||
|
|
eda28131ef | ||
|
|
d385a668a9 | ||
|
|
ef359424a5 | ||
|
|
dce4514f7f | ||
|
|
ad7bd8850f | ||
|
|
5d16da732f | ||
|
|
ee6703fedd | ||
|
|
ec8be5bf61 | ||
|
|
f3c2d03637 | ||
|
|
eb2b31007d | ||
|
|
98f9dbde95 | ||
|
|
7e090ea2a6 | ||
|
|
6a02faeeab | ||
|
|
a6a939cfd3 | ||
|
|
5a10298bd3 | ||
|
|
f52f7c101c | ||
|
|
7824f66cd3 | ||
|
|
71c1e8a731 | ||
|
|
74bc94bcec | ||
|
|
f566da0ff6 | ||
|
|
216aff96fd | ||
|
|
9de11c25a6 | ||
|
|
599e1030d8 | ||
|
|
d62da61d8a | ||
|
|
a2ca973925 | ||
|
|
98da29fd7b | ||
|
|
888ff61d30 | ||
|
|
c1be3adf3b | ||
|
|
ede9d9964d | ||
|
|
5cd69e8d39 | ||
|
|
ceb3672482 | ||
|
|
e519a9d2c4 | ||
|
|
2a3d6604c0 | ||
|
|
9cf0ea5395 | ||
|
|
db37e138bf | ||
|
|
a5a6d43f47 | ||
|
|
d9950aa4f1 | ||
|
|
6e9a395063 | ||
|
|
7374aafb90 | ||
|
|
b54e9880b4 | ||
|
|
09672a287f | ||
|
|
9d877d670e |
@@ -81,7 +81,7 @@ jobs:
|
||||
- run:
|
||||
# Important: all words have to be in lowercase, and separated by "\n".
|
||||
name: exclude known exceptions
|
||||
command: 'echo -e "unknwon" > words_to_ignore.txt'
|
||||
command: 'echo -e "unknwon\nreferer\nerrorstring" > words_to_ignore.txt'
|
||||
- run:
|
||||
name: check documentation spelling errors
|
||||
command: 'codespell -I ./words_to_ignore.txt docs/'
|
||||
@@ -566,6 +566,7 @@ jobs:
|
||||
root: .
|
||||
paths:
|
||||
- dist/grafana-*.msi
|
||||
- dist/grafana-*.msi.sha256
|
||||
|
||||
store-build-artifacts:
|
||||
docker:
|
||||
@@ -700,7 +701,7 @@ workflows:
|
||||
- backend-lint
|
||||
- mysql-integration-test
|
||||
- postgres-integration-test
|
||||
filters: *filter-only-master
|
||||
filters: *filter-only-release
|
||||
|
||||
build-branches-and-prs:
|
||||
jobs:
|
||||
|
||||
@@ -66,8 +66,8 @@ RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
|
||||
"$GF_PATHS_DATA" && \
|
||||
cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \
|
||||
cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \
|
||||
chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \
|
||||
chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS"
|
||||
chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" && \
|
||||
chmod 777 -R "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING"
|
||||
|
||||
COPY --from=0 /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-server /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-cli ./bin/
|
||||
COPY --from=1 /usr/src/app/public ./public
|
||||
|
||||
@@ -113,9 +113,9 @@ type = database
|
||||
|
||||
# cache connectionstring options
|
||||
# database: will use Grafana primary database.
|
||||
# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana`
|
||||
# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=0`. Only addr is required.
|
||||
# memcache: 127.0.0.1:11211
|
||||
connstr =
|
||||
;connstr =
|
||||
|
||||
#################################### Data proxy ###########################
|
||||
[dataproxy]
|
||||
@@ -179,6 +179,31 @@ cookie_samesite = lax
|
||||
# set to true if you want to allow browsers to render Grafana in a <frame>, <iframe>, <embed> or <object>. default is false.
|
||||
allow_embedding = false
|
||||
|
||||
# Set to true if you want to enable http strict transport security (HSTS) response header.
|
||||
# This is only sent when HTTPS is enabled in this configuration.
|
||||
# HSTS tells browsers that the site should only be accessed using HTTPS.
|
||||
# The default will change to true in the next minor release, 6.3.
|
||||
strict_transport_security = false
|
||||
|
||||
# Sets how long a browser should cache HSTS. Only applied if strict_transport_security is enabled.
|
||||
strict_transport_security_max_age_seconds = 86400
|
||||
|
||||
# Set to true if to enable HSTS preloading option. Only applied if strict_transport_security is enabled.
|
||||
strict_transport_security_preload = false
|
||||
|
||||
# Set to true if to enable the HSTS includeSubDomains option. Only applied if strict_transport_security is enabled.
|
||||
strict_transport_security_subdomains = false
|
||||
|
||||
# Set to true to enable the X-Content-Type-Options response header.
|
||||
# The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME types advertised
|
||||
# in the Content-Type headers should not be changed and be followed. The default will change to true in the next minor release, 6.3.
|
||||
x_content_type_options = false
|
||||
|
||||
# Set to true to enable the X-XSS-Protection header, which tells browsers to stop pages from loading
|
||||
# when they detect reflected cross-site scripting (XSS) attacks. The default will change to true in the next minor release, 6.3.
|
||||
x_xss_protection = false
|
||||
|
||||
|
||||
#################################### Snapshots ###########################
|
||||
[snapshots]
|
||||
# snapshot sharing options
|
||||
|
||||
@@ -109,7 +109,7 @@ log_queries =
|
||||
|
||||
# cache connectionstring options
|
||||
# database: will use Grafana primary database.
|
||||
# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana`
|
||||
# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=0`. Only addr is required.
|
||||
# memcache: 127.0.0.1:11211
|
||||
;connstr =
|
||||
|
||||
@@ -175,6 +175,30 @@ log_queries =
|
||||
# set to true if you want to allow browsers to render Grafana in a <frame>, <iframe>, <embed> or <object>. default is false.
|
||||
;allow_embedding = false
|
||||
|
||||
# Set to true if you want to enable http strict transport security (HSTS) response header.
|
||||
# This is only sent when HTTPS is enabled in this configuration.
|
||||
# HSTS tells browsers that the site should only be accessed using HTTPS.
|
||||
# The default version will change to true in the next minor release, 6.3.
|
||||
;strict_transport_security = false
|
||||
|
||||
# Sets how long a browser should cache HSTS. Only applied if strict_transport_security is enabled.
|
||||
;strict_transport_security_max_age_seconds = 86400
|
||||
|
||||
# Set to true if to enable HSTS preloading option. Only applied if strict_transport_security is enabled.
|
||||
;strict_transport_security_preload = false
|
||||
|
||||
# Set to true if to enable the HSTS includeSubDomains option. Only applied if strict_transport_security is enabled.
|
||||
;strict_transport_security_subdomains = false
|
||||
|
||||
# Set to true to enable the X-Content-Type-Options response header.
|
||||
# The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME types advertised
|
||||
# in the Content-Type headers should not be changed and be followed. The default will change to true in the next minor release, 6.3.
|
||||
;x_content_type_options = false
|
||||
|
||||
# Set to true to enable the X-XSS-Protection header, which tells browsers to stop pages from loading
|
||||
# when they detect reflected cross-site scripting (XSS) attacks. The default will change to true in the next minor release, 6.3.
|
||||
;x_xss_protection = false
|
||||
|
||||
#################################### Snapshots ###########################
|
||||
[snapshots]
|
||||
# snapshot sharing options
|
||||
|
||||
1337
devenv/dev-dashboards/datasource-testdata/new_features_in_v62.json
Normal file
1337
devenv/dev-dashboards/datasource-testdata/new_features_in_v62.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -48,28 +48,6 @@
|
||||
"y": 0
|
||||
},
|
||||
"headings": false,
|
||||
"id": 8,
|
||||
"limit": 1000,
|
||||
"links": [],
|
||||
"query": "",
|
||||
"recent": false,
|
||||
"search": true,
|
||||
"starred": false,
|
||||
"tags": ["panel-demo"],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "tag: panel-demo",
|
||||
"type": "dashlist"
|
||||
},
|
||||
{
|
||||
"folderId": null,
|
||||
"gridPos": {
|
||||
"h": 13,
|
||||
"w": 6,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"headings": false,
|
||||
"id": 2,
|
||||
"limit": 1000,
|
||||
"links": [],
|
||||
@@ -83,6 +61,28 @@
|
||||
"title": "tag: panel-tests",
|
||||
"type": "dashlist"
|
||||
},
|
||||
{
|
||||
"folderId": null,
|
||||
"gridPos": {
|
||||
"h": 26,
|
||||
"w": 6,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"headings": false,
|
||||
"id": 3,
|
||||
"limit": 1000,
|
||||
"links": [],
|
||||
"query": "",
|
||||
"recent": false,
|
||||
"search": true,
|
||||
"starred": false,
|
||||
"tags": ["gdev", "demo"],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "tag: dashboard-demo",
|
||||
"type": "dashlist"
|
||||
},
|
||||
{
|
||||
"folderId": null,
|
||||
"gridPos": {
|
||||
@@ -114,28 +114,6 @@
|
||||
"y": 13
|
||||
},
|
||||
"headings": false,
|
||||
"id": 3,
|
||||
"limit": 1000,
|
||||
"links": [],
|
||||
"query": "",
|
||||
"recent": false,
|
||||
"search": true,
|
||||
"starred": false,
|
||||
"tags": ["gdev", "demo"],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "tag: dashboard-demo",
|
||||
"type": "dashlist"
|
||||
},
|
||||
{
|
||||
"folderId": null,
|
||||
"gridPos": {
|
||||
"h": 13,
|
||||
"w": 6,
|
||||
"x": 12,
|
||||
"y": 13
|
||||
},
|
||||
"headings": false,
|
||||
"id": 4,
|
||||
"limit": 1000,
|
||||
"links": [],
|
||||
@@ -167,5 +145,5 @@
|
||||
"timezone": "",
|
||||
"title": "Grafana Dev Overview & Home",
|
||||
"uid": "j6T00KRZz",
|
||||
"version": 1
|
||||
"version": 2
|
||||
}
|
||||
|
||||
@@ -15,26 +15,27 @@
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": 7501,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": "gdev-testdata",
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 18,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 7,
|
||||
"id": 2,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "gradient",
|
||||
"displayMode": "lcd",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "watt"
|
||||
"unit": "decgbytes"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
@@ -47,7 +48,7 @@
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 1,
|
||||
"value": 40
|
||||
"value": 60
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -59,95 +60,188 @@
|
||||
},
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "sda1",
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda2",
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda3",
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda4",
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda5",
|
||||
"refId": "E",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "10003,33333"
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda6",
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda7",
|
||||
"refId": "G",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda8",
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda9",
|
||||
"refId": "I",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda10",
|
||||
"refId": "J",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda11",
|
||||
"refId": "K",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda12",
|
||||
"refId": "L",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda13",
|
||||
"refId": "M",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda14",
|
||||
"refId": "N",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda15",
|
||||
"refId": "O",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda16",
|
||||
"refId": "P",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "Q",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Usage",
|
||||
"title": "",
|
||||
"transparent": true,
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"datasource": "gdev-testdata",
|
||||
"gridPos": {
|
||||
"h": 22,
|
||||
"w": 6,
|
||||
"x": 18,
|
||||
"y": 0
|
||||
"h": 10,
|
||||
"w": 16,
|
||||
"x": 0,
|
||||
"y": 7
|
||||
},
|
||||
"id": 8,
|
||||
"id": 4,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "gradient",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "celsius"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "blue",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"index": 1,
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 2,
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 3,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "Inside",
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"alias": "Outhouse",
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Area B",
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Basement",
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Garage",
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Gradient mode",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"datasource": "gdev-testdata",
|
||||
"gridPos": {
|
||||
"h": 10,
|
||||
"w": 6,
|
||||
"x": 16,
|
||||
"y": 7
|
||||
},
|
||||
"id": 6,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "basic",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
@@ -160,19 +254,24 @@
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "green",
|
||||
"color": "blue",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"color": "green",
|
||||
"index": 1,
|
||||
"value": 55
|
||||
"value": 42.5
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 2,
|
||||
"value": 80
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 2,
|
||||
"value": 95
|
||||
"index": 3,
|
||||
"value": 90
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
@@ -181,10 +280,6 @@
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "E",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
@@ -194,22 +289,6 @@
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "I",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "J",
|
||||
"scenarioId": "random_walk"
|
||||
@@ -241,47 +320,78 @@
|
||||
{
|
||||
"refId": "Q",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Basic",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
"datasource": "gdev-testdata",
|
||||
"gridPos": {
|
||||
"h": 22,
|
||||
"w": 2,
|
||||
"x": 22,
|
||||
"y": 7
|
||||
},
|
||||
"id": 8,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "lcd",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"max": 100,
|
||||
"min": 0
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "red",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"refId": "G",
|
||||
"scenarioId": "random_walk"
|
||||
"color": "red",
|
||||
"index": 1,
|
||||
"value": 90
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
{
|
||||
"refId": "R",
|
||||
"scenarioId": "random_walk"
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"refId": "S",
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Usage",
|
||||
"title": "Completion",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"datasource": "gdev-testdata",
|
||||
"gridPos": {
|
||||
"h": 15,
|
||||
"w": 11,
|
||||
"h": 12,
|
||||
"w": 22,
|
||||
"x": 0,
|
||||
"y": 7
|
||||
"y": 17
|
||||
},
|
||||
"id": 6,
|
||||
"id": 10,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "gradient",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "celsius"
|
||||
"unit": "decgbytes"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
@@ -294,12 +404,12 @@
|
||||
{
|
||||
"color": "green",
|
||||
"index": 1,
|
||||
"value": 20
|
||||
"value": 30
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 2,
|
||||
"value": 40
|
||||
"value": 60
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -309,69 +419,113 @@
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal"
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "Inside",
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"alias": "Outhouse",
|
||||
"alias": "sda1",
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Area B",
|
||||
"alias": "sda2",
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Basement",
|
||||
"alias": "sda3",
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Garage",
|
||||
"alias": "sda4",
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Attic",
|
||||
"alias": "sda5",
|
||||
"refId": "E",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda6",
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda7",
|
||||
"refId": "G",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda8",
|
||||
"refId": "H",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda9",
|
||||
"refId": "I",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda10",
|
||||
"refId": "J",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda11",
|
||||
"refId": "K",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda12",
|
||||
"refId": "L",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda13",
|
||||
"refId": "M",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda14",
|
||||
"refId": "N",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda15",
|
||||
"refId": "O",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda16",
|
||||
"refId": "P",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Temperature",
|
||||
"title": "",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"datasource": "gdev-testdata",
|
||||
"gridPos": {
|
||||
"h": 15,
|
||||
"w": 7,
|
||||
"x": 11,
|
||||
"y": 7
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 29
|
||||
},
|
||||
"id": 9,
|
||||
"id": 11,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "basic",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "celsius"
|
||||
"unit": "decgbytes"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
@@ -384,12 +538,12 @@
|
||||
{
|
||||
"color": "green",
|
||||
"index": 1,
|
||||
"value": 20
|
||||
"value": 30
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 2,
|
||||
"value": 40
|
||||
"value": 60
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -399,89 +553,113 @@
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal"
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "Inside",
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"alias": "Outhouse",
|
||||
"alias": "sda1",
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Area B",
|
||||
"alias": "sda2",
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Basement",
|
||||
"alias": "sda3",
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Garage",
|
||||
"alias": "sda4",
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Attic",
|
||||
"alias": "sda5",
|
||||
"refId": "E",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda6",
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda7",
|
||||
"refId": "G",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda8",
|
||||
"refId": "H",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda9",
|
||||
"refId": "I",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda10",
|
||||
"refId": "J",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda11",
|
||||
"refId": "K",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda12",
|
||||
"refId": "L",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda13",
|
||||
"refId": "M",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda14",
|
||||
"refId": "N",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda15",
|
||||
"refId": "O",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "sda16",
|
||||
"refId": "P",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Temperature",
|
||||
"title": "",
|
||||
"type": "bargauge"
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
"refresh": "10s",
|
||||
"schemaVersion": 18,
|
||||
"style": "dark",
|
||||
"tags": ["gdev", "bargauge", "panel-demo"],
|
||||
"tags": ["gdev", "demo"],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-30m",
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": ["1s", "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
|
||||
"refresh_intervals": ["2s", "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
|
||||
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Bar Gauge Animated Demo",
|
||||
"uid": "k5IUwQeikaa",
|
||||
"version": 1
|
||||
"title": "Bar Gauge Demo",
|
||||
"uid": "vmie2cmWz",
|
||||
"version": 3
|
||||
}
|
||||
@@ -1,376 +0,0 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 18,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 7,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "gradient",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "watt"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 1,
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 2,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "E",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "10003,33333"
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "G",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"refId": "I",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "J",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "K",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "L",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "M",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "N",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "O",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "P",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "Q",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Usage",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 20,
|
||||
"w": 6,
|
||||
"x": 18,
|
||||
"y": 0
|
||||
},
|
||||
"id": 8,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "gradient",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "watt"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 1,
|
||||
"value": 65
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 2,
|
||||
"value": 95
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "E",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "I",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "J",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "K",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "L",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "M",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "N",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "O",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "P",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "Q",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "G",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "R",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "S",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Usage",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 13,
|
||||
"w": 18,
|
||||
"x": 0,
|
||||
"y": 7
|
||||
},
|
||||
"id": 6,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "gradient",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "celsius"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "blue",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"index": 1,
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 2,
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 3,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "Inside",
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"alias": "Outhouse",
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Area B",
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Basement",
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Garage",
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Attic",
|
||||
"refId": "E",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Temperature",
|
||||
"type": "bargauge"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 18,
|
||||
"style": "dark",
|
||||
"tags": ["gdev", "bargauge", "panel-demo"],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
|
||||
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Bar Gauge Gradient Demo",
|
||||
"uid": "RndRQw6mz",
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,405 +0,0 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 22,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 7,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "lcd",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "watt"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 1,
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 2,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "E",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "10003,33333"
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "G",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"refId": "I",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "J",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "K",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "L",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "M",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "N",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "O",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "P",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "Q",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Usage",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 20,
|
||||
"w": 2,
|
||||
"x": 22,
|
||||
"y": 0
|
||||
},
|
||||
"id": 11,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "lcd",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "percent"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 1,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Progress",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 13,
|
||||
"w": 10,
|
||||
"x": 0,
|
||||
"y": 7
|
||||
},
|
||||
"id": 6,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "gradient",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "celsius"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "blue",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"index": 1,
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 2,
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 3,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "Inside",
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"alias": "Outhouse",
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Area B",
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Basement",
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Garage",
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Attic",
|
||||
"refId": "E",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Temperature",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 13,
|
||||
"w": 12,
|
||||
"x": 10,
|
||||
"y": 7
|
||||
},
|
||||
"id": 8,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "basic",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "watt"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "purple",
|
||||
"index": 1,
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"color": "blue",
|
||||
"index": 2,
|
||||
"value": 70
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "I",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "J",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "K",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "L",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "M",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "N",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "O",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "P",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "Q",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Usage",
|
||||
"type": "bargauge"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 18,
|
||||
"style": "dark",
|
||||
"tags": ["gdev", "bargauge", "panel-demo"],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
|
||||
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Bar Gauge All Modes Demo",
|
||||
"uid": "zt2f6NgZzaa",
|
||||
"version": 1
|
||||
}
|
||||
829
devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge.json
Normal file
829
devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge.json
Normal file
@@ -0,0 +1,829 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 6,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "gradient",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "celsius"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "blue",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"index": 1,
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 2,
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 3,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "Inside",
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"alias": "Outhouse",
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Title above bar",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 5,
|
||||
"x": 6,
|
||||
"y": 0
|
||||
},
|
||||
"id": 12,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "gradient",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "celsius"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "blue",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"index": 1,
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 2,
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 3,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "Inside",
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"alias": "Outhouse",
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Title to left of bar",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 7,
|
||||
"x": 11,
|
||||
"y": 0
|
||||
},
|
||||
"id": 13,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "basic",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "celsius"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "blue",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"index": 1,
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 2,
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 3,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "Inside",
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"alias": "Outhouse",
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Basic mode",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 6,
|
||||
"x": 18,
|
||||
"y": 0
|
||||
},
|
||||
"id": 14,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "lcd",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "celsius"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "blue",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"index": 1,
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 2,
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 3,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "Inside",
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"alias": "Outhouse",
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "LED",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 11,
|
||||
"x": 0,
|
||||
"y": 7
|
||||
},
|
||||
"id": 7,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "lcd",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "watt"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 1,
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 2,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "E",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "10003,33333"
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "G",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"refId": "I",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "J",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "K",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "L",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "M",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "N",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "O",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "P",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "Q",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "LED Vertical",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 13,
|
||||
"x": 11,
|
||||
"y": 7
|
||||
},
|
||||
"id": 8,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "basic",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "watt"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "purple",
|
||||
"index": 1,
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"color": "blue",
|
||||
"index": 2,
|
||||
"value": 70
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "I",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "J",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "K",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "L",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "M",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "N",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "O",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "P",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "Q",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Basic vertical ",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 11,
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"id": 16,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "lcd",
|
||||
"fieldOptions": {
|
||||
"calcs": ["last"],
|
||||
"defaults": {
|
||||
"max": 100,
|
||||
"min": 0
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "blue",
|
||||
"index": 1,
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 2,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
"pluginVersion": "6.3.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0,-100"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Negative value below min",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 3,
|
||||
"x": 11,
|
||||
"y": 16
|
||||
},
|
||||
"id": 17,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "lcd",
|
||||
"fieldOptions": {
|
||||
"calcs": ["last"],
|
||||
"defaults": {
|
||||
"max": 100,
|
||||
"min": 0
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "blue",
|
||||
"index": 1,
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 2,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"pluginVersion": "6.3.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0,-100"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Negative value below min",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 3,
|
||||
"x": 14,
|
||||
"y": 16
|
||||
},
|
||||
"id": 18,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "lcd",
|
||||
"fieldOptions": {
|
||||
"calcs": ["last"],
|
||||
"defaults": {
|
||||
"max": 100,
|
||||
"min": -10
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "blue",
|
||||
"index": 1,
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 2,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"pluginVersion": "6.3.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,6"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Positive value above min",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 3,
|
||||
"x": 17,
|
||||
"y": 16
|
||||
},
|
||||
"id": 19,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "lcd",
|
||||
"fieldOptions": {
|
||||
"calcs": ["last"],
|
||||
"defaults": {
|
||||
"max": 35,
|
||||
"min": -20
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "blue",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"index": 1,
|
||||
"value": 5
|
||||
},
|
||||
{
|
||||
"color": "#EAB839",
|
||||
"index": 2,
|
||||
"value": 25
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 3,
|
||||
"value": 30
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"pluginVersion": "6.3.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,6"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Negative min ",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 4,
|
||||
"x": 20,
|
||||
"y": 16
|
||||
},
|
||||
"id": 20,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "gradient",
|
||||
"fieldOptions": {
|
||||
"calcs": ["last"],
|
||||
"defaults": {
|
||||
"max": 35,
|
||||
"min": -20
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "blue",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"index": 1,
|
||||
"value": 5
|
||||
},
|
||||
{
|
||||
"color": "#EAB839",
|
||||
"index": 2,
|
||||
"value": 25
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 3,
|
||||
"value": 30
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"pluginVersion": "6.3.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "30,30"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Negative min",
|
||||
"type": "bargauge"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 18,
|
||||
"style": "dark",
|
||||
"tags": ["gdev", "panel-tests"],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
|
||||
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Panel Tests - Bar Gauge",
|
||||
"uid": "O6f11TZWk",
|
||||
"version": 12
|
||||
}
|
||||
@@ -1,400 +0,0 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 22,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 7,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "lcd",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "watt"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 1,
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 2,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "E",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "10003,33333"
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "G",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"refId": "I",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "J",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "K",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "L",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "M",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "N",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "O",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "P",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "Q",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Usage",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 21,
|
||||
"w": 2,
|
||||
"x": 22,
|
||||
"y": 0
|
||||
},
|
||||
"id": 11,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "lcd",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "percent"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 1,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Progress",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 13,
|
||||
"w": 10,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 6,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "lcd",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "celsius"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 1,
|
||||
"value": 40
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 2,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "Inside",
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"alias": "Outhouse",
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Area B",
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Basement",
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Garage",
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"alias": "Attic",
|
||||
"refId": "E",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Temperature",
|
||||
"type": "bargauge"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 13,
|
||||
"w": 12,
|
||||
"x": 10,
|
||||
"y": 8
|
||||
},
|
||||
"id": 8,
|
||||
"links": [],
|
||||
"options": {
|
||||
"displayMode": "lcd",
|
||||
"fieldOptions": {
|
||||
"calcs": ["mean"],
|
||||
"defaults": {
|
||||
"decimals": null,
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"unit": "watt"
|
||||
},
|
||||
"mappings": [],
|
||||
"override": {},
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "orange",
|
||||
"index": 1,
|
||||
"value": 85
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"index": 2,
|
||||
"value": 95
|
||||
}
|
||||
],
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
"pluginVersion": "6.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "H",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "100,100,100"
|
||||
},
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "I",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "J",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "K",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "L",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "M",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "N",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "O",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "P",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "Q",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Usage",
|
||||
"type": "bargauge"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 18,
|
||||
"style": "dark",
|
||||
"tags": ["gdev", "bargauge", "panel-demo"],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
|
||||
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Bar Gauge LED Demo",
|
||||
"uid": "0G3rbkqmkaa",
|
||||
"version": 1
|
||||
}
|
||||
@@ -320,6 +320,30 @@ When `false`, the HTTP header `X-Frame-Options: deny` will be set in Grafana HTT
|
||||
browsers to not allow rendering Grafana in a `<frame>`, `<iframe>`, `<embed>` or `<object>`. The main goal is to
|
||||
mitigate the risk of [Clickjacking](https://www.owasp.org/index.php/Clickjacking). Default is `false`.
|
||||
|
||||
### strict_transport_security
|
||||
|
||||
Set to `true` if you want to enable http `Strict-Transport-Security` (HSTS) response header. This is only sent when HTTPS is enabled in this configuration. HSTS tells browsers that the site should only be accessed using HTTPS. The default value is `false` until the next minor release, `6.3`.
|
||||
|
||||
### strict_transport_security_max_age_seconds
|
||||
|
||||
Sets how long a browser should cache HSTS in seconds. Only applied if strict_transport_security is enabled. The default value is `86400`.
|
||||
|
||||
### strict_transport_security_preload
|
||||
|
||||
Set to `true` if to enable HSTS `preloading` option. Only applied if strict_transport_security is enabled. The default value is `false`.
|
||||
|
||||
### strict_transport_security_subdomains
|
||||
|
||||
Set to `true` if to enable the HSTS includeSubDomains option. Only applied if strict_transport_security is enabled. The default value is `false`.
|
||||
|
||||
### x_content_type_options
|
||||
|
||||
Set to `true` to enable the X-Content-Type-Options response header. The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME types advertised in the Content-Type headers should not be changed and be followed. The default value is `false` until the next minor release, `6.3`.
|
||||
|
||||
### x_xss_protection
|
||||
|
||||
Set to `false` to disable the X-XSS-Protection header, which tells browsers to stop pages from loading when they detect reflected cross-site scripting (XSS) attacks. The default value is `false` until the next minor release, `6.3`.
|
||||
|
||||
<hr />
|
||||
|
||||
## [users]
|
||||
|
||||
@@ -51,6 +51,36 @@ then the Grafana proxy will transform it into "https://management.azure.com/foo/
|
||||
|
||||
The `method` parameter is optional. It can be set to any HTTP verb to provide more fine-grained control.
|
||||
|
||||
### Dynamic Routes
|
||||
|
||||
When using routes, you can also reference a variable stored in JsonData or SecureJsonData which will be interpolated when connecting to the datasource.
|
||||
|
||||
With JsonData:
|
||||
```json
|
||||
"routes": [
|
||||
{
|
||||
"path": "custom/api/v5/*",
|
||||
"method": "*",
|
||||
"url": "{{.JsonData.dynamicUrl}}",
|
||||
...
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
With SecureJsonData:
|
||||
```json
|
||||
"routes": [{
|
||||
"path": "custom/api/v5/*",
|
||||
"method": "*",
|
||||
"url": "{{.SecureJsonData.dynamicUrl}}",
|
||||
...
|
||||
}]
|
||||
```
|
||||
|
||||
In the above example, the app is able to set the value for `dynamicUrl` in JsonData or SecureJsonData and it will be replaced on-demand.
|
||||
|
||||
An app using this feature can be found [here](https://github.com/grafana/kentik-app).
|
||||
|
||||
## Encrypting Sensitive Data
|
||||
|
||||
When a user saves a password or secret with your datasource plugin's Config page, then you can save data to a column in the datasource table called `secureJsonData` that is an encrypted blob. Any data saved in the blob is encrypted by Grafana and can only be decrypted by the Grafana server on the backend. This means once a password is saved, no sensitive data is sent to the browser. If the password is saved in the `jsonData` blob or the `password` field then it is unencrypted and anyone with Admin access (with the help of Chrome Developer Tools) can read it.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"company": "Grafana Labs"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "6.2.0-pre",
|
||||
"version": "6.2.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
@@ -191,7 +191,7 @@
|
||||
"eventemitter3": "2.0.3",
|
||||
"file-saver": "1.3.8",
|
||||
"immutable": "3.8.2",
|
||||
"jquery": "3.4.0",
|
||||
"jquery": "3.4.1",
|
||||
"lodash": "4.17.11",
|
||||
"moment": "2.24.0",
|
||||
"mousetrap": "1.6.3",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@types/react-color": "2.17.0",
|
||||
"classnames": "2.2.6",
|
||||
"d3": "5.9.1",
|
||||
"jquery": "3.4.0",
|
||||
"jquery": "3.4.1",
|
||||
"lodash": "4.17.11",
|
||||
"moment": "2.24.0",
|
||||
"papaparse": "4.6.3",
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { BarGauge, Props, getValueColor, getBasicAndGradientStyles, getBarGradient, getTitleStyles } from './BarGauge';
|
||||
import {
|
||||
BarGauge,
|
||||
Props,
|
||||
getValueColor,
|
||||
getBasicAndGradientStyles,
|
||||
getBarGradient,
|
||||
getTitleStyles,
|
||||
getValuePercent,
|
||||
} from './BarGauge';
|
||||
import { VizOrientation, DisplayValue } from '../../types';
|
||||
import { getTheme } from '../../themes';
|
||||
|
||||
@@ -63,6 +71,24 @@ describe('BarGauge', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get value percent', () => {
|
||||
it('0 to 100 and value 40', () => {
|
||||
expect(getValuePercent(40, 0, 100)).toEqual(0.4);
|
||||
});
|
||||
|
||||
it('50 to 100 and value 75', () => {
|
||||
expect(getValuePercent(75, 50, 100)).toEqual(0.5);
|
||||
});
|
||||
|
||||
it('-30 to 30 and value 0', () => {
|
||||
expect(getValuePercent(0, -30, 30)).toEqual(0.5);
|
||||
});
|
||||
|
||||
it('-30 to 30 and value 30', () => {
|
||||
expect(getValuePercent(30, -30, 30)).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Vertical bar without title', () => {
|
||||
it('should not include title height in height', () => {
|
||||
const props = getProps({
|
||||
|
||||
@@ -11,8 +11,9 @@ import { DisplayValue, Themeable, TimeSeriesValue, Threshold, VizOrientation } f
|
||||
const MIN_VALUE_HEIGHT = 18;
|
||||
const MAX_VALUE_HEIGHT = 50;
|
||||
const MIN_VALUE_WIDTH = 50;
|
||||
const MAX_VALUE_WIDTH = 100;
|
||||
const LINE_HEIGHT = 1.5;
|
||||
const MAX_VALUE_WIDTH = 150;
|
||||
const TITLE_LINE_HEIGHT = 1.5;
|
||||
const VALUE_LINE_HEIGHT = 1;
|
||||
|
||||
export interface Props extends Themeable {
|
||||
height: number;
|
||||
@@ -161,7 +162,7 @@ export class BarGauge extends PureComponent<Props> {
|
||||
const cells: JSX.Element[] = [];
|
||||
|
||||
for (let i = 0; i < cellCount; i++) {
|
||||
const currentValue = (valueRange / cellCount) * i;
|
||||
const currentValue = minValue + (valueRange / cellCount) * i;
|
||||
const cellColor = this.getCellColor(currentValue);
|
||||
const cellStyles: CSSProperties = {
|
||||
borderRadius: '2px',
|
||||
@@ -227,7 +228,7 @@ function calculateTitleDimensions(props: Props): TitleDimensions {
|
||||
return {
|
||||
fontSize: 14,
|
||||
width: width,
|
||||
height: 14 * LINE_HEIGHT,
|
||||
height: 14 * TITLE_LINE_HEIGHT,
|
||||
placement: 'below',
|
||||
};
|
||||
}
|
||||
@@ -238,7 +239,7 @@ function calculateTitleDimensions(props: Props): TitleDimensions {
|
||||
const titleHeight = Math.max(Math.min(height * maxTitleHeightRatio, MAX_VALUE_HEIGHT), 17);
|
||||
|
||||
return {
|
||||
fontSize: titleHeight / LINE_HEIGHT,
|
||||
fontSize: titleHeight / TITLE_LINE_HEIGHT,
|
||||
width: 0,
|
||||
height: titleHeight,
|
||||
placement: 'above',
|
||||
@@ -251,7 +252,7 @@ function calculateTitleDimensions(props: Props): TitleDimensions {
|
||||
const titleHeight = Math.max(height * maxTitleHeightRatio, MIN_VALUE_HEIGHT);
|
||||
|
||||
return {
|
||||
fontSize: titleHeight / LINE_HEIGHT,
|
||||
fontSize: titleHeight / TITLE_LINE_HEIGHT,
|
||||
height: 0,
|
||||
width: Math.min(Math.max(width * maxTitleWidthRatio, 50), 200),
|
||||
placement: 'left',
|
||||
@@ -345,11 +346,6 @@ function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions {
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('titleDim', titleDim);
|
||||
// console.log('valueWidth', valueWidth);
|
||||
// console.log('width', width);
|
||||
// console.log('total', titleDim.width + maxBarWidth + valueWidth);
|
||||
|
||||
return {
|
||||
valueWidth,
|
||||
valueHeight,
|
||||
@@ -360,6 +356,10 @@ function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions {
|
||||
};
|
||||
}
|
||||
|
||||
export function getValuePercent(value: number, minValue: number, maxValue: number): number {
|
||||
return Math.min((value - minValue) / (maxValue - minValue), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only exported to for unit test
|
||||
*/
|
||||
@@ -367,7 +367,7 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles
|
||||
const { displayMode, maxValue, minValue, value } = props;
|
||||
const { valueWidth, valueHeight, maxBarHeight, maxBarWidth } = calculateBarAndValueDimensions(props);
|
||||
|
||||
const valuePercent = Math.min(value.numeric / (maxValue - minValue), 1);
|
||||
const valuePercent = getValuePercent(value.numeric, minValue, maxValue);
|
||||
const valueColor = getValueColor(props);
|
||||
const valueStyles = getValueStyles(value.text, valueColor, valueWidth, valueHeight);
|
||||
const isBasic = displayMode === 'basic';
|
||||
@@ -450,7 +450,7 @@ export function getBarGradient(props: Props, maxSize: number): string {
|
||||
for (let i = 0; i < thresholds.length; i++) {
|
||||
const threshold = thresholds[i];
|
||||
const color = getColorFromHexRgbOrName(threshold.color);
|
||||
const valuePercent = Math.min(threshold.value / (maxValue - minValue), 1);
|
||||
const valuePercent = getValuePercent(threshold.value, minValue, maxValue);
|
||||
const pos = valuePercent * maxSize;
|
||||
const offset = Math.round(pos - (pos - lastpos) / 2);
|
||||
|
||||
@@ -486,7 +486,7 @@ export function getValueColor(props: Props): string {
|
||||
* Only exported to for unit test
|
||||
*/
|
||||
function getValueStyles(value: string, color: string, width: number, height: number): CSSProperties {
|
||||
const heightFont = height / LINE_HEIGHT;
|
||||
const heightFont = height / VALUE_LINE_HEIGHT;
|
||||
const guess = width / (value.length * 1.1);
|
||||
const fontSize = Math.min(Math.max(guess, 14), heightFont);
|
||||
|
||||
@@ -496,33 +496,15 @@ function getValueStyles(value: string, color: string, width: number, height: num
|
||||
width: `${width}px`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: fontSize.toFixed(2) + 'px',
|
||||
lineHeight: VALUE_LINE_HEIGHT,
|
||||
fontSize: fontSize.toFixed(4) + 'px',
|
||||
};
|
||||
}
|
||||
|
||||
// let canvasElement: HTMLCanvasElement | null = null;
|
||||
//
|
||||
// interface TextDimensions {
|
||||
// width: number;
|
||||
// height: number;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
|
||||
// *
|
||||
// * @param {String} text The text to be rendered.
|
||||
// * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
|
||||
// *
|
||||
// * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
|
||||
// */
|
||||
// function getTextWidth(text: string): number {
|
||||
// // re-use canvas object for better performance
|
||||
// canvasElement = canvasElement || document.createElement('canvas');
|
||||
// const context = canvasElement.getContext('2d');
|
||||
// if (context) {
|
||||
// context.font = 'normal 16px Roboto';
|
||||
// const metrics = context.measureText(text);
|
||||
// const canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
|
||||
// var context = canvas.getContext("2d");
|
||||
// context.font = "'Roboto', 'Helvetica Neue', Arial, sans-serif";
|
||||
// var metrics = context.measureText(text);
|
||||
// return metrics.width;
|
||||
// }
|
||||
// return 16;
|
||||
// }
|
||||
|
||||
@@ -18,8 +18,9 @@ exports[`BarGauge Render with basic options should render 1`] = `
|
||||
"alignItems": "center",
|
||||
"color": "#73BF69",
|
||||
"display": "flex",
|
||||
"fontSize": "27.27px",
|
||||
"fontSize": "27.2727px",
|
||||
"height": "300px",
|
||||
"lineHeight": 1,
|
||||
"paddingLeft": "10px",
|
||||
"width": "60px",
|
||||
}
|
||||
|
||||
@@ -42,9 +42,10 @@ export class CustomScrollbar extends Component<Props> {
|
||||
|
||||
updateScroll() {
|
||||
const ref = this.ref.current;
|
||||
const { scrollTop } = this.props;
|
||||
|
||||
if (ref && !isNil(this.props.scrollTop)) {
|
||||
ref.scrollTop(this.props.scrollTop);
|
||||
if (ref && !isNil(scrollTop)) {
|
||||
ref.scrollTop(scrollTop);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +71,44 @@ export class CustomScrollbar extends Component<Props> {
|
||||
this.updateScroll();
|
||||
}
|
||||
|
||||
renderTrack = (track: 'track-vertical' | 'track-horizontal', hideTrack: boolean | undefined, passedProps: any) => {
|
||||
return (
|
||||
<div
|
||||
{...passedProps}
|
||||
className={cx(
|
||||
css`
|
||||
visibility: ${hideTrack ? 'none' : 'visible'};
|
||||
`,
|
||||
track
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderThumb = (thumb: 'thumb-horizontal' | 'thumb-vertical', passedProps: any) => {
|
||||
return <div {...passedProps} className={thumb} />;
|
||||
};
|
||||
|
||||
renderTrackHorizontal = (passedProps: any) => {
|
||||
return this.renderTrack('track-horizontal', this.props.hideHorizontalTrack, passedProps);
|
||||
};
|
||||
|
||||
renderTrackVertical = (passedProps: any) => {
|
||||
return this.renderTrack('track-vertical', this.props.hideVerticalTrack, passedProps);
|
||||
};
|
||||
|
||||
renderThumbHorizontal = (passedProps: any) => {
|
||||
return this.renderThumb('thumb-horizontal', passedProps);
|
||||
};
|
||||
|
||||
renderThumbVertical = (passedProps: any) => {
|
||||
return this.renderThumb('thumb-vertical', passedProps);
|
||||
};
|
||||
|
||||
renderView = (passedProps: any) => {
|
||||
return <div {...passedProps} className="view" />;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
@@ -80,8 +119,6 @@ export class CustomScrollbar extends Component<Props> {
|
||||
autoHide,
|
||||
autoHideTimeout,
|
||||
hideTracksWhenNotNeeded,
|
||||
hideHorizontalTrack,
|
||||
hideVerticalTrack,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -97,31 +134,11 @@ export class CustomScrollbar extends Component<Props> {
|
||||
// Before these where set to inhert but that caused problems with cut of legends in firefox
|
||||
autoHeightMax={autoHeightMax}
|
||||
autoHeightMin={autoHeightMin}
|
||||
renderTrackHorizontal={props => (
|
||||
<div
|
||||
{...props}
|
||||
className={cx(
|
||||
css`
|
||||
visibility: ${hideHorizontalTrack ? 'none' : 'visible'};
|
||||
`,
|
||||
'track-horizontal'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
renderTrackVertical={props => (
|
||||
<div
|
||||
{...props}
|
||||
className={cx(
|
||||
css`
|
||||
visibility: ${hideVerticalTrack ? 'none' : 'visible'};
|
||||
`,
|
||||
'track-vertical'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
|
||||
renderThumbVertical={props => <div {...props} className="thumb-vertical" />}
|
||||
renderView={props => <div {...props} className="view" />}
|
||||
renderTrackHorizontal={this.renderTrackHorizontal}
|
||||
renderTrackVertical={this.renderTrackVertical}
|
||||
renderThumbHorizontal={this.renderThumbHorizontal}
|
||||
renderThumbVertical={this.renderThumbVertical}
|
||||
renderView={this.renderView}
|
||||
>
|
||||
{children}
|
||||
</Scrollbars>
|
||||
|
||||
@@ -37,7 +37,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="css-17l4171 track-horizontal"
|
||||
className="css-52gpmd track-horizontal"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
@@ -58,7 +58,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="css-17l4171 track-vertical"
|
||||
className="css-52gpmd track-vertical"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
|
||||
@@ -58,7 +58,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
if (length > 12) {
|
||||
return FONT_SCALE - (length * 5) / 110;
|
||||
}
|
||||
return FONT_SCALE - (length * 5) / 100;
|
||||
return FONT_SCALE - (length * 5) / 101;
|
||||
}
|
||||
|
||||
draw() {
|
||||
@@ -69,16 +69,17 @@ export class Gauge extends PureComponent<Props> {
|
||||
|
||||
const backgroundColor = selectThemeVariant(
|
||||
{
|
||||
dark: theme.colors.dark3,
|
||||
light: '#e6e6e6',
|
||||
dark: theme.colors.dark8,
|
||||
light: theme.colors.gray6,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
|
||||
const gaugeWidth = Math.min(dimension / 6, 40) / gaugeWidthReduceRatio;
|
||||
const gaugeWidth = Math.min(dimension / 5.5, 40) / gaugeWidthReduceRatio;
|
||||
const thresholdMarkersWidth = gaugeWidth / 5;
|
||||
const fontSize = Math.min(dimension / 5.5, 100) * (value.text !== null ? this.getFontScale(value.text.length) : 1);
|
||||
const fontSize = Math.min(dimension / 4, 100) * (value.text !== null ? this.getFontScale(value.text.length) : 1);
|
||||
|
||||
const thresholdLabelFontSize = fontSize / 2.5;
|
||||
|
||||
const options: any = {
|
||||
@@ -181,7 +182,7 @@ function calculateGaugeAutoProps(width: number, height: number, title: string |
|
||||
const titleFontSize = Math.min((width * 0.15) / 1.5, 20); // 20% of height * line-height, max 40px
|
||||
const titleHeight = titleFontSize * 1.5;
|
||||
const availableHeight = showLabel ? height - titleHeight : height;
|
||||
const gaugeHeight = Math.min(availableHeight * 0.7, width);
|
||||
const gaugeHeight = Math.min(availableHeight, width);
|
||||
|
||||
return {
|
||||
showLabel,
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { FunctionComponent } from 'react';
|
||||
interface Props {
|
||||
title?: string;
|
||||
onClose?: () => void;
|
||||
children: JSX.Element | JSX.Element[] | boolean;
|
||||
children: React.ReactNode;
|
||||
onAdd?: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,59 +1,74 @@
|
||||
// Libraries
|
||||
import React, { PureComponent, ChangeEvent } from 'react';
|
||||
import React, { ChangeEvent, useState, useCallback } from 'react';
|
||||
|
||||
// Components
|
||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
||||
import { FormField } from '../FormField/FormField';
|
||||
import { FormLabel } from '../FormLabel/FormLabel';
|
||||
import { UnitPicker } from '../UnitPicker/UnitPicker';
|
||||
|
||||
// Types
|
||||
import { Field } from '../../types/data';
|
||||
import { toNumberString, toIntegerOrUndefined } from '../../utils';
|
||||
import { toIntegerOrUndefined } from '../../utils';
|
||||
import { SelectOptionItem } from '../Select/Select';
|
||||
|
||||
import { VAR_SERIES_NAME, VAR_FIELD_NAME, VAR_CALC, VAR_CELL_PREFIX } from '../../utils/fieldDisplay';
|
||||
import { PanelOptionsGroup } from '../index';
|
||||
|
||||
const labelWidth = 6;
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
options: Partial<Field>;
|
||||
value: Partial<Field>;
|
||||
onChange: (fieldProperties: Partial<Field>) => void;
|
||||
showMinMax: boolean;
|
||||
}
|
||||
|
||||
export class FieldPropertiesEditor extends PureComponent<Props> {
|
||||
onTitleChange = (event: ChangeEvent<HTMLInputElement>) =>
|
||||
this.props.onChange({ ...this.props.options, title: event.target.value });
|
||||
export const FieldPropertiesEditor: React.FC<Props> = ({ value, onChange, showMinMax }) => {
|
||||
const { unit, title } = value;
|
||||
|
||||
// @ts-ignore
|
||||
onUnitChange = (unit: SelectOptionItem<string>) => this.props.onChange({ ...this.props.value, unit: unit.value });
|
||||
const [decimals, setDecimals] = useState(
|
||||
value.decimals !== undefined && value.decimals !== null ? value.decimals.toString() : ''
|
||||
);
|
||||
const [min, setMin] = useState(value.min !== undefined && value.min !== null ? value.min.toString() : '');
|
||||
const [max, setMax] = useState(value.max !== undefined && value.max !== null ? value.max.toString() : '');
|
||||
|
||||
onDecimalChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.props.onChange({
|
||||
...this.props.options,
|
||||
decimals: toIntegerOrUndefined(event.target.value),
|
||||
});
|
||||
const onTitleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({ ...value, title: event.target.value });
|
||||
};
|
||||
|
||||
onMinChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.props.onChange({
|
||||
...this.props.options,
|
||||
min: toIntegerOrUndefined(event.target.value),
|
||||
});
|
||||
const onDecimalChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setDecimals(event.target.value);
|
||||
},
|
||||
[value.decimals, onChange]
|
||||
);
|
||||
|
||||
const onMinChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setMin(event.target.value);
|
||||
},
|
||||
[value.min, onChange]
|
||||
);
|
||||
|
||||
const onMaxChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setMax(event.target.value);
|
||||
},
|
||||
[value.max, onChange]
|
||||
);
|
||||
|
||||
const onUnitChange = (unit: SelectOptionItem<string>) => {
|
||||
onChange({ ...value, unit: unit.value });
|
||||
};
|
||||
|
||||
onMaxChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.props.onChange({
|
||||
...this.props.options,
|
||||
max: toIntegerOrUndefined(event.target.value),
|
||||
const commitChanges = useCallback(() => {
|
||||
onChange({
|
||||
...value,
|
||||
decimals: toIntegerOrUndefined(decimals),
|
||||
min: toIntegerOrUndefined(min),
|
||||
max: toIntegerOrUndefined(max),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { showMinMax, title } = this.props;
|
||||
const { unit, decimals, min, max } = this.props.options;
|
||||
}, [min, max, decimals]);
|
||||
|
||||
const titleTooltip = (
|
||||
<div>
|
||||
@@ -66,37 +81,36 @@ export class FieldPropertiesEditor extends PureComponent<Props> {
|
||||
{'$' + VAR_CELL_PREFIX + '{N}'} / {'$' + VAR_CALC}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<PanelOptionsGroup title={title}>
|
||||
<>
|
||||
<PanelOptionsGroup title="Field">
|
||||
<FormField
|
||||
label="Title"
|
||||
labelWidth={labelWidth}
|
||||
onChange={this.onTitleChange}
|
||||
value={this.props.options.title}
|
||||
onChange={onTitleChange}
|
||||
value={title}
|
||||
tooltip={titleTooltip}
|
||||
placeholder="Auto"
|
||||
/>
|
||||
|
||||
<div className="gf-form">
|
||||
<FormLabel width={labelWidth}>Unit</FormLabel>
|
||||
<UnitPicker defaultValue={unit} onChange={this.onUnitChange} />
|
||||
<UnitPicker defaultValue={unit} onChange={onUnitChange} />
|
||||
</div>
|
||||
{showMinMax && (
|
||||
<>
|
||||
<FormField
|
||||
label="Min"
|
||||
labelWidth={labelWidth}
|
||||
onChange={this.onMinChange}
|
||||
value={toNumberString(min)}
|
||||
onChange={onMinChange}
|
||||
onBlur={commitChanges}
|
||||
value={min}
|
||||
type="number"
|
||||
/>
|
||||
<FormField
|
||||
label="Max"
|
||||
labelWidth={labelWidth}
|
||||
onChange={this.onMaxChange}
|
||||
value={toNumberString(max)}
|
||||
onChange={onMaxChange}
|
||||
onBlur={commitChanges}
|
||||
value={max}
|
||||
type="number"
|
||||
/>
|
||||
</>
|
||||
@@ -105,12 +119,11 @@ export class FieldPropertiesEditor extends PureComponent<Props> {
|
||||
label="Decimals"
|
||||
labelWidth={labelWidth}
|
||||
placeholder="auto"
|
||||
onChange={this.onDecimalChange}
|
||||
value={toNumberString(decimals)}
|
||||
onChange={onDecimalChange}
|
||||
onBlur={commitChanges}
|
||||
value={decimals}
|
||||
type="number"
|
||||
/>
|
||||
</>
|
||||
</PanelOptionsGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,6 +80,13 @@ export interface DataSourcePluginMeta extends PluginMeta {
|
||||
mixed?: boolean;
|
||||
hasQueryHelp?: boolean;
|
||||
queryOptions?: PluginMetaQueryOptions;
|
||||
sort?: number;
|
||||
/**
|
||||
* By default, hidden queries are not passed to the datasource
|
||||
* Set this to true in plugin.json to have hidden queries passed to the
|
||||
* DataSource query method
|
||||
*/
|
||||
hiddenQueries?: boolean;
|
||||
}
|
||||
|
||||
interface PluginMetaQueryOptions {
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface PanelData {
|
||||
}
|
||||
|
||||
export interface PanelProps<T = any> {
|
||||
id: number; // ID within the current dashboard
|
||||
data: PanelData;
|
||||
// TODO: annotation?: PanelData;
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
|
||||
"$GF_PATHS_DATA" && \
|
||||
cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \
|
||||
cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \
|
||||
chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \
|
||||
chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS"
|
||||
chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" && \
|
||||
chmod -R 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING"
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
|
||||
|
||||
if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN {
|
||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true})
|
||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "gicon gicon-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
|
||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "gicon gicon-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/"})
|
||||
}
|
||||
|
||||
if len(appLink.Children) > 0 {
|
||||
|
||||
@@ -67,14 +67,14 @@ func (provider *accessTokenProvider) getAccessToken(data templateData) (string,
|
||||
}
|
||||
}
|
||||
|
||||
urlInterpolated, err := interpolateString(provider.route.TokenAuth.Url, data)
|
||||
urlInterpolated, err := InterpolateString(provider.route.TokenAuth.Url, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
params := make(url.Values)
|
||||
for key, value := range provider.route.TokenAuth.Params {
|
||||
interpolatedParam, err := interpolateString(value, data)
|
||||
interpolatedParam, err := InterpolateString(value, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -119,7 +119,7 @@ func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data
|
||||
conf := &jwt.Config{}
|
||||
|
||||
if val, ok := provider.route.JwtTokenAuth.Params["client_email"]; ok {
|
||||
interpolatedVal, err := interpolateString(val, data)
|
||||
interpolatedVal, err := InterpolateString(val, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -127,7 +127,7 @@ func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data
|
||||
}
|
||||
|
||||
if val, ok := provider.route.JwtTokenAuth.Params["private_key"]; ok {
|
||||
interpolatedVal, err := interpolateString(val, data)
|
||||
interpolatedVal, err := InterpolateString(val, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -135,7 +135,7 @@ func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data
|
||||
}
|
||||
|
||||
if val, ok := provider.route.JwtTokenAuth.Params["token_uri"]; ok {
|
||||
interpolatedVal, err := interpolateString(val, data)
|
||||
interpolatedVal, err := InterpolateString(val, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package pluginproxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
@@ -24,7 +22,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
|
||||
SecureJsonData: ds.SecureJsonData.Decrypt(),
|
||||
}
|
||||
|
||||
interpolatedURL, err := interpolateString(route.Url, data)
|
||||
interpolatedURL, err := InterpolateString(route.Url, data)
|
||||
if err != nil {
|
||||
logger.Error("Error interpolating proxy url", "error", err)
|
||||
return
|
||||
@@ -81,24 +79,9 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
|
||||
logger.Info("Requesting", "url", req.URL.String())
|
||||
}
|
||||
|
||||
func interpolateString(text string, data templateData) (string, error) {
|
||||
t, err := template.New("content").Parse(text)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not parse template %s", text)
|
||||
}
|
||||
|
||||
var contentBuf bytes.Buffer
|
||||
err = t.Execute(&contentBuf, data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to execute template %s", text)
|
||||
}
|
||||
|
||||
return contentBuf.String(), nil
|
||||
}
|
||||
|
||||
func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data templateData) error {
|
||||
for _, header := range route.Headers {
|
||||
interpolated, err := interpolateString(header.Content, data)
|
||||
interpolated, err := InterpolateString(header.Content, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestDsAuthProvider(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
interpolated, err := interpolateString("{{.SecureJsonData.Test}}", data)
|
||||
interpolated, err := InterpolateString("{{.SecureJsonData.Test}}", data)
|
||||
So(err, ShouldBeNil)
|
||||
So(interpolated, ShouldEqual, "0asd+asd")
|
||||
})
|
||||
|
||||
@@ -354,7 +354,7 @@ func addOAuthPassThruAuth(c *m.ReqContext, req *http.Request) {
|
||||
// If the tokens are not the same, update the entry in the DB
|
||||
if token.AccessToken != authInfoQuery.Result.OAuthAccessToken {
|
||||
updateAuthCommand := &m.UpdateAuthInfoCommand{
|
||||
UserId: authInfoQuery.Result.Id,
|
||||
UserId: authInfoQuery.Result.UserId,
|
||||
AuthModule: authInfoQuery.Result.AuthModule,
|
||||
AuthId: authInfoQuery.Result.AuthId,
|
||||
OAuthToken: token,
|
||||
|
||||
@@ -2,12 +2,13 @@ package pluginproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@@ -38,6 +39,24 @@ func getHeaders(route *plugins.AppPluginRoute, orgId int64, appID string) (http.
|
||||
return result, err
|
||||
}
|
||||
|
||||
func updateURL(route *plugins.AppPluginRoute, orgId int64, appID string) (string, error) {
|
||||
query := m.GetPluginSettingByIdQuery{OrgId: orgId, PluginId: appID}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data := templateData{
|
||||
JsonData: query.Result.JsonData,
|
||||
SecureJsonData: query.Result.SecureJsonData.Decrypt(),
|
||||
}
|
||||
interpolated, err := InterpolateString(route.Url, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return interpolated, err
|
||||
}
|
||||
|
||||
// NewApiPluginProxy create a plugin proxy
|
||||
func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appID string, cfg *setting.Cfg) *httputil.ReverseProxy {
|
||||
targetURL, _ := url.Parse(route.Url)
|
||||
|
||||
@@ -48,7 +67,6 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
|
||||
req.Host = targetURL.Host
|
||||
|
||||
req.URL.Path = util.JoinURLFragments(targetURL.Path, proxyPath)
|
||||
|
||||
// clear cookie headers
|
||||
req.Header.Del("Cookie")
|
||||
req.Header.Del("Set-Cookie")
|
||||
@@ -72,13 +90,13 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
|
||||
}
|
||||
|
||||
// Create a HTTP header with the context in it.
|
||||
ctxJson, err := json.Marshal(ctx.SignedInUser)
|
||||
ctxJSON, err := json.Marshal(ctx.SignedInUser)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "failed to marshal context to json.", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Add("X-Grafana-Context", string(ctxJson))
|
||||
req.Header.Add("X-Grafana-Context", string(ctxJSON))
|
||||
|
||||
if cfg.SendUserHeader && !ctx.SignedInUser.IsAnonymous {
|
||||
req.Header.Add("X-Grafana-User", ctx.SignedInUser.Login)
|
||||
@@ -97,6 +115,27 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
|
||||
}
|
||||
}
|
||||
|
||||
if len(route.Url) > 0 {
|
||||
interpolatedURL, err := updateURL(route, ctx.OrgId, appID)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "Could not interpolate plugin route url", err)
|
||||
}
|
||||
targetURL, err := url.Parse(interpolatedURL)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "Could not parse custom url: %v", err)
|
||||
return
|
||||
}
|
||||
req.URL.Scheme = targetURL.Scheme
|
||||
req.URL.Host = targetURL.Host
|
||||
req.Host = targetURL.Host
|
||||
req.URL.Path = util.JoinURLFragments(targetURL.Path, proxyPath)
|
||||
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "Could not interpolate plugin route url", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// reqBytes, _ := httputil.DumpRequestOut(req, true);
|
||||
// log.Trace("Proxying plugin request: %s", string(reqBytes))
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ func TestPluginProxy(t *testing.T) {
|
||||
},
|
||||
},
|
||||
&setting.Cfg{SendUserHeader: true},
|
||||
nil,
|
||||
)
|
||||
|
||||
Convey("Should add header with username", func() {
|
||||
@@ -69,6 +70,7 @@ func TestPluginProxy(t *testing.T) {
|
||||
},
|
||||
},
|
||||
&setting.Cfg{SendUserHeader: false},
|
||||
nil,
|
||||
)
|
||||
Convey("Should not add header with username", func() {
|
||||
// Get will return empty string even if header is not set
|
||||
@@ -82,6 +84,7 @@ func TestPluginProxy(t *testing.T) {
|
||||
SignedInUser: &m.SignedInUser{IsAnonymous: true},
|
||||
},
|
||||
&setting.Cfg{SendUserHeader: true},
|
||||
nil,
|
||||
)
|
||||
|
||||
Convey("Should not add header with username", func() {
|
||||
@@ -89,14 +92,59 @@ func TestPluginProxy(t *testing.T) {
|
||||
So(req.Header.Get("X-Grafana-User"), ShouldEqual, "")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When getting templated url", t, func() {
|
||||
route := &plugins.AppPluginRoute{
|
||||
Url: "{{.JsonData.dynamicUrl}}",
|
||||
Method: "GET",
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetPluginSettingByIdQuery) error {
|
||||
query.Result = &m.PluginSetting{
|
||||
JsonData: map[string]interface{}{
|
||||
"dynamicUrl": "https://dynamic.grafana.com",
|
||||
},
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
req := getPluginProxiedRequest(
|
||||
&m.ReqContext{
|
||||
SignedInUser: &m.SignedInUser{
|
||||
Login: "test_user",
|
||||
},
|
||||
},
|
||||
&setting.Cfg{SendUserHeader: true},
|
||||
route,
|
||||
)
|
||||
Convey("Headers should be updated", func() {
|
||||
header, err := getHeaders(route, 1, "my-app")
|
||||
So(err, ShouldBeNil)
|
||||
So(header.Get("X-Grafana-User"), ShouldEqual, "")
|
||||
})
|
||||
Convey("Should set req.URL to be interpolated value from jsonData", func() {
|
||||
So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com")
|
||||
})
|
||||
Convey("Route url should not be modified", func() {
|
||||
So(route.Url, ShouldEqual, "{{.JsonData.dynamicUrl}}")
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// getPluginProxiedRequest is a helper for easier setup of tests based on global config and ReqContext.
|
||||
func getPluginProxiedRequest(ctx *m.ReqContext, cfg *setting.Cfg) *http.Request {
|
||||
route := &plugins.AppPluginRoute{}
|
||||
func getPluginProxiedRequest(ctx *m.ReqContext, cfg *setting.Cfg, route *plugins.AppPluginRoute) *http.Request {
|
||||
// insert dummy route if none is specified
|
||||
if route == nil {
|
||||
route = &plugins.AppPluginRoute{
|
||||
Path: "api/v4/",
|
||||
Url: "https://www.google.com",
|
||||
ReqRole: m.ROLE_EDITOR,
|
||||
}
|
||||
}
|
||||
proxy := NewApiPluginProxy(ctx, "", route, "", cfg)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
req, err := http.NewRequest(http.MethodGet, route.Url, nil)
|
||||
So(err, ShouldBeNil)
|
||||
proxy.Director(req)
|
||||
return req
|
||||
|
||||
49
pkg/api/pluginproxy/utils.go
Normal file
49
pkg/api/pluginproxy/utils.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package pluginproxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"text/template"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
// InterpolateString accepts template data and return a string with substitutions
|
||||
func InterpolateString(text string, data templateData) (string, error) {
|
||||
t, err := template.New("content").Parse(text)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not parse template %s", text)
|
||||
}
|
||||
|
||||
var contentBuf bytes.Buffer
|
||||
err = t.Execute(&contentBuf, data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to execute template %s", text)
|
||||
}
|
||||
|
||||
return contentBuf.String(), nil
|
||||
}
|
||||
|
||||
// InterpolateURL accepts template data and return a string with substitutions
|
||||
func InterpolateURL(anURL *url.URL, route *plugins.AppPluginRoute, orgID int64, appID string) (*url.URL, error) {
|
||||
query := m.GetPluginSettingByIdQuery{OrgId: orgID, PluginId: appID}
|
||||
result, err := url.Parse(anURL.String())
|
||||
if query.Result != nil {
|
||||
if len(query.Result.JsonData) > 0 {
|
||||
data := templateData{
|
||||
JsonData: query.Result.JsonData,
|
||||
}
|
||||
interpolatedResult, err := InterpolateString(anURL.String(), data)
|
||||
if err == nil {
|
||||
result, err = url.Parse(interpolatedResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing plugin route url %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
21
pkg/api/pluginproxy/utils_test.go
Normal file
21
pkg/api/pluginproxy/utils_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package pluginproxy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestInterpolateString(t *testing.T) {
|
||||
Convey("When interpolating string", t, func() {
|
||||
data := templateData{
|
||||
SecureJsonData: map[string]string{
|
||||
"Test": "0asd+asd",
|
||||
},
|
||||
}
|
||||
|
||||
interpolated, err := InterpolateString("{{.SecureJsonData.Test}}", data)
|
||||
So(err, ShouldBeNil)
|
||||
So(interpolated, ShouldEqual, "0asd+asd")
|
||||
})
|
||||
}
|
||||
@@ -7,28 +7,33 @@ import (
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/fatih/color"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/commands/datamigrations"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func runDbCommand(command func(commandLine CommandLine) error) func(context *cli.Context) {
|
||||
func runDbCommand(command func(commandLine utils.CommandLine, sqlStore *sqlstore.SqlStore) error) func(context *cli.Context) {
|
||||
return func(context *cli.Context) {
|
||||
cmd := &contextCommandLine{context}
|
||||
cmd := &utils.ContextCommandLine{Context: context}
|
||||
|
||||
cfg := setting.NewCfg()
|
||||
|
||||
cfg.Load(&setting.CommandLineArgs{
|
||||
Config: cmd.String("config"),
|
||||
HomePath: cmd.String("homepath"),
|
||||
Args: flag.Args(),
|
||||
})
|
||||
|
||||
cfg.LogConfigSources()
|
||||
|
||||
engine := &sqlstore.SqlStore{}
|
||||
engine.Cfg = cfg
|
||||
engine.Bus = bus.GetBus()
|
||||
engine.Init()
|
||||
|
||||
if err := command(cmd); err != nil {
|
||||
if err := command(cmd, engine); err != nil {
|
||||
logger.Errorf("\n%s: ", color.RedString("Error"))
|
||||
logger.Errorf("%s\n\n", err)
|
||||
|
||||
@@ -40,10 +45,10 @@ func runDbCommand(command func(commandLine CommandLine) error) func(context *cli
|
||||
}
|
||||
}
|
||||
|
||||
func runPluginCommand(command func(commandLine CommandLine) error) func(context *cli.Context) {
|
||||
func runPluginCommand(command func(commandLine utils.CommandLine) error) func(context *cli.Context) {
|
||||
return func(context *cli.Context) {
|
||||
|
||||
cmd := &contextCommandLine{context}
|
||||
cmd := &utils.ContextCommandLine{Context: context}
|
||||
if err := command(cmd); err != nil {
|
||||
logger.Errorf("\n%s: ", color.RedString("Error"))
|
||||
logger.Errorf("%s %s\n\n", color.RedString("✗"), err)
|
||||
@@ -91,12 +96,7 @@ var pluginCommands = []cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var adminCommands = []cli.Command{
|
||||
{
|
||||
Name: "reset-admin-password",
|
||||
Usage: "reset-admin-password <new password>",
|
||||
Action: runDbCommand(resetPasswordCommand),
|
||||
Flags: []cli.Flag{
|
||||
var dbCommandFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "homepath",
|
||||
Usage: "path to grafana install/home path, defaults to working directory",
|
||||
@@ -105,6 +105,25 @@ var adminCommands = []cli.Command{
|
||||
Name: "config",
|
||||
Usage: "path to config file",
|
||||
},
|
||||
}
|
||||
|
||||
var adminCommands = []cli.Command{
|
||||
{
|
||||
Name: "reset-admin-password",
|
||||
Usage: "reset-admin-password <new password>",
|
||||
Action: runDbCommand(resetPasswordCommand),
|
||||
Flags: dbCommandFlags,
|
||||
},
|
||||
{
|
||||
Name: "data-migration",
|
||||
Usage: "Runs a script that migrates or cleanups data in your db",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "encrypt-datasource-passwords",
|
||||
Usage: "Migrates passwords from unsecured fields to secure_json_data field. Return ok unless there is an error. Safe to execute multiple times.",
|
||||
Action: runDbCommand(datamigrations.EncryptDatasourcePaswords),
|
||||
Flags: dbCommandFlags,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package datamigrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
var (
|
||||
datasourceTypes = []string{
|
||||
"mysql",
|
||||
"influxdb",
|
||||
"elasticsearch",
|
||||
"graphite",
|
||||
"prometheus",
|
||||
"opentsdb",
|
||||
}
|
||||
)
|
||||
|
||||
// EncryptDatasourcePaswords migrates un-encrypted secrets on datasources
|
||||
// to the secureJson Column.
|
||||
func EncryptDatasourcePaswords(c utils.CommandLine, sqlStore *sqlstore.SqlStore) error {
|
||||
return sqlStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error {
|
||||
passwordsUpdated, err := migrateColumn(session, "password")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
basicAuthUpdated, err := migrateColumn(session, "basic_auth_password")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("\n")
|
||||
if passwordsUpdated > 0 {
|
||||
logger.Infof("%s Encrypted password field for %d datasources \n", color.GreenString("✔"), passwordsUpdated)
|
||||
}
|
||||
|
||||
if basicAuthUpdated > 0 {
|
||||
logger.Infof("%s Encrypted basic_auth_password field for %d datasources \n", color.GreenString("✔"), basicAuthUpdated)
|
||||
}
|
||||
|
||||
if passwordsUpdated == 0 && basicAuthUpdated == 0 {
|
||||
logger.Infof("%s All datasources secrets are allready encrypted\n", color.GreenString("✔"))
|
||||
}
|
||||
|
||||
logger.Info("\n")
|
||||
|
||||
logger.Warn("Warning: Datasource provisioning files need to be manually changed to prevent overwriting of " +
|
||||
"the data during provisioning. See https://grafana.com/docs/installation/upgrading/#upgrading-to-v6-2 for " +
|
||||
"details")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func migrateColumn(session *sqlstore.DBSession, column string) (int, error) {
|
||||
var rows []map[string]string
|
||||
|
||||
session.Cols("id", column, "secure_json_data")
|
||||
session.Table("data_source")
|
||||
session.In("type", datasourceTypes)
|
||||
session.Where(column + " IS NOT NULL AND " + column + " != ''")
|
||||
err := session.Find(&rows)
|
||||
|
||||
if err != nil {
|
||||
return 0, errutil.Wrapf(err, "failed to select column: %s", column)
|
||||
}
|
||||
|
||||
rowsUpdated, err := updateRows(session, rows, column)
|
||||
return rowsUpdated, errutil.Wrapf(err, "failed to update column: %s", column)
|
||||
}
|
||||
|
||||
func updateRows(session *sqlstore.DBSession, rows []map[string]string, passwordFieldName string) (int, error) {
|
||||
var rowsUpdated int
|
||||
|
||||
for _, row := range rows {
|
||||
newSecureJSONData, err := getUpdatedSecureJSONData(row, passwordFieldName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(newSecureJSONData)
|
||||
if err != nil {
|
||||
return 0, errutil.Wrap("marshaling newSecureJsonData failed", err)
|
||||
}
|
||||
|
||||
newRow := map[string]interface{}{"secure_json_data": data, passwordFieldName: ""}
|
||||
session.Table("data_source")
|
||||
session.Where("id = ?", row["id"])
|
||||
// Setting both columns while having value only for secure_json_data should clear the [passwordFieldName] column
|
||||
session.Cols("secure_json_data", passwordFieldName)
|
||||
|
||||
_, err = session.Update(newRow)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
rowsUpdated++
|
||||
}
|
||||
return rowsUpdated, nil
|
||||
}
|
||||
|
||||
func getUpdatedSecureJSONData(row map[string]string, passwordFieldName string) (map[string]interface{}, error) {
|
||||
encryptedPassword, err := util.Encrypt([]byte(row[passwordFieldName]), setting.SecretKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var secureJSONData map[string]interface{}
|
||||
|
||||
if err := json.Unmarshal([]byte(row["secure_json_data"]), &secureJSONData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jsonFieldName := util.ToCamelCase(passwordFieldName)
|
||||
secureJSONData[jsonFieldName] = encryptedPassword
|
||||
return secureJSONData, nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package datamigrations
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/commands/commandstest"
|
||||
"github.com/grafana/grafana/pkg/components/securejsondata"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPasswordMigrationCommand(t *testing.T) {
|
||||
//setup datasources with password, basic_auth and none
|
||||
sqlstore := sqlstore.InitTestDB(t)
|
||||
session := sqlstore.NewSession()
|
||||
defer session.Close()
|
||||
|
||||
datasources := []*models.DataSource{
|
||||
{Type: "influxdb", Name: "influxdb", Password: "foobar"},
|
||||
{Type: "graphite", Name: "graphite", BasicAuthPassword: "foobar"},
|
||||
{Type: "prometheus", Name: "prometheus", SecureJsonData: securejsondata.GetEncryptedJsonData(map[string]string{})},
|
||||
}
|
||||
|
||||
// set required default values
|
||||
for _, ds := range datasources {
|
||||
ds.Created = time.Now()
|
||||
ds.Updated = time.Now()
|
||||
ds.SecureJsonData = securejsondata.GetEncryptedJsonData(map[string]string{})
|
||||
}
|
||||
|
||||
_, err := session.Insert(&datasources)
|
||||
assert.Nil(t, err)
|
||||
|
||||
//run migration
|
||||
err = EncryptDatasourcePaswords(&commandstest.FakeCommandLine{}, sqlstore)
|
||||
assert.Nil(t, err)
|
||||
|
||||
//verify that no datasources still have password or basic_auth
|
||||
var dss []*models.DataSource
|
||||
err = session.SQL("select * from data_source").Find(&dss)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, len(dss), 3)
|
||||
|
||||
for _, ds := range dss {
|
||||
sj := ds.SecureJsonData.Decrypt()
|
||||
|
||||
if ds.Name == "influxdb" {
|
||||
assert.Equal(t, ds.Password, "")
|
||||
v, exist := sj["password"]
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, v, "foobar", "expected password to be moved to securejson")
|
||||
}
|
||||
|
||||
if ds.Name == "graphite" {
|
||||
assert.Equal(t, ds.BasicAuthPassword, "")
|
||||
v, exist := sj["basicAuthPassword"]
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, v, "foobar", "expected basic_auth_password to be moved to securejson")
|
||||
}
|
||||
|
||||
if ds.Name == "prometheus" {
|
||||
assert.Equal(t, len(sj), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,14 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
||||
s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
||||
)
|
||||
|
||||
func validateInput(c CommandLine, pluginFolder string) error {
|
||||
func validateInput(c utils.CommandLine, pluginFolder string) error {
|
||||
arg := c.Args().First()
|
||||
if arg == "" {
|
||||
return errors.New("please specify plugin to install")
|
||||
@@ -46,7 +47,7 @@ func validateInput(c CommandLine, pluginFolder string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func installCommand(c CommandLine) error {
|
||||
func installCommand(c utils.CommandLine) error {
|
||||
pluginFolder := c.PluginDirectory()
|
||||
if err := validateInput(c, pluginFolder); err != nil {
|
||||
return err
|
||||
@@ -60,7 +61,7 @@ func installCommand(c CommandLine) error {
|
||||
|
||||
// InstallPlugin downloads the plugin code as a zip file from the Grafana.com API
|
||||
// and then extracts the zip into the plugins directory.
|
||||
func InstallPlugin(pluginName, version string, c CommandLine) error {
|
||||
func InstallPlugin(pluginName, version string, c utils.CommandLine) error {
|
||||
pluginFolder := c.PluginDirectory()
|
||||
downloadURL := c.PluginURL()
|
||||
if downloadURL == "" {
|
||||
|
||||
@@ -3,9 +3,10 @@ package commands
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
)
|
||||
|
||||
func listremoteCommand(c CommandLine) error {
|
||||
func listremoteCommand(c utils.CommandLine) error {
|
||||
plugin, err := s.ListAllPlugins(c.RepoDirectory())
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -5,9 +5,10 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
)
|
||||
|
||||
func validateVersionInput(c CommandLine) error {
|
||||
func validateVersionInput(c utils.CommandLine) error {
|
||||
arg := c.Args().First()
|
||||
if arg == "" {
|
||||
return errors.New("please specify plugin to list versions for")
|
||||
@@ -16,7 +17,7 @@ func validateVersionInput(c CommandLine) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func listversionsCommand(c CommandLine) error {
|
||||
func listversionsCommand(c utils.CommandLine) error {
|
||||
if err := validateVersionInput(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
||||
s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
)
|
||||
|
||||
var ls_getPlugins func(path string) []m.InstalledPlugin = s.GetLocalPlugins
|
||||
@@ -31,7 +32,7 @@ var validateLsCommand = func(pluginDir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func lsCommand(c CommandLine) error {
|
||||
func lsCommand(c utils.CommandLine) error {
|
||||
pluginDir := c.PluginDirectory()
|
||||
if err := validateLsCommand(pluginDir); err != nil {
|
||||
return err
|
||||
|
||||
@@ -5,12 +5,13 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
)
|
||||
|
||||
var removePlugin func(pluginPath, id string) error = services.RemoveInstalledPlugin
|
||||
|
||||
func removeCommand(c CommandLine) error {
|
||||
func removeCommand(c utils.CommandLine) error {
|
||||
pluginPath := c.PluginDirectory()
|
||||
|
||||
plugin := c.Args().First()
|
||||
|
||||
@@ -6,13 +6,15 @@ import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
const AdminUserId = 1
|
||||
|
||||
func resetPasswordCommand(c CommandLine) error {
|
||||
func resetPasswordCommand(c utils.CommandLine, sqlStore *sqlstore.SqlStore) error {
|
||||
newPassword := c.Args().First()
|
||||
|
||||
password := models.Password(newPassword)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
||||
s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
@@ -27,7 +28,7 @@ func ShouldUpgrade(installed string, remote m.Plugin) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func upgradeAllCommand(c CommandLine) error {
|
||||
func upgradeAllCommand(c utils.CommandLine) error {
|
||||
pluginsDir := c.PluginDirectory()
|
||||
|
||||
localPlugins := s.GetLocalPlugins(pluginsDir)
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
)
|
||||
|
||||
func upgradeCommand(c CommandLine) error {
|
||||
func upgradeCommand(c utils.CommandLine) error {
|
||||
pluginsDir := c.PluginDirectory()
|
||||
pluginName := c.Args().First()
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
@@ -17,6 +18,7 @@ var version = "master"
|
||||
func main() {
|
||||
setupLogging()
|
||||
|
||||
flag.Parse()
|
||||
app := cli.NewApp()
|
||||
app.Name = "Grafana cli"
|
||||
app.Usage = ""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/codegangsta/cli"
|
||||
@@ -22,30 +22,30 @@ type CommandLine interface {
|
||||
PluginURL() string
|
||||
}
|
||||
|
||||
type contextCommandLine struct {
|
||||
type ContextCommandLine struct {
|
||||
*cli.Context
|
||||
}
|
||||
|
||||
func (c *contextCommandLine) ShowHelp() {
|
||||
func (c *ContextCommandLine) ShowHelp() {
|
||||
cli.ShowCommandHelp(c.Context, c.Command.Name)
|
||||
}
|
||||
|
||||
func (c *contextCommandLine) ShowVersion() {
|
||||
func (c *ContextCommandLine) ShowVersion() {
|
||||
cli.ShowVersion(c.Context)
|
||||
}
|
||||
|
||||
func (c *contextCommandLine) Application() *cli.App {
|
||||
func (c *ContextCommandLine) Application() *cli.App {
|
||||
return c.App
|
||||
}
|
||||
|
||||
func (c *contextCommandLine) PluginDirectory() string {
|
||||
func (c *ContextCommandLine) PluginDirectory() string {
|
||||
return c.GlobalString("pluginsDir")
|
||||
}
|
||||
|
||||
func (c *contextCommandLine) RepoDirectory() string {
|
||||
func (c *ContextCommandLine) RepoDirectory() string {
|
||||
return c.GlobalString("repo")
|
||||
}
|
||||
|
||||
func (c *contextCommandLine) PluginURL() string {
|
||||
func (c *ContextCommandLine) PluginURL() string {
|
||||
return c.GlobalString("pluginUrl")
|
||||
}
|
||||
@@ -39,10 +39,14 @@ func (dc *databaseCache) Run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (dc *databaseCache) internalRunGC() {
|
||||
err := dc.SQLStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error {
|
||||
now := getTime().Unix()
|
||||
sql := `DELETE FROM cache_data WHERE (? - created_at) >= expires AND expires <> 0`
|
||||
|
||||
_, err := dc.SQLStore.NewSession().Exec(sql, now)
|
||||
_, err := session.Exec(sql, now)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
dc.log.Error("failed to run garbage collect", "error", err)
|
||||
}
|
||||
@@ -66,7 +70,10 @@ func (dc *databaseCache) Get(key string) (interface{}, error) {
|
||||
if cacheHit.Expires > 0 {
|
||||
existedButExpired := getTime().Unix()-cacheHit.CreatedAt >= cacheHit.Expires
|
||||
if existedButExpired {
|
||||
_ = dc.Delete(key) //ignore this error since we will return `ErrCacheItemNotFound` anyway
|
||||
err = dc.Delete(key) //ignore this error since we will return `ErrCacheItemNotFound` anyway
|
||||
if err != nil {
|
||||
dc.log.Debug("Deletion of expired key failed: %v", err)
|
||||
}
|
||||
return nil, ErrCacheItemNotFound
|
||||
}
|
||||
}
|
||||
@@ -87,37 +94,46 @@ func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration
|
||||
}
|
||||
|
||||
session := dc.SQLStore.NewSession()
|
||||
|
||||
var cacheHit CacheData
|
||||
has, err := session.Where("cache_key = ?", key).Get(&cacheHit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
var expiresInSeconds int64
|
||||
if expire != 0 {
|
||||
expiresInSeconds = int64(expire) / int64(time.Second)
|
||||
}
|
||||
|
||||
// insert or update depending on if item already exist
|
||||
if has {
|
||||
sql := `UPDATE cache_data SET data=?, created_at=?, expires=? WHERE cache_key=?`
|
||||
_, err = session.Exec(sql, data, getTime().Unix(), expiresInSeconds, key)
|
||||
} else {
|
||||
// attempt to insert the key
|
||||
sql := `INSERT INTO cache_data (cache_key,data,created_at,expires) VALUES(?,?,?,?)`
|
||||
_, err = session.Exec(sql, key, data, getTime().Unix(), expiresInSeconds)
|
||||
if err != nil {
|
||||
// attempt to update if a unique constrain violation or a deadlock (for MySQL) occurs
|
||||
// if the update fails propagate the error
|
||||
// which eventually will result in a key that is not finally set
|
||||
// but since it's a cache does not harm a lot
|
||||
if dc.SQLStore.Dialect.IsUniqueConstraintViolation(err) || dc.SQLStore.Dialect.IsDeadlock(err) {
|
||||
sql := `UPDATE cache_data SET data=?, created_at=?, expires=? WHERE cache_key=?`
|
||||
_, err = session.Exec(sql, data, getTime().Unix(), expiresInSeconds, key)
|
||||
if err != nil && dc.SQLStore.Dialect.IsDeadlock(err) {
|
||||
// most probably somebody else is upserting the key
|
||||
// so it is safe enough not to propagate this error
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (dc *databaseCache) Delete(key string) error {
|
||||
return dc.SQLStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error {
|
||||
sql := "DELETE FROM cache_data WHERE cache_key=?"
|
||||
_, err := dc.SQLStore.NewSession().Exec(sql, key)
|
||||
_, err := session.Exec(sql, key)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// CacheData is the struct representing the table in the database
|
||||
type CacheData struct {
|
||||
CacheKey string
|
||||
Data []byte
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package remotecache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
redis "gopkg.in/redis.v2"
|
||||
)
|
||||
|
||||
@@ -13,12 +17,47 @@ type redisStorage struct {
|
||||
c *redis.Client
|
||||
}
|
||||
|
||||
func newRedisStorage(opts *setting.RemoteCacheOptions) *redisStorage {
|
||||
opt := &redis.Options{
|
||||
Network: "tcp",
|
||||
Addr: opts.ConnStr,
|
||||
// parseRedisConnStr parses k=v pairs in csv and builds a redis Options object
|
||||
func parseRedisConnStr(connStr string) (*redis.Options, error) {
|
||||
keyValueCSV := strings.Split(connStr, ",")
|
||||
options := &redis.Options{Network: "tcp"}
|
||||
for _, rawKeyValue := range keyValueCSV {
|
||||
keyValueTuple := strings.Split(rawKeyValue, "=")
|
||||
if len(keyValueTuple) != 2 {
|
||||
return nil, fmt.Errorf("incorrect redis connection string format detected for '%v', format is key=value,key=value", rawKeyValue)
|
||||
}
|
||||
return &redisStorage{c: redis.NewClient(opt)}
|
||||
connKey := keyValueTuple[0]
|
||||
connVal := keyValueTuple[1]
|
||||
switch connKey {
|
||||
case "addr":
|
||||
options.Addr = connVal
|
||||
case "password":
|
||||
options.Password = connVal
|
||||
case "db":
|
||||
i, err := strconv.ParseInt(connVal, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errutil.Wrap("value for db in redis connection string must be a number", err)
|
||||
}
|
||||
options.DB = i
|
||||
case "pool_size":
|
||||
i, err := strconv.Atoi(connVal)
|
||||
if err != nil {
|
||||
return nil, errutil.Wrap("value for pool_size in redis connection string must be a number", err)
|
||||
}
|
||||
options.PoolSize = i
|
||||
default:
|
||||
return nil, fmt.Errorf("unrecorgnized option '%v' in redis connection string", connVal)
|
||||
}
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func newRedisStorage(opts *setting.RemoteCacheOptions) (*redisStorage, error) {
|
||||
opt, err := parseRedisConnStr(opts.ConnStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &redisStorage{c: redis.NewClient(opt)}, nil
|
||||
}
|
||||
|
||||
// Set sets value to given key in session.
|
||||
@@ -28,7 +67,6 @@ func (s *redisStorage) Set(key string, val interface{}, expires time.Duration) e
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
status := s.c.SetEx(key, expires, string(value))
|
||||
return status.Err()
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
func TestRedisCacheStorage(t *testing.T) {
|
||||
|
||||
opts := &setting.RemoteCacheOptions{Name: redisCacheType, ConnStr: "localhost:6379"}
|
||||
opts := &setting.RemoteCacheOptions{Name: redisCacheType, ConnStr: "addr=localhost:6379"}
|
||||
client := createTestClient(t, opts, nil)
|
||||
runTestsForClient(t, client)
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func (ds *RemoteCache) Run(ctx context.Context) error {
|
||||
|
||||
func createClient(opts *setting.RemoteCacheOptions, sqlstore *sqlstore.SqlStore) (CacheStorage, error) {
|
||||
if opts.Name == redisCacheType {
|
||||
return newRedisStorage(opts), nil
|
||||
return newRedisStorage(opts)
|
||||
}
|
||||
|
||||
if opts.Name == memcachedCacheType {
|
||||
|
||||
@@ -31,6 +31,7 @@ func initContextWithAuthProxy(store *remotecache.RemoteCache, ctx *m.ReqContext,
|
||||
|
||||
// Check if allowed to continue with this IP
|
||||
if result, err := auth.IsAllowedIP(); result == false {
|
||||
ctx.Logger.Error("auth proxy: failed to check whitelisted ip addresses", "message", err.Error(), "error", err.DetailsError)
|
||||
ctx.Handle(407, err.Error(), err.DetailsError)
|
||||
return true
|
||||
}
|
||||
@@ -38,6 +39,7 @@ func initContextWithAuthProxy(store *remotecache.RemoteCache, ctx *m.ReqContext,
|
||||
// Try to get user id from various sources
|
||||
id, err := auth.GetUserID()
|
||||
if err != nil {
|
||||
ctx.Logger.Error("auth proxy: failed to login", "message", err.Error(), "error", err.DetailsError)
|
||||
ctx.Handle(500, err.Error(), err.DetailsError)
|
||||
return true
|
||||
}
|
||||
@@ -45,6 +47,7 @@ func initContextWithAuthProxy(store *remotecache.RemoteCache, ctx *m.ReqContext,
|
||||
// Get full user info
|
||||
user, err := auth.GetSignedUser(id)
|
||||
if err != nil {
|
||||
ctx.Logger.Error("auth proxy: failed to get signed in user", "message", err.Error(), "error", err.DetailsError)
|
||||
ctx.Handle(500, err.Error(), err.DetailsError)
|
||||
return true
|
||||
}
|
||||
@@ -54,7 +57,8 @@ func initContextWithAuthProxy(store *remotecache.RemoteCache, ctx *m.ReqContext,
|
||||
ctx.IsSignedIn = true
|
||||
|
||||
// Remember user data it in cache
|
||||
if err := auth.Remember(); err != nil {
|
||||
if err := auth.Remember(id); err != nil {
|
||||
ctx.Logger.Error("auth proxy: failed to store user in cache", "message", err.Error(), "error", err.DetailsError)
|
||||
ctx.Handle(500, err.Error(), err.DetailsError)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -298,23 +298,24 @@ func (auth *AuthProxy) GetSignedUser(userID int64) (*models.SignedInUser, *Error
|
||||
}
|
||||
|
||||
// Remember user in cache
|
||||
func (auth *AuthProxy) Remember() *Error {
|
||||
func (auth *AuthProxy) Remember(id int64) *Error {
|
||||
key := auth.getKey()
|
||||
|
||||
// Make sure we do not rewrite the expiration time
|
||||
if auth.InCache() {
|
||||
// Check if user already in cache
|
||||
userID, err := auth.store.Get(key)
|
||||
if err != nil && err != remotecache.ErrCacheItemNotFound {
|
||||
return newError("failed to lookup user in cache", err)
|
||||
}
|
||||
|
||||
if userID != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
key = auth.getKey()
|
||||
value, _ = auth.GetUserIDViaCache()
|
||||
expiration = time.Duration(-auth.cacheTTL) * time.Minute
|
||||
|
||||
err = auth.store.Set(key, value, expiration)
|
||||
)
|
||||
expiration := time.Duration(auth.cacheTTL) * time.Minute
|
||||
|
||||
err = auth.store.Set(key, id, expiration)
|
||||
if err != nil {
|
||||
return newError(err.Error(), nil)
|
||||
return newError("failed to store user in cache", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
@@ -241,10 +242,35 @@ func AddDefaultResponseHeaders() macaron.Handler {
|
||||
if !setting.AllowEmbedding {
|
||||
AddXFrameOptionsDenyHeader(w)
|
||||
}
|
||||
|
||||
AddSecurityHeaders(w)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// AddSecurityHeaders adds various HTTP(S) response headers that enable various security protections behaviors in the client's browser.
|
||||
func AddSecurityHeaders(w macaron.ResponseWriter) {
|
||||
if setting.Protocol == setting.HTTPS && setting.StrictTransportSecurity {
|
||||
strictHeader := "Strict-Transport-Security"
|
||||
w.Header().Add(strictHeader, fmt.Sprintf("max-age=%v", setting.StrictTransportSecurityMaxAge))
|
||||
if setting.StrictTransportSecurityPreload {
|
||||
w.Header().Add(strictHeader, "preload")
|
||||
}
|
||||
if setting.StrictTransportSecuritySubDomains {
|
||||
w.Header().Add(strictHeader, "includeSubDomains")
|
||||
}
|
||||
}
|
||||
|
||||
if setting.ContentTypeProtectionHeader {
|
||||
w.Header().Add("X-Content-Type-Options", "nosniff")
|
||||
}
|
||||
|
||||
if setting.XSSProtectionHeader {
|
||||
w.Header().Add("X-XSS-Protection", "1")
|
||||
w.Header().Add("X-XSS-Protection", "mode=block")
|
||||
}
|
||||
}
|
||||
|
||||
func AddNoCacheHeaders(w macaron.ResponseWriter) {
|
||||
w.Header().Add("Cache-Control", "no-cache")
|
||||
w.Header().Add("Pragma", "no-cache")
|
||||
|
||||
@@ -23,6 +23,7 @@ type DataSourcePlugin struct {
|
||||
Alerting bool `json:"alerting"`
|
||||
Explore bool `json:"explore"`
|
||||
Table bool `json:"tables"`
|
||||
HiddenQueries bool `json:"hiddenQueries"`
|
||||
Logs bool `json:"logs"`
|
||||
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
|
||||
BuiltIn bool `json:"builtIn,omitempty"`
|
||||
|
||||
@@ -48,6 +48,7 @@ type Dialect interface {
|
||||
NoOpSql() string
|
||||
|
||||
IsUniqueConstraintViolation(err error) bool
|
||||
IsDeadlock(err error) bool
|
||||
}
|
||||
|
||||
func NewDialect(engine *xorm.Engine) Dialect {
|
||||
|
||||
@@ -134,12 +134,20 @@ func (db *Mysql) CleanDB() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Mysql) IsUniqueConstraintViolation(err error) bool {
|
||||
func (db *Mysql) isThisError(err error, errcode uint16) bool {
|
||||
if driverErr, ok := err.(*mysql.MySQLError); ok {
|
||||
if driverErr.Number == mysqlerr.ER_DUP_ENTRY {
|
||||
if driverErr.Number == errcode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (db *Mysql) IsUniqueConstraintViolation(err error) bool {
|
||||
return db.isThisError(err, mysqlerr.ER_DUP_ENTRY)
|
||||
}
|
||||
|
||||
func (db *Mysql) IsDeadlock(err error) bool {
|
||||
return db.isThisError(err, mysqlerr.ER_LOCK_DEADLOCK)
|
||||
}
|
||||
|
||||
@@ -138,12 +138,20 @@ func (db *Postgres) CleanDB() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Postgres) IsUniqueConstraintViolation(err error) bool {
|
||||
func (db *Postgres) isThisError(err error, errcode string) bool {
|
||||
if driverErr, ok := err.(*pq.Error); ok {
|
||||
if driverErr.Code == "23505" {
|
||||
if string(driverErr.Code) == errcode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (db *Postgres) IsUniqueConstraintViolation(err error) bool {
|
||||
return db.isThisError(err, "23505")
|
||||
}
|
||||
|
||||
func (db *Postgres) IsDeadlock(err error) bool {
|
||||
return db.isThisError(err, "40P01")
|
||||
}
|
||||
|
||||
@@ -85,12 +85,20 @@ func (db *Sqlite3) CleanDB() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Sqlite3) IsUniqueConstraintViolation(err error) bool {
|
||||
func (db *Sqlite3) isThisError(err error, errcode int) bool {
|
||||
if driverErr, ok := err.(sqlite3.Error); ok {
|
||||
if driverErr.ExtendedCode == sqlite3.ErrConstraintUnique {
|
||||
if int(driverErr.ExtendedCode) == errcode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (db *Sqlite3) IsUniqueConstraintViolation(err error) bool {
|
||||
return db.isThisError(err, int(sqlite3.ErrConstraintUnique))
|
||||
}
|
||||
|
||||
func (db *Sqlite3) IsDeadlock(err error) bool {
|
||||
return false // No deadlock
|
||||
}
|
||||
|
||||
@@ -45,19 +45,13 @@ func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, permi
|
||||
sb.sql.WriteString(` AND
|
||||
(
|
||||
dashboard.id IN (
|
||||
SELECT distinct d.id AS DashboardId
|
||||
SELECT distinct DashboardId from (
|
||||
SELECT d.id AS DashboardId
|
||||
FROM dashboard AS d
|
||||
LEFT JOIN dashboard folder on folder.id = d.folder_id
|
||||
LEFT JOIN dashboard AS folder on folder.id = d.folder_id
|
||||
LEFT JOIN dashboard_acl AS da ON
|
||||
da.dashboard_id = d.id OR
|
||||
da.dashboard_id = d.folder_id OR
|
||||
(
|
||||
-- include default permissions -->
|
||||
da.org_id = -1 AND (
|
||||
(folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR
|
||||
(folder.id IS NULL AND d.has_acl = ` + falseStr + `)
|
||||
)
|
||||
)
|
||||
da.dashboard_id = d.folder_id
|
||||
LEFT JOIN team_member as ugm on ugm.team_id = da.team_id
|
||||
WHERE
|
||||
d.org_id = ? AND
|
||||
@@ -67,9 +61,32 @@ func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, permi
|
||||
ugm.user_id = ? OR
|
||||
da.role IN (?` + strings.Repeat(",?", len(okRoles)-1) + `)
|
||||
)
|
||||
UNION
|
||||
SELECT d.id AS DashboardId
|
||||
FROM dashboard AS d
|
||||
LEFT JOIN dashboard AS folder on folder.id = d.folder_id
|
||||
LEFT JOIN dashboard_acl AS da ON
|
||||
(
|
||||
-- include default permissions -->
|
||||
da.org_id = -1 AND (
|
||||
(folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR
|
||||
(folder.id IS NULL AND d.has_acl = ` + falseStr + `)
|
||||
)
|
||||
)
|
||||
WHERE
|
||||
d.org_id = ? AND
|
||||
da.permission >= ? AND
|
||||
(
|
||||
da.user_id = ? OR
|
||||
da.role IN (?` + strings.Repeat(",?", len(okRoles)-1) + `)
|
||||
)
|
||||
) AS a
|
||||
)
|
||||
)`)
|
||||
|
||||
sb.params = append(sb.params, user.OrgId, permission, user.UserId, user.UserId)
|
||||
sb.params = append(sb.params, okRoles...)
|
||||
|
||||
sb.params = append(sb.params, user.OrgId, permission, user.UserId)
|
||||
sb.params = append(sb.params, okRoles...)
|
||||
}
|
||||
|
||||
343
pkg/services/sqlstore/sqlbuilder_test.go
Normal file
343
pkg/services/sqlstore/sqlbuilder_test.go
Normal file
@@ -0,0 +1,343 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSqlBuilder(t *testing.T) {
|
||||
t.Run("writeDashboardPermissionFilter", func(t *testing.T) {
|
||||
t.Run("user ACL", func(t *testing.T) {
|
||||
test(t,
|
||||
DashboardProps{},
|
||||
&DashboardPermission{User: true, Permission: models.PERMISSION_VIEW},
|
||||
Search{UserFromACL: true, RequiredPermission: models.PERMISSION_VIEW},
|
||||
shouldFind,
|
||||
)
|
||||
|
||||
test(t,
|
||||
DashboardProps{},
|
||||
&DashboardPermission{User: true, Permission: models.PERMISSION_VIEW},
|
||||
Search{UserFromACL: true, RequiredPermission: models.PERMISSION_EDIT},
|
||||
shouldNotFind,
|
||||
)
|
||||
|
||||
test(t,
|
||||
DashboardProps{},
|
||||
&DashboardPermission{User: true, Permission: models.PERMISSION_EDIT},
|
||||
Search{UserFromACL: true, RequiredPermission: models.PERMISSION_EDIT},
|
||||
shouldFind,
|
||||
)
|
||||
|
||||
test(t,
|
||||
DashboardProps{},
|
||||
&DashboardPermission{User: true, Permission: models.PERMISSION_VIEW},
|
||||
Search{RequiredPermission: models.PERMISSION_VIEW},
|
||||
shouldNotFind,
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("role ACL", func(t *testing.T) {
|
||||
test(t,
|
||||
DashboardProps{},
|
||||
&DashboardPermission{Role: models.ROLE_VIEWER, Permission: models.PERMISSION_VIEW},
|
||||
Search{UsersOrgRole: models.ROLE_VIEWER, RequiredPermission: models.PERMISSION_VIEW},
|
||||
shouldFind,
|
||||
)
|
||||
|
||||
test(t,
|
||||
DashboardProps{},
|
||||
&DashboardPermission{Role: models.ROLE_VIEWER, Permission: models.PERMISSION_VIEW},
|
||||
Search{UsersOrgRole: models.ROLE_VIEWER, RequiredPermission: models.PERMISSION_EDIT},
|
||||
shouldNotFind,
|
||||
)
|
||||
|
||||
test(t,
|
||||
DashboardProps{},
|
||||
&DashboardPermission{Role: models.ROLE_EDITOR, Permission: models.PERMISSION_VIEW},
|
||||
Search{UsersOrgRole: models.ROLE_VIEWER, RequiredPermission: models.PERMISSION_VIEW},
|
||||
shouldNotFind,
|
||||
)
|
||||
|
||||
test(t,
|
||||
DashboardProps{},
|
||||
&DashboardPermission{Role: models.ROLE_EDITOR, Permission: models.PERMISSION_VIEW},
|
||||
Search{UsersOrgRole: models.ROLE_VIEWER, RequiredPermission: models.PERMISSION_VIEW},
|
||||
shouldNotFind,
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("team ACL", func(t *testing.T) {
|
||||
test(t,
|
||||
DashboardProps{},
|
||||
&DashboardPermission{Team: true, Permission: models.PERMISSION_VIEW},
|
||||
Search{UserFromACL: true, RequiredPermission: models.PERMISSION_VIEW},
|
||||
shouldFind,
|
||||
)
|
||||
|
||||
test(t,
|
||||
DashboardProps{},
|
||||
&DashboardPermission{Team: true, Permission: models.PERMISSION_VIEW},
|
||||
Search{UserFromACL: true, RequiredPermission: models.PERMISSION_EDIT},
|
||||
shouldNotFind,
|
||||
)
|
||||
|
||||
test(t,
|
||||
DashboardProps{},
|
||||
&DashboardPermission{Team: true, Permission: models.PERMISSION_EDIT},
|
||||
Search{UserFromACL: true, RequiredPermission: models.PERMISSION_EDIT},
|
||||
shouldFind,
|
||||
)
|
||||
|
||||
test(t,
|
||||
DashboardProps{},
|
||||
&DashboardPermission{Team: true, Permission: models.PERMISSION_EDIT},
|
||||
Search{UserFromACL: false, RequiredPermission: models.PERMISSION_EDIT},
|
||||
shouldNotFind,
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("defaults for user ACL", func(t *testing.T) {
|
||||
test(t,
|
||||
DashboardProps{},
|
||||
nil,
|
||||
Search{OrgId: -1, UsersOrgRole: models.ROLE_VIEWER, RequiredPermission: models.PERMISSION_VIEW},
|
||||
shouldNotFind,
|
||||
)
|
||||
|
||||
test(t,
|
||||
DashboardProps{OrgId: -1},
|
||||
nil,
|
||||
Search{OrgId: -1, UsersOrgRole: models.ROLE_VIEWER, RequiredPermission: models.PERMISSION_VIEW},
|
||||
shouldFind,
|
||||
)
|
||||
|
||||
test(t,
|
||||
DashboardProps{OrgId: -1},
|
||||
nil,
|
||||
Search{OrgId: -1, UsersOrgRole: models.ROLE_EDITOR, RequiredPermission: models.PERMISSION_EDIT},
|
||||
shouldFind,
|
||||
)
|
||||
|
||||
test(t,
|
||||
DashboardProps{OrgId: -1},
|
||||
nil,
|
||||
Search{OrgId: -1, UsersOrgRole: models.ROLE_VIEWER, RequiredPermission: models.PERMISSION_EDIT},
|
||||
shouldNotFind,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
var shouldFind = true
|
||||
var shouldNotFind = false
|
||||
|
||||
type DashboardProps struct {
|
||||
OrgId int64
|
||||
}
|
||||
|
||||
type DashboardPermission struct {
|
||||
User bool
|
||||
Team bool
|
||||
Role models.RoleType
|
||||
Permission models.PermissionType
|
||||
}
|
||||
|
||||
type Search struct {
|
||||
UsersOrgRole models.RoleType
|
||||
UserFromACL bool
|
||||
RequiredPermission models.PermissionType
|
||||
OrgId int64
|
||||
}
|
||||
|
||||
type dashboardResponse struct {
|
||||
Id int64
|
||||
}
|
||||
|
||||
func test(t *testing.T, dashboardProps DashboardProps, dashboardPermission *DashboardPermission, search Search, shouldFind bool) {
|
||||
// Will also cleanup the db
|
||||
sqlStore := InitTestDB(t)
|
||||
|
||||
dashboard, err := createDummyDashboard(dashboardProps)
|
||||
if !assert.Equal(t, nil, err) {
|
||||
return
|
||||
}
|
||||
|
||||
var aclUserId int64
|
||||
if dashboardPermission != nil {
|
||||
aclUserId, err = createDummyAcl(dashboardPermission, search, dashboard.Id)
|
||||
if !assert.Equal(t, nil, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
dashboards, err := getDashboards(sqlStore, search, aclUserId)
|
||||
if !assert.Equal(t, nil, err) {
|
||||
return
|
||||
}
|
||||
|
||||
if shouldFind {
|
||||
if assert.Equal(t, 1, len(dashboards), "Should return one dashboard") {
|
||||
assert.Equal(t, dashboards[0].Id, dashboard.Id, "Should return created dashboard")
|
||||
}
|
||||
} else {
|
||||
assert.Equal(t, 0, len(dashboards), "Should node return any dashboard")
|
||||
}
|
||||
}
|
||||
|
||||
func createDummyUser() (*models.User, error) {
|
||||
uid := rand.Intn(9999999)
|
||||
createUserCmd := &models.CreateUserCommand{
|
||||
Email: string(uid) + "@example.com",
|
||||
Login: string(uid),
|
||||
Name: string(uid),
|
||||
Company: "",
|
||||
OrgName: "",
|
||||
Password: string(uid),
|
||||
EmailVerified: true,
|
||||
IsAdmin: false,
|
||||
SkipOrgSetup: false,
|
||||
DefaultOrgRole: string(models.ROLE_VIEWER),
|
||||
}
|
||||
err := CreateUser(context.Background(), createUserCmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &createUserCmd.Result, nil
|
||||
}
|
||||
|
||||
func createDummyTeam() (*models.Team, error) {
|
||||
cmd := &models.CreateTeamCommand{
|
||||
// Does not matter in this tests actually
|
||||
OrgId: 1,
|
||||
Name: "test",
|
||||
Email: "test@example.com",
|
||||
}
|
||||
err := CreateTeam(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cmd.Result, nil
|
||||
}
|
||||
|
||||
func createDummyDashboard(dashboardProps DashboardProps) (*models.Dashboard, error) {
|
||||
json, _ := simplejson.NewJson([]byte(`{"schemaVersion":17,"title":"gdev dashboards","uid":"","version":1}`))
|
||||
|
||||
saveDashboardCmd := &models.SaveDashboardCommand{
|
||||
Dashboard: json,
|
||||
UserId: 0,
|
||||
Overwrite: false,
|
||||
Message: "",
|
||||
RestoredFrom: 0,
|
||||
PluginId: "",
|
||||
FolderId: 0,
|
||||
IsFolder: false,
|
||||
UpdatedAt: time.Time{},
|
||||
}
|
||||
if dashboardProps.OrgId != 0 {
|
||||
saveDashboardCmd.OrgId = dashboardProps.OrgId
|
||||
} else {
|
||||
saveDashboardCmd.OrgId = 1
|
||||
}
|
||||
|
||||
err := SaveDashboard(saveDashboardCmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return saveDashboardCmd.Result, nil
|
||||
}
|
||||
|
||||
func createDummyAcl(dashboardPermission *DashboardPermission, search Search, dashboardId int64) (int64, error) {
|
||||
acl := &models.DashboardAcl{
|
||||
OrgId: 1,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
Permission: dashboardPermission.Permission,
|
||||
DashboardId: dashboardId,
|
||||
}
|
||||
|
||||
var user *models.User
|
||||
var err error
|
||||
if dashboardPermission.User {
|
||||
user, err = createDummyUser()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
acl.UserId = user.Id
|
||||
}
|
||||
|
||||
if dashboardPermission.Team {
|
||||
team, err := createDummyTeam()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if search.UserFromACL {
|
||||
user, err = createDummyUser()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
addTeamMemberCmd := &models.AddTeamMemberCommand{
|
||||
UserId: user.Id,
|
||||
OrgId: 1,
|
||||
TeamId: team.Id,
|
||||
}
|
||||
err = AddTeamMember(addTeamMemberCmd)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
acl.TeamId = team.Id
|
||||
}
|
||||
|
||||
if len(string(dashboardPermission.Role)) > 0 {
|
||||
acl.Role = &dashboardPermission.Role
|
||||
}
|
||||
|
||||
updateAclCmd := &models.UpdateDashboardAclCommand{
|
||||
DashboardId: dashboardId,
|
||||
Items: []*models.DashboardAcl{acl},
|
||||
}
|
||||
err = UpdateDashboardAcl(updateAclCmd)
|
||||
if user != nil {
|
||||
return user.Id, err
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
func getDashboards(sqlStore *SqlStore, search Search, aclUserId int64) ([]*dashboardResponse, error) {
|
||||
builder := &SqlBuilder{}
|
||||
signedInUser := &models.SignedInUser{
|
||||
UserId: 9999999999,
|
||||
}
|
||||
|
||||
if search.OrgId == 0 {
|
||||
signedInUser.OrgId = 1
|
||||
} else {
|
||||
signedInUser.OrgId = search.OrgId
|
||||
}
|
||||
|
||||
if len(string(search.UsersOrgRole)) > 0 {
|
||||
signedInUser.OrgRole = search.UsersOrgRole
|
||||
} else {
|
||||
signedInUser.OrgRole = models.ROLE_VIEWER
|
||||
}
|
||||
if search.UserFromACL {
|
||||
signedInUser.UserId = aclUserId
|
||||
}
|
||||
|
||||
var res []*dashboardResponse
|
||||
builder.Write("SELECT * FROM dashboard WHERE true")
|
||||
builder.writeDashboardPermissionFilter(signedInUser, search.RequiredPermission)
|
||||
err := sqlStore.engine.SQL(builder.GetSqlString(), builder.params...).Find(&res)
|
||||
return res, err
|
||||
}
|
||||
@@ -39,6 +39,11 @@ var (
|
||||
const ContextSessionName = "db-session"
|
||||
|
||||
func init() {
|
||||
// This change will make xorm use an empty default schema for postgres and
|
||||
// by that mimic the functionality of how it was functioning before
|
||||
// xorm's changes above.
|
||||
xorm.DefaultPostgresSchema = ""
|
||||
|
||||
registry.Register(®istry.Descriptor{
|
||||
Name: "SqlStore",
|
||||
Instance: &SqlStore{},
|
||||
@@ -88,12 +93,12 @@ func (ss *SqlStore) inTransactionWithRetryCtx(ctx context.Context, callback dbTr
|
||||
|
||||
err = callback(sess)
|
||||
|
||||
// special handling of database locked errors for sqlite, then we can retry 3 times
|
||||
// special handling of database locked errors for sqlite, then we can retry 5 times
|
||||
if sqlError, ok := err.(sqlite3.Error); ok && retry < 5 {
|
||||
if sqlError.Code == sqlite3.ErrLocked {
|
||||
if sqlError.Code == sqlite3.ErrLocked || sqlError.Code == sqlite3.ErrBusy {
|
||||
sess.Rollback()
|
||||
time.Sleep(time.Millisecond * time.Duration(10))
|
||||
sqlog.Info("Database table locked, sleeping then retrying", "retry", retry)
|
||||
sqlog.Info("Database locked, sleeping then retrying", "error", err, "retry", retry)
|
||||
return ss.inTransactionWithRetryCtx(ctx, callback, retry+1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,12 @@ func (ss *SqlStore) inTransactionWithRetry(ctx context.Context, fn func(ctx cont
|
||||
|
||||
err = fn(withValue)
|
||||
|
||||
// special handling of database locked errors for sqlite, then we can retry 3 times
|
||||
// special handling of database locked errors for sqlite, then we can retry 5 times
|
||||
if sqlError, ok := err.(sqlite3.Error); ok && retry < 5 {
|
||||
if sqlError.Code == sqlite3.ErrLocked {
|
||||
if sqlError.Code == sqlite3.ErrLocked || sqlError.Code == sqlite3.ErrBusy {
|
||||
sess.Rollback()
|
||||
time.Sleep(time.Millisecond * time.Duration(10))
|
||||
ss.log.Info("Database table locked, sleeping then retrying", "retry", retry)
|
||||
ss.log.Info("Database locked, sleeping then retrying", "error", err, "retry", retry)
|
||||
return ss.inTransactionWithRetry(ctx, fn, retry+1)
|
||||
}
|
||||
}
|
||||
@@ -69,12 +69,12 @@ func inTransactionWithRetryCtx(ctx context.Context, callback dbTransactionFunc,
|
||||
|
||||
err = callback(sess)
|
||||
|
||||
// special handling of database locked errors for sqlite, then we can retry 3 times
|
||||
// special handling of database locked errors for sqlite, then we can retry 5 times
|
||||
if sqlError, ok := err.(sqlite3.Error); ok && retry < 5 {
|
||||
if sqlError.Code == sqlite3.ErrLocked {
|
||||
if sqlError.Code == sqlite3.ErrLocked || sqlError.Code == sqlite3.ErrBusy {
|
||||
sess.Rollback()
|
||||
time.Sleep(time.Millisecond * time.Duration(10))
|
||||
sqlog.Info("Database table locked, sleeping then retrying", "retry", retry)
|
||||
sqlog.Info("Database locked, sleeping then retrying", "error", err, "retry", retry)
|
||||
return inTransactionWithRetry(callback, retry+1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,8 +216,8 @@ func UpdateAuthInfo(cmd *m.UpdateAuthInfoCommand) error {
|
||||
UserId: cmd.UserId,
|
||||
AuthModule: cmd.AuthModule,
|
||||
}
|
||||
|
||||
_, err := sess.Update(authUser, cond)
|
||||
upd, err := sess.Update(authUser, cond)
|
||||
sqlog.Debug("Updated user_auth", "user_id", cmd.UserId, "auth_module", cmd.AuthModule, "rows", upd)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
@@ -94,6 +94,12 @@ var (
|
||||
CookieSecure bool
|
||||
CookieSameSite http.SameSite
|
||||
AllowEmbedding bool
|
||||
XSSProtectionHeader bool
|
||||
ContentTypeProtectionHeader bool
|
||||
StrictTransportSecurity bool
|
||||
StrictTransportSecurityMaxAge int
|
||||
StrictTransportSecurityPreload bool
|
||||
StrictTransportSecuritySubDomains bool
|
||||
|
||||
// Snapshots
|
||||
ExternalSnapshotUrl string
|
||||
@@ -693,6 +699,13 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
|
||||
AllowEmbedding = security.Key("allow_embedding").MustBool(false)
|
||||
|
||||
ContentTypeProtectionHeader = security.Key("x_content_type_options").MustBool(false)
|
||||
XSSProtectionHeader = security.Key("x_xss_protection").MustBool(false)
|
||||
StrictTransportSecurity = security.Key("strict_transport_security").MustBool(false)
|
||||
StrictTransportSecurityMaxAge = security.Key("strict_transport_security_max_age_seconds").MustInt(86400)
|
||||
StrictTransportSecurityPreload = security.Key("strict_transport_security_preload").MustBool(false)
|
||||
StrictTransportSecuritySubDomains = security.Key("strict_transport_security_subdomains").MustBool(false)
|
||||
|
||||
// read snapshots settings
|
||||
snapshots := iniFile.Section("snapshots")
|
||||
ExternalSnapshotUrl, err = valueAsString(snapshots, "external_snapshot_url", "")
|
||||
|
||||
@@ -85,11 +85,14 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
|
||||
azlog.Debug("AzureMonitor", "target", azureMonitorTarget)
|
||||
|
||||
urlComponents := map[string]string{}
|
||||
urlComponents["subscription"] = fmt.Sprintf("%v", query.Model.Get("subscription").MustString())
|
||||
urlComponents["resourceGroup"] = fmt.Sprintf("%v", azureMonitorTarget["resourceGroup"])
|
||||
urlComponents["metricDefinition"] = fmt.Sprintf("%v", azureMonitorTarget["metricDefinition"])
|
||||
urlComponents["resourceName"] = fmt.Sprintf("%v", azureMonitorTarget["resourceName"])
|
||||
|
||||
ub := urlBuilder{
|
||||
DefaultSubscription: query.DataSource.JsonData.Get("subscriptionId").MustString(),
|
||||
Subscription: urlComponents["subscription"],
|
||||
ResourceGroup: urlComponents["resourceGroup"],
|
||||
MetricDefinition: urlComponents["metricDefinition"],
|
||||
ResourceName: urlComponents["resourceName"],
|
||||
@@ -199,8 +202,7 @@ func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo *mode
|
||||
}
|
||||
|
||||
cloudName := dsInfo.JsonData.Get("cloudName").MustString("azuremonitor")
|
||||
subscriptionID := dsInfo.JsonData.Get("subscriptionId").MustString()
|
||||
proxyPass := fmt.Sprintf("%s/subscriptions/%s", cloudName, subscriptionID)
|
||||
proxyPass := fmt.Sprintf("%s/subscriptions", cloudName)
|
||||
|
||||
u, _ := url.Parse(dsInfo.Url)
|
||||
u.Path = path.Join(u.Path, "render")
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
@@ -27,7 +28,13 @@ func TestAzureMonitorDatasource(t *testing.T) {
|
||||
},
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
DataSource: &models.DataSource{
|
||||
JsonData: simplejson.NewFromAny(map[string]interface{}{
|
||||
"subscriptionId": "default-subscription",
|
||||
}),
|
||||
},
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"subscription": "12345678-aaaa-bbbb-cccc-123456789abc",
|
||||
"azureMonitor": map[string]interface{}{
|
||||
"timeGrain": "PT1M",
|
||||
"aggregation": "Average",
|
||||
@@ -49,7 +56,7 @@ func TestAzureMonitorDatasource(t *testing.T) {
|
||||
|
||||
So(len(queries), ShouldEqual, 1)
|
||||
So(queries[0].RefID, ShouldEqual, "A")
|
||||
So(queries[0].URL, ShouldEqual, "resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics")
|
||||
So(queries[0].URL, ShouldEqual, "12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics")
|
||||
So(queries[0].Target, ShouldEqual, "aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z")
|
||||
So(len(queries[0].Params), ShouldEqual, 5)
|
||||
So(queries[0].Params["timespan"][0], ShouldEqual, "2018-03-15T13:00:00Z/2018-03-15T13:34:00Z")
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
|
||||
// urlBuilder builds the URL for calling the Azure Monitor API
|
||||
type urlBuilder struct {
|
||||
DefaultSubscription string
|
||||
Subscription string
|
||||
ResourceGroup string
|
||||
MetricDefinition string
|
||||
ResourceName string
|
||||
@@ -16,13 +18,19 @@ type urlBuilder struct {
|
||||
// should be returned
|
||||
func (ub *urlBuilder) Build() string {
|
||||
|
||||
subscription := ub.Subscription
|
||||
|
||||
if ub.Subscription == "" {
|
||||
subscription = ub.DefaultSubscription
|
||||
}
|
||||
|
||||
if strings.Count(ub.MetricDefinition, "/") > 1 {
|
||||
rn := strings.Split(ub.ResourceName, "/")
|
||||
lastIndex := strings.LastIndex(ub.MetricDefinition, "/")
|
||||
service := ub.MetricDefinition[lastIndex+1:]
|
||||
md := ub.MetricDefinition[0:lastIndex]
|
||||
return fmt.Sprintf("resourceGroups/%s/providers/%s/%s/%s/%s/providers/microsoft.insights/metrics", ub.ResourceGroup, md, rn[0], service, rn[1])
|
||||
return fmt.Sprintf("%s/resourceGroups/%s/providers/%s/%s/%s/%s/providers/microsoft.insights/metrics", subscription, ub.ResourceGroup, md, rn[0], service, rn[1])
|
||||
}
|
||||
|
||||
return fmt.Sprintf("resourceGroups/%s/providers/%s/%s/providers/microsoft.insights/metrics", ub.ResourceGroup, ub.MetricDefinition, ub.ResourceName)
|
||||
return fmt.Sprintf("%s/resourceGroups/%s/providers/%s/%s/providers/microsoft.insights/metrics", subscription, ub.ResourceGroup, ub.MetricDefinition, ub.ResourceName)
|
||||
}
|
||||
|
||||
@@ -11,35 +11,51 @@ func TestURLBuilder(t *testing.T) {
|
||||
|
||||
Convey("when metric definition is in the short form", func() {
|
||||
ub := &urlBuilder{
|
||||
DefaultSubscription: "default-sub",
|
||||
ResourceGroup: "rg",
|
||||
MetricDefinition: "Microsoft.Compute/virtualMachines",
|
||||
ResourceName: "rn",
|
||||
}
|
||||
|
||||
url := ub.Build()
|
||||
So(url, ShouldEqual, "resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics")
|
||||
So(url, ShouldEqual, "default-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics")
|
||||
})
|
||||
|
||||
Convey("when metric definition is in the short form and a subscription is defined", func() {
|
||||
ub := &urlBuilder{
|
||||
DefaultSubscription: "default-sub",
|
||||
Subscription: "specified-sub",
|
||||
ResourceGroup: "rg",
|
||||
MetricDefinition: "Microsoft.Compute/virtualMachines",
|
||||
ResourceName: "rn",
|
||||
}
|
||||
|
||||
url := ub.Build()
|
||||
So(url, ShouldEqual, "specified-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics")
|
||||
})
|
||||
|
||||
Convey("when metric definition is Microsoft.Storage/storageAccounts/blobServices", func() {
|
||||
ub := &urlBuilder{
|
||||
DefaultSubscription: "default-sub",
|
||||
ResourceGroup: "rg",
|
||||
MetricDefinition: "Microsoft.Storage/storageAccounts/blobServices",
|
||||
ResourceName: "rn1/default",
|
||||
}
|
||||
|
||||
url := ub.Build()
|
||||
So(url, ShouldEqual, "resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/providers/microsoft.insights/metrics")
|
||||
So(url, ShouldEqual, "default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/providers/microsoft.insights/metrics")
|
||||
})
|
||||
|
||||
Convey("when metric definition is Microsoft.Storage/storageAccounts/fileServices", func() {
|
||||
ub := &urlBuilder{
|
||||
DefaultSubscription: "default-sub",
|
||||
ResourceGroup: "rg",
|
||||
MetricDefinition: "Microsoft.Storage/storageAccounts/fileServices",
|
||||
ResourceName: "rn1/default",
|
||||
}
|
||||
|
||||
url := ub.Build()
|
||||
So(url, ShouldEqual, "resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/providers/microsoft.insights/metrics")
|
||||
So(url, ShouldEqual, "default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/providers/microsoft.insights/metrics")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
package errutil
|
||||
|
||||
import "golang.org/x/xerrors"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Wrap is a simple wrapper around Errorf that is doing error wrapping. You can read how that works in
|
||||
// https://godoc.org/golang.org/x/xerrors#Errorf but its API is very implicit which is a reason for this wrapper.
|
||||
// There is also a discussion (https://github.com/golang/go/issues/29934) where many comments make arguments for such
|
||||
// wrapper so hopefully it will be added in the standard lib later.
|
||||
func Wrap(message string, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return xerrors.Errorf("%v: %w", message, err)
|
||||
}
|
||||
|
||||
// Wrapf is a simple wrapper around Errorf that is doing error wrapping
|
||||
// Wrapf allows you to send a format and args instead of just a message.
|
||||
func Wrapf(err error, message string, a ...interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Wrap(fmt.Sprintf(message, a...), err)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -66,3 +67,19 @@ func GetAgeString(t time.Time) string {
|
||||
|
||||
return "< 1m"
|
||||
}
|
||||
|
||||
// ToCamelCase changes kebab case, snake case or mixed strings to camel case. See unit test for examples.
|
||||
func ToCamelCase(str string) string {
|
||||
var finalParts []string
|
||||
parts := strings.Split(str, "_")
|
||||
|
||||
for _, part := range parts {
|
||||
finalParts = append(finalParts, strings.Split(part, "-")...)
|
||||
}
|
||||
|
||||
for index, part := range finalParts[1:] {
|
||||
finalParts[index+1] = strings.Title(part)
|
||||
}
|
||||
|
||||
return strings.Join(finalParts, "")
|
||||
}
|
||||
|
||||
@@ -37,3 +37,12 @@ func TestDateAge(t *testing.T) {
|
||||
So(GetAgeString(time.Now().Add(-time.Hour*24*409)), ShouldEqual, "1y")
|
||||
})
|
||||
}
|
||||
|
||||
func TestToCamelCase(t *testing.T) {
|
||||
Convey("ToCamelCase", t, func() {
|
||||
So(ToCamelCase("kebab-case-string"), ShouldEqual, "kebabCaseString")
|
||||
So(ToCamelCase("snake_case_string"), ShouldEqual, "snakeCaseString")
|
||||
So(ToCamelCase("mixed-case_string"), ShouldEqual, "mixedCaseString")
|
||||
So(ToCamelCase("alreadyCamelCase"), ShouldEqual, "alreadyCamelCase")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ export class HelpCtrl {
|
||||
{ keys: ['g', 'h'], description: 'Go to Home Dashboard' },
|
||||
{ keys: ['g', 'p'], description: 'Go to Profile' },
|
||||
{ keys: ['s', 'o'], description: 'Open search' },
|
||||
{ keys: ['s', 's'], description: 'Open search with starred filter' },
|
||||
{ keys: ['s', 't'], description: 'Open search in tags view' },
|
||||
{ keys: ['esc'], description: 'Exit edit/setting views' },
|
||||
],
|
||||
Dashboard: [
|
||||
|
||||
@@ -32,6 +32,10 @@ class SearchQueryParser {
|
||||
}
|
||||
}
|
||||
|
||||
interface OpenSearchParams {
|
||||
query?: string;
|
||||
}
|
||||
|
||||
export class SearchCtrl {
|
||||
isOpen: boolean;
|
||||
query: SearchQuery;
|
||||
@@ -88,7 +92,7 @@ export class SearchCtrl {
|
||||
appEvents.emit('search-query');
|
||||
}
|
||||
|
||||
openSearch(evt, payload) {
|
||||
openSearch(payload: OpenSearchParams = {}) {
|
||||
if (this.isOpen) {
|
||||
this.closeSearch();
|
||||
return;
|
||||
@@ -99,19 +103,16 @@ export class SearchCtrl {
|
||||
this.selectedIndex = -1;
|
||||
this.results = [];
|
||||
this.query = {
|
||||
query: evt ? `${evt.query} ` : '',
|
||||
parsedQuery: this.queryParser.parse(evt && evt.query),
|
||||
query: payload.query ? `${payload.query} ` : '',
|
||||
parsedQuery: this.queryParser.parse(payload.query),
|
||||
tags: [],
|
||||
starred: false,
|
||||
};
|
||||
|
||||
this.currentSearchId = 0;
|
||||
this.ignoreClose = true;
|
||||
this.isLoading = true;
|
||||
|
||||
if (payload && payload.starred) {
|
||||
this.query.starred = true;
|
||||
}
|
||||
|
||||
this.$timeout(() => {
|
||||
this.ignoreClose = false;
|
||||
this.giveSearchFocus = true;
|
||||
|
||||
@@ -356,7 +356,12 @@ export function seriesDataToLogsModel(seriesData: SeriesData[], intervalMs: numb
|
||||
return logsModel;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return {
|
||||
hasUniqueLabels: false,
|
||||
rows: [],
|
||||
meta: [],
|
||||
series: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function logSeriesToLogsModel(logSeries: SeriesData[]): LogsModel {
|
||||
|
||||
@@ -43,21 +43,11 @@ export class KeybindingSrv {
|
||||
this.bind('g h', this.goToHome);
|
||||
this.bind('g a', this.openAlerting);
|
||||
this.bind('g p', this.goToProfile);
|
||||
this.bind('s s', this.openSearchStarred);
|
||||
this.bind('s o', this.openSearch);
|
||||
this.bind('s t', this.openSearchTags);
|
||||
this.bind('f', this.openSearch);
|
||||
this.bindGlobal('esc', this.exit);
|
||||
}
|
||||
|
||||
openSearchStarred() {
|
||||
appEvents.emit('show-dash-search', { starred: true });
|
||||
}
|
||||
|
||||
openSearchTags() {
|
||||
appEvents.emit('show-dash-search', { tagsMode: true });
|
||||
}
|
||||
|
||||
openSearch() {
|
||||
appEvents.emit('show-dash-search');
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ describe('file_export', () => {
|
||||
[0x123, 'some string with \n in the middle', 10.01, false],
|
||||
[0b1011, 'some string with ; in the middle', -12.34, true],
|
||||
[123, 'some string with ;; in the middle', -12.34, true],
|
||||
[1234, '=a bogus formula ', '-and another', '+another', '@ref'],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -108,7 +109,8 @@ describe('file_export', () => {
|
||||
'501;"some string with "" at the end""";0.01;false\r\n' +
|
||||
'291;"some string with \n in the middle";10.01;false\r\n' +
|
||||
'11;"some string with ; in the middle";-12.34;true\r\n' +
|
||||
'123;"some string with ;; in the middle";-12.34;true';
|
||||
'123;"some string with ;; in the middle";-12.34;true\r\n' +
|
||||
'1234;"\'=a bogus formula";"\'-and another";"\'+another";"\'@ref"';
|
||||
|
||||
expect(returnedText).toBe(expectedText);
|
||||
});
|
||||
|
||||
@@ -333,22 +333,29 @@ describe('LogsParsers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const emptyLogsModel = {
|
||||
hasUniqueLabels: false,
|
||||
rows: [],
|
||||
meta: [],
|
||||
series: [],
|
||||
};
|
||||
|
||||
describe('seriesDataToLogsModel', () => {
|
||||
it('given empty series should return undefined', () => {
|
||||
expect(seriesDataToLogsModel([] as SeriesData[], 0)).toBeUndefined();
|
||||
it('given empty series should return empty logs model', () => {
|
||||
expect(seriesDataToLogsModel([] as SeriesData[], 0)).toMatchObject(emptyLogsModel);
|
||||
});
|
||||
|
||||
it('given series without correct series name should not be processed', () => {
|
||||
it('given series without correct series name should return empty logs model', () => {
|
||||
const series: SeriesData[] = [
|
||||
{
|
||||
fields: [],
|
||||
rows: [],
|
||||
},
|
||||
];
|
||||
expect(seriesDataToLogsModel(series, 0)).toBeUndefined();
|
||||
expect(seriesDataToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
|
||||
});
|
||||
|
||||
it('given series without a time field should not be processed', () => {
|
||||
it('given series without a time field should return empty logs model', () => {
|
||||
const series: SeriesData[] = [
|
||||
{
|
||||
fields: [
|
||||
@@ -360,10 +367,10 @@ describe('seriesDataToLogsModel', () => {
|
||||
rows: [],
|
||||
},
|
||||
];
|
||||
expect(seriesDataToLogsModel(series, 0)).toBeUndefined();
|
||||
expect(seriesDataToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
|
||||
});
|
||||
|
||||
it('given series without a string field should not be processed', () => {
|
||||
it('given series without a string field should return empty logs model', () => {
|
||||
const series: SeriesData[] = [
|
||||
{
|
||||
fields: [
|
||||
@@ -375,7 +382,7 @@ describe('seriesDataToLogsModel', () => {
|
||||
rows: [],
|
||||
},
|
||||
];
|
||||
expect(seriesDataToLogsModel(series, 0)).toBeUndefined();
|
||||
expect(seriesDataToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
|
||||
});
|
||||
|
||||
it('given one series should return expected logs model', () => {
|
||||
|
||||
@@ -17,7 +17,11 @@ function csvEscaped(text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.split(QUOTE).join(QUOTE + QUOTE);
|
||||
return text
|
||||
.split(QUOTE)
|
||||
.join(QUOTE + QUOTE)
|
||||
.replace(/^([-+=@])/, "'$1")
|
||||
.replace(/\s+$/, '');
|
||||
}
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
@@ -212,7 +212,7 @@ exports[`ServerStats Should render table with stats 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="css-17l4171 track-horizontal"
|
||||
className="css-52gpmd track-horizontal"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
@@ -233,7 +233,7 @@ exports[`ServerStats Should render table with stats 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="css-17l4171 track-vertical"
|
||||
className="css-52gpmd track-vertical"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
|
||||
@@ -60,17 +60,14 @@ export class DashNav extends PureComponent<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
onOpenSearch = () => {
|
||||
const { dashboard } = this.props;
|
||||
const haveFolder = dashboard.meta.folderId > 0;
|
||||
appEvents.emit(
|
||||
'show-dash-search',
|
||||
haveFolder
|
||||
? {
|
||||
onDahboardNameClick = () => {
|
||||
appEvents.emit('show-dash-search');
|
||||
};
|
||||
|
||||
onFolderNameClick = () => {
|
||||
appEvents.emit('show-dash-search', {
|
||||
query: 'folder:current',
|
||||
}
|
||||
: null
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
@@ -148,11 +145,20 @@ export class DashNav extends PureComponent<Props> {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a className="navbar-page-btn" onClick={this.onOpenSearch}>
|
||||
<div className="navbar-page-btn">
|
||||
{!this.isInFullscreenOrSettings && <i className="gicon gicon-dashboard" />}
|
||||
{haveFolder && <span className="navbar-page-btn--folder">{folderTitle} / </span>}
|
||||
{dashboard.title} <i className="fa fa-caret-down" />
|
||||
{haveFolder && (
|
||||
<>
|
||||
<a className="navbar-page-btn__folder" onClick={this.onFolderNameClick}>
|
||||
{folderTitle}
|
||||
</a>
|
||||
<i className="fa fa-chevron-right navbar-page-btn__folder-icon" />
|
||||
</>
|
||||
)}
|
||||
<a onClick={this.onDahboardNameClick}>
|
||||
{dashboard.title} <i className="fa fa-caret-down navbar-page-btn__search" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{this.isSettings && <span className="navbar-settings-title"> / Settings</span>}
|
||||
<div className="navbar__spacer" />
|
||||
|
||||
@@ -61,6 +61,7 @@ export interface State {
|
||||
isFullscreen: boolean;
|
||||
fullscreenPanel: PanelModel | null;
|
||||
scrollTop: number;
|
||||
updateScrollTop: number;
|
||||
rememberScrollTop: number;
|
||||
showLoadingState: boolean;
|
||||
}
|
||||
@@ -73,6 +74,7 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
showLoadingState: false,
|
||||
fullscreenPanel: null,
|
||||
scrollTop: 0,
|
||||
updateScrollTop: null,
|
||||
rememberScrollTop: 0,
|
||||
};
|
||||
|
||||
@@ -168,7 +170,7 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
isEditing: false,
|
||||
isFullscreen: false,
|
||||
fullscreenPanel: null,
|
||||
scrollTop: this.state.rememberScrollTop,
|
||||
updateScrollTop: this.state.rememberScrollTop,
|
||||
},
|
||||
this.triggerPanelsRendering.bind(this)
|
||||
);
|
||||
@@ -204,7 +206,7 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
|
||||
setScrollTop = (e: MouseEvent<HTMLElement>): void => {
|
||||
const target = e.target as HTMLElement;
|
||||
this.setState({ scrollTop: target.scrollTop });
|
||||
this.setState({ scrollTop: target.scrollTop, updateScrollTop: null });
|
||||
};
|
||||
|
||||
onAddPanel = () => {
|
||||
@@ -251,7 +253,7 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { dashboard, editview, $injector, isInitSlow, initError } = this.props;
|
||||
const { isSettingsOpening, isEditing, isFullscreen, scrollTop } = this.state;
|
||||
const { isSettingsOpening, isEditing, isFullscreen, scrollTop, updateScrollTop } = this.state;
|
||||
|
||||
if (!dashboard) {
|
||||
if (isInitSlow) {
|
||||
@@ -285,9 +287,9 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
/>
|
||||
<div className="scroll-canvas scroll-canvas--dashboard">
|
||||
<CustomScrollbar
|
||||
autoHeightMin={'100%'}
|
||||
autoHeightMin="100%"
|
||||
setScrollTop={this.setScrollTop}
|
||||
scrollTop={scrollTop}
|
||||
scrollTop={updateScrollTop}
|
||||
updateAfterMountMs={500}
|
||||
className="custom-scrollbar--page"
|
||||
>
|
||||
|
||||
@@ -111,7 +111,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
||||
autoHideTimeout={200}
|
||||
className="custom-scrollbar--page"
|
||||
hideTracksWhenNotNeeded={false}
|
||||
scrollTop={0}
|
||||
scrollTop={null}
|
||||
setScrollTop={[Function]}
|
||||
updateAfterMountMs={500}
|
||||
>
|
||||
@@ -349,7 +349,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
autoHideTimeout={200}
|
||||
className="custom-scrollbar--page"
|
||||
hideTracksWhenNotNeeded={false}
|
||||
scrollTop={0}
|
||||
scrollTop={null}
|
||||
setScrollTop={[Function]}
|
||||
updateAfterMountMs={500}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { DashboardGrid, Props } from './DashboardGrid';
|
||||
import { DashboardModel } from '../state';
|
||||
|
||||
interface ScenarioContext {
|
||||
props: Props;
|
||||
wrapper?: ShallowWrapper<Props, any, DashboardGrid>;
|
||||
setup?: (fn: () => void) => void;
|
||||
setProps: (props: Partial<Props>) => void;
|
||||
}
|
||||
|
||||
function getTestDashboard(overrides?: any, metaOverrides?: any): DashboardModel {
|
||||
const data = Object.assign(
|
||||
{
|
||||
title: 'My dashboard',
|
||||
panels: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'graph',
|
||||
title: 'My graph',
|
||||
gridPos: { x: 0, y: 0, w: 24, h: 10 },
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'graph2',
|
||||
title: 'My graph2',
|
||||
gridPos: { x: 0, y: 10, w: 25, h: 10 },
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'graph3',
|
||||
title: 'My graph3',
|
||||
gridPos: { x: 0, y: 20, w: 25, h: 100 },
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'graph4',
|
||||
title: 'My graph4',
|
||||
gridPos: { x: 0, y: 120, w: 25, h: 10 },
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides
|
||||
);
|
||||
|
||||
const meta = Object.assign({ canSave: true, canEdit: true }, metaOverrides);
|
||||
return new DashboardModel(data, meta);
|
||||
}
|
||||
|
||||
function dashboardGridScenario(description, scenarioFn: (ctx: ScenarioContext) => void) {
|
||||
describe(description, () => {
|
||||
let setupFn: () => void;
|
||||
|
||||
const ctx: ScenarioContext = {
|
||||
setup: fn => {
|
||||
setupFn = fn;
|
||||
},
|
||||
props: {
|
||||
isEditing: false,
|
||||
isFullscreen: false,
|
||||
scrollTop: null,
|
||||
dashboard: getTestDashboard(),
|
||||
},
|
||||
setProps: (props: Partial<Props>) => {
|
||||
Object.assign(ctx.props, props);
|
||||
if (ctx.wrapper) {
|
||||
ctx.wrapper.setProps(ctx.props);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setupFn();
|
||||
ctx.wrapper = shallow(<DashboardGrid {...ctx.props} />);
|
||||
});
|
||||
|
||||
scenarioFn(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
describe('DashboardGrid', () => {
|
||||
dashboardGridScenario('Can render dashboard grid', ctx => {
|
||||
ctx.setup(() => {});
|
||||
|
||||
it('Should render', () => {
|
||||
expect(ctx.wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -205,7 +205,7 @@ export class DashboardGrid extends PureComponent<Props> {
|
||||
return false;
|
||||
}
|
||||
|
||||
const top = parseInt(elem.style.top.replace('px', ''), 10);
|
||||
const top = elem.offsetTop;
|
||||
const height = panel.gridPos.h * GRID_CELL_HEIGHT + 40;
|
||||
const bottom = top + height;
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
import DashboardGrid from './DashboardGrid';
|
||||
|
||||
react2AngularDirective('dashboardGrid', DashboardGrid, [['dashboard', { watchDepth: 'reference' }]]);
|
||||
@@ -250,9 +250,10 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
{loading === LoadingState.Loading && this.renderLoadingState()}
|
||||
<div className="panel-content">
|
||||
<PanelComponent
|
||||
id={panel.id}
|
||||
data={data}
|
||||
timeRange={data.request ? data.request.range : this.timeSrv.timeRange()}
|
||||
options={panel.getOptions(plugin.defaults)}
|
||||
options={panel.getOptions()}
|
||||
width={width - theme.panelPadding * 2}
|
||||
height={innerPanelHeight}
|
||||
renderCounter={renderCounter}
|
||||
|
||||
@@ -81,6 +81,7 @@ export class PanelHeader extends Component<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={panelHeaderClass}>
|
||||
<PanelHeaderCorner
|
||||
panel={panel}
|
||||
title={panel.title}
|
||||
@@ -89,8 +90,12 @@ export class PanelHeader extends Component<Props, State> {
|
||||
links={panel.links}
|
||||
error={error}
|
||||
/>
|
||||
<div className={panelHeaderClass}>
|
||||
<div className="panel-title-container" onClick={this.onMenuToggle} onMouseDown={this.onMouseDown}>
|
||||
<div
|
||||
className="panel-title-container"
|
||||
onClick={this.onMenuToggle}
|
||||
onMouseDown={this.onMouseDown}
|
||||
aria-label="Panel Title"
|
||||
>
|
||||
<div className="panel-title">
|
||||
<span className="icon-gf panel-alert-icon" />
|
||||
<span className="panel-title-text">
|
||||
|
||||
@@ -0,0 +1,996 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
<SizeMe(GridWrapper)
|
||||
className="layout"
|
||||
isDraggable={true}
|
||||
isFullscreen={false}
|
||||
isResizable={true}
|
||||
layout={
|
||||
Array [
|
||||
Object {
|
||||
"h": 10,
|
||||
"i": "1",
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
Object {
|
||||
"h": 10,
|
||||
"i": "2",
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
},
|
||||
Object {
|
||||
"h": 100,
|
||||
"i": "3",
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
},
|
||||
Object {
|
||||
"h": 10,
|
||||
"i": "4",
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 120,
|
||||
},
|
||||
]
|
||||
}
|
||||
onDragStop={[Function]}
|
||||
onLayoutChange={[Function]}
|
||||
onResize={[Function]}
|
||||
onResizeStop={[Function]}
|
||||
onWidthChange={[Function]}
|
||||
>
|
||||
<div
|
||||
className=""
|
||||
id="panel-1"
|
||||
key="1"
|
||||
>
|
||||
<DashboardPanel
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
"list": Array [
|
||||
Object {
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
},
|
||||
"autoUpdate": undefined,
|
||||
"description": undefined,
|
||||
"editable": true,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {
|
||||
"panel-added": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"panel-removed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"repeats-processed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-collapsed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-expanded": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"view-mode-changed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
},
|
||||
"_eventsCount": 6,
|
||||
},
|
||||
},
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": Array [],
|
||||
"meta": Object {
|
||||
"canEdit": true,
|
||||
"canMakeEditable": false,
|
||||
"canSave": true,
|
||||
"canShare": true,
|
||||
"canStar": true,
|
||||
"fullscreen": false,
|
||||
"isEditing": false,
|
||||
"showSettings": true,
|
||||
},
|
||||
"originalTemplating": Array [],
|
||||
"originalTime": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"panels": Array [
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph",
|
||||
"transparent": false,
|
||||
"type": "graph",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
},
|
||||
"id": 2,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph2",
|
||||
"transparent": false,
|
||||
"type": "graph2",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 100,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
},
|
||||
"id": 3,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph3",
|
||||
"transparent": false,
|
||||
"type": "graph3",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 120,
|
||||
},
|
||||
"id": 4,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph4",
|
||||
"transparent": false,
|
||||
"type": "graph4",
|
||||
},
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 18,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
"templating": Object {
|
||||
"list": Array [],
|
||||
},
|
||||
"time": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"timepicker": Object {},
|
||||
"timezone": "",
|
||||
"title": "My dashboard",
|
||||
"uid": null,
|
||||
"version": 0,
|
||||
}
|
||||
}
|
||||
isInView={false}
|
||||
panel={
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph",
|
||||
"transparent": false,
|
||||
"type": "graph",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=""
|
||||
id="panel-2"
|
||||
key="2"
|
||||
>
|
||||
<DashboardPanel
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
"list": Array [
|
||||
Object {
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
},
|
||||
"autoUpdate": undefined,
|
||||
"description": undefined,
|
||||
"editable": true,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {
|
||||
"panel-added": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"panel-removed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"repeats-processed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-collapsed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-expanded": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"view-mode-changed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
},
|
||||
"_eventsCount": 6,
|
||||
},
|
||||
},
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": Array [],
|
||||
"meta": Object {
|
||||
"canEdit": true,
|
||||
"canMakeEditable": false,
|
||||
"canSave": true,
|
||||
"canShare": true,
|
||||
"canStar": true,
|
||||
"fullscreen": false,
|
||||
"isEditing": false,
|
||||
"showSettings": true,
|
||||
},
|
||||
"originalTemplating": Array [],
|
||||
"originalTime": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"panels": Array [
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph",
|
||||
"transparent": false,
|
||||
"type": "graph",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
},
|
||||
"id": 2,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph2",
|
||||
"transparent": false,
|
||||
"type": "graph2",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 100,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
},
|
||||
"id": 3,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph3",
|
||||
"transparent": false,
|
||||
"type": "graph3",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 120,
|
||||
},
|
||||
"id": 4,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph4",
|
||||
"transparent": false,
|
||||
"type": "graph4",
|
||||
},
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 18,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
"templating": Object {
|
||||
"list": Array [],
|
||||
},
|
||||
"time": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"timepicker": Object {},
|
||||
"timezone": "",
|
||||
"title": "My dashboard",
|
||||
"uid": null,
|
||||
"version": 0,
|
||||
}
|
||||
}
|
||||
isInView={false}
|
||||
panel={
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
},
|
||||
"id": 2,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph2",
|
||||
"transparent": false,
|
||||
"type": "graph2",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=""
|
||||
id="panel-3"
|
||||
key="3"
|
||||
>
|
||||
<DashboardPanel
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
"list": Array [
|
||||
Object {
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
},
|
||||
"autoUpdate": undefined,
|
||||
"description": undefined,
|
||||
"editable": true,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {
|
||||
"panel-added": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"panel-removed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"repeats-processed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-collapsed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-expanded": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"view-mode-changed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
},
|
||||
"_eventsCount": 6,
|
||||
},
|
||||
},
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": Array [],
|
||||
"meta": Object {
|
||||
"canEdit": true,
|
||||
"canMakeEditable": false,
|
||||
"canSave": true,
|
||||
"canShare": true,
|
||||
"canStar": true,
|
||||
"fullscreen": false,
|
||||
"isEditing": false,
|
||||
"showSettings": true,
|
||||
},
|
||||
"originalTemplating": Array [],
|
||||
"originalTime": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"panels": Array [
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph",
|
||||
"transparent": false,
|
||||
"type": "graph",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
},
|
||||
"id": 2,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph2",
|
||||
"transparent": false,
|
||||
"type": "graph2",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 100,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
},
|
||||
"id": 3,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph3",
|
||||
"transparent": false,
|
||||
"type": "graph3",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 120,
|
||||
},
|
||||
"id": 4,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph4",
|
||||
"transparent": false,
|
||||
"type": "graph4",
|
||||
},
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 18,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
"templating": Object {
|
||||
"list": Array [],
|
||||
},
|
||||
"time": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"timepicker": Object {},
|
||||
"timezone": "",
|
||||
"title": "My dashboard",
|
||||
"uid": null,
|
||||
"version": 0,
|
||||
}
|
||||
}
|
||||
isInView={false}
|
||||
panel={
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 100,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
},
|
||||
"id": 3,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph3",
|
||||
"transparent": false,
|
||||
"type": "graph3",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=""
|
||||
id="panel-4"
|
||||
key="4"
|
||||
>
|
||||
<DashboardPanel
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
"list": Array [
|
||||
Object {
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
},
|
||||
"autoUpdate": undefined,
|
||||
"description": undefined,
|
||||
"editable": true,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {
|
||||
"panel-added": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"panel-removed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"repeats-processed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-collapsed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-expanded": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"view-mode-changed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
},
|
||||
"_eventsCount": 6,
|
||||
},
|
||||
},
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": Array [],
|
||||
"meta": Object {
|
||||
"canEdit": true,
|
||||
"canMakeEditable": false,
|
||||
"canSave": true,
|
||||
"canShare": true,
|
||||
"canStar": true,
|
||||
"fullscreen": false,
|
||||
"isEditing": false,
|
||||
"showSettings": true,
|
||||
},
|
||||
"originalTemplating": Array [],
|
||||
"originalTime": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"panels": Array [
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph",
|
||||
"transparent": false,
|
||||
"type": "graph",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
},
|
||||
"id": 2,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph2",
|
||||
"transparent": false,
|
||||
"type": "graph2",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 100,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
},
|
||||
"id": 3,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph3",
|
||||
"transparent": false,
|
||||
"type": "graph3",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 120,
|
||||
},
|
||||
"id": 4,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph4",
|
||||
"transparent": false,
|
||||
"type": "graph4",
|
||||
},
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 18,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
"templating": Object {
|
||||
"list": Array [],
|
||||
},
|
||||
"time": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"timepicker": Object {},
|
||||
"timezone": "",
|
||||
"title": "My dashboard",
|
||||
"uid": null,
|
||||
"version": 0,
|
||||
}
|
||||
}
|
||||
isInView={false}
|
||||
panel={
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 120,
|
||||
},
|
||||
"id": 4,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph4",
|
||||
"transparent": false,
|
||||
"type": "graph4",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SizeMe(GridWrapper)>
|
||||
`;
|
||||
@@ -1,5 +1,3 @@
|
||||
import './dashgrid/DashboardGridDirective';
|
||||
|
||||
// Services
|
||||
import './services/UnsavedChangesSrv';
|
||||
import './services/DashboardLoaderSrv';
|
||||
|
||||
@@ -53,8 +53,8 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
getReactPanelOptions = () => {
|
||||
const { panel, plugin } = this.props;
|
||||
return panel.getOptions(plugin.defaults);
|
||||
const { panel } = this.props;
|
||||
return panel.getOptions();
|
||||
};
|
||||
|
||||
renderPanelOptions() {
|
||||
|
||||
@@ -7,13 +7,8 @@ describe('PanelModel', () => {
|
||||
describe('when creating new panel model', () => {
|
||||
let model;
|
||||
let modelJson;
|
||||
|
||||
beforeEach(() => {
|
||||
modelJson = {
|
||||
type: 'table',
|
||||
showColumns: true,
|
||||
targets: [{ refId: 'A' }, { noRefId: true }],
|
||||
options: {
|
||||
let persistedOptionsMock;
|
||||
const defaultOptionsMock = {
|
||||
fieldOptions: {
|
||||
thresholds: [
|
||||
{
|
||||
@@ -28,24 +23,54 @@ describe('PanelModel', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
showThresholds: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
persistedOptionsMock = {
|
||||
fieldOptions: {
|
||||
thresholds: [
|
||||
{
|
||||
color: '#F2495C',
|
||||
index: 1,
|
||||
value: 50,
|
||||
},
|
||||
{
|
||||
color: '#73BF69',
|
||||
index: 0,
|
||||
value: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
modelJson = {
|
||||
type: 'table',
|
||||
showColumns: true,
|
||||
targets: [{ refId: 'A' }, { noRefId: true }],
|
||||
options: persistedOptionsMock,
|
||||
};
|
||||
|
||||
model = new PanelModel(modelJson);
|
||||
model.pluginLoaded(
|
||||
getPanelPlugin(
|
||||
const panelPlugin = getPanelPlugin(
|
||||
{
|
||||
id: 'table',
|
||||
},
|
||||
null, // react
|
||||
TablePanelCtrl // angular
|
||||
)
|
||||
);
|
||||
panelPlugin.setDefaults(defaultOptionsMock);
|
||||
model.pluginLoaded(panelPlugin);
|
||||
});
|
||||
|
||||
it('should apply defaults', () => {
|
||||
expect(model.gridPos.h).toBe(3);
|
||||
});
|
||||
|
||||
it('should apply option defaults', () => {
|
||||
expect(model.getOptions().showThresholds).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set model props on instance', () => {
|
||||
expect(model.showColumns).toBe(true);
|
||||
});
|
||||
@@ -89,11 +114,22 @@ describe('PanelModel', () => {
|
||||
});
|
||||
|
||||
describe('when changing panel type', () => {
|
||||
const newPanelPluginDefaults = {
|
||||
showThresholdLabels: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
model.changePlugin(getPanelPlugin({ id: 'graph' }));
|
||||
const newPlugin = getPanelPlugin({ id: 'graph' });
|
||||
newPlugin.setDefaults(newPanelPluginDefaults);
|
||||
model.changePlugin(newPlugin);
|
||||
model.alert = { id: 2 };
|
||||
});
|
||||
|
||||
it('should apply next panel option defaults', () => {
|
||||
expect(model.getOptions().showThresholdLabels).toBeFalsy();
|
||||
expect(model.getOptions().showThresholds).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should remove table properties but keep core props', () => {
|
||||
expect(model.showColumns).toBe(undefined);
|
||||
});
|
||||
@@ -153,19 +189,5 @@ describe('PanelModel', () => {
|
||||
expect(panelQueryRunner).toBe(sameQueryRunner);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get panel options', () => {
|
||||
it('should apply defaults', () => {
|
||||
model.options = { existingProp: 10 };
|
||||
const options = model.getOptions({
|
||||
defaultProp: true,
|
||||
existingProp: 0,
|
||||
});
|
||||
|
||||
expect(options.defaultProp).toBe(true);
|
||||
expect(options.existingProp).toBe(10);
|
||||
expect(model.options).toBe(options);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,8 +157,8 @@ export class PanelModel {
|
||||
}
|
||||
}
|
||||
|
||||
getOptions(panelDefaults: any) {
|
||||
return _.defaultsDeep(this.options || {}, panelDefaults);
|
||||
getOptions() {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
updateOptions(options: object) {
|
||||
@@ -179,7 +179,6 @@ export class PanelModel {
|
||||
|
||||
model[property] = _.cloneDeep(this[property]);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
@@ -247,9 +246,18 @@ export class PanelModel {
|
||||
});
|
||||
}
|
||||
|
||||
private applyPluginOptionDefaults(plugin: PanelPlugin) {
|
||||
if (plugin.angularConfigCtrl) {
|
||||
return;
|
||||
}
|
||||
this.options = _.defaultsDeep({}, this.options || {}, plugin.defaults);
|
||||
}
|
||||
|
||||
pluginLoaded(plugin: PanelPlugin) {
|
||||
this.plugin = plugin;
|
||||
|
||||
this.applyPluginOptionDefaults(plugin);
|
||||
|
||||
if (plugin.panel && plugin.onPanelMigration) {
|
||||
const version = getPluginVersion(plugin);
|
||||
if (version !== this.pluginVersion) {
|
||||
@@ -284,7 +292,7 @@ export class PanelModel {
|
||||
// switch
|
||||
this.type = pluginId;
|
||||
this.plugin = newPlugin;
|
||||
|
||||
this.applyPluginOptionDefaults(newPlugin);
|
||||
// Let panel plugins inspect options from previous panel and keep any that it can use
|
||||
if (newPlugin.onPanelTypeChanged) {
|
||||
this.options = this.options || {};
|
||||
@@ -324,7 +332,7 @@ export class PanelModel {
|
||||
}
|
||||
|
||||
hasTitle() {
|
||||
return !!this.title.length;
|
||||
return this.title && this.title.length > 0;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
@@ -97,9 +97,6 @@ export class PanelQueryRunner {
|
||||
delayStateNotification,
|
||||
} = options;
|
||||
|
||||
// filter out hidden queries & deep clone them
|
||||
const clonedAndFilteredQueries = cloneDeep(queries.filter(q => !q.hide));
|
||||
|
||||
const request: DataQueryRequest = {
|
||||
requestId: getNextRequestId(),
|
||||
timezone,
|
||||
@@ -109,7 +106,7 @@ export class PanelQueryRunner {
|
||||
timeInfo,
|
||||
interval: '',
|
||||
intervalMs: 0,
|
||||
targets: clonedAndFilteredQueries,
|
||||
targets: cloneDeep(queries),
|
||||
maxDataPoints: maxDataPoints || widthPixels,
|
||||
scopedVars: scopedVars || {},
|
||||
cacheTimeout,
|
||||
@@ -124,6 +121,10 @@ export class PanelQueryRunner {
|
||||
try {
|
||||
const ds = await getDataSource(datasource, request.scopedVars);
|
||||
|
||||
if (ds.meta && !ds.meta.hiddenQueries) {
|
||||
request.targets = request.targets.filter(q => !q.hide);
|
||||
}
|
||||
|
||||
// Attach the datasource name to each query
|
||||
request.targets = request.targets.map(query => {
|
||||
if (!query.datasource) {
|
||||
|
||||
@@ -275,7 +275,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
||||
<LogsContainer
|
||||
width={width}
|
||||
exploreId={exploreId}
|
||||
onChangeTime={this.onChangeTime}
|
||||
onClickLabel={this.onClickLabel}
|
||||
onStartScanning={this.onStartScanning}
|
||||
onStopScanning={this.onStopScanning}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import moment from 'moment';
|
||||
import { RawTimeRange, TimeRange, LogLevel, TimeZone, AbsoluteTimeRange } from '@grafana/ui';
|
||||
|
||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||
import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model';
|
||||
import { StoreState } from 'app/types';
|
||||
|
||||
import { toggleLogs, changeDedupStrategy } from './state/actions';
|
||||
import { toggleLogs, changeDedupStrategy, changeTime } from './state/actions';
|
||||
import Logs from './Logs';
|
||||
import Panel from './Panel';
|
||||
import { toggleLogLevelAction } from 'app/features/explore/state/actionTypes';
|
||||
@@ -20,7 +21,6 @@ interface LogsContainerProps {
|
||||
logsHighlighterExpressions?: string[];
|
||||
logsResult?: LogsModel;
|
||||
dedupedResult?: LogsModel;
|
||||
onChangeTime: (range: AbsoluteTimeRange) => void;
|
||||
onClickLabel: (key: string, value: string) => void;
|
||||
onStartScanning: () => void;
|
||||
onStopScanning: () => void;
|
||||
@@ -35,9 +35,19 @@ interface LogsContainerProps {
|
||||
dedupStrategy: LogsDedupStrategy;
|
||||
hiddenLogLevels: Set<LogLevel>;
|
||||
width: number;
|
||||
changeTime: typeof changeTime;
|
||||
}
|
||||
|
||||
export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
onChangeTime = (absRange: AbsoluteTimeRange) => {
|
||||
const { exploreId, timeZone, changeTime } = this.props;
|
||||
const range = {
|
||||
from: timeZone.isUtc ? moment.utc(absRange.from) : moment(absRange.from),
|
||||
to: timeZone.isUtc ? moment.utc(absRange.to) : moment(absRange.to),
|
||||
};
|
||||
|
||||
changeTime(exploreId, range);
|
||||
};
|
||||
onClickLogsButton = () => {
|
||||
this.props.toggleLogs(this.props.exploreId, this.props.showingLogs);
|
||||
};
|
||||
@@ -61,7 +71,6 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
logsHighlighterExpressions,
|
||||
logsResult,
|
||||
dedupedResult,
|
||||
onChangeTime,
|
||||
onClickLabel,
|
||||
onStartScanning,
|
||||
onStopScanning,
|
||||
@@ -83,7 +92,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
exploreId={exploreId}
|
||||
highlighterExpressions={logsHighlighterExpressions}
|
||||
loading={loading}
|
||||
onChangeTime={onChangeTime}
|
||||
onChangeTime={this.onChangeTime}
|
||||
onClickLabel={onClickLabel}
|
||||
onStartScanning={onStartScanning}
|
||||
onStopScanning={onStopScanning}
|
||||
@@ -130,6 +139,7 @@ const mapDispatchToProps = {
|
||||
toggleLogs,
|
||||
changeDedupStrategy,
|
||||
toggleLogLevelAction,
|
||||
changeTime,
|
||||
};
|
||||
|
||||
export default hot(module)(
|
||||
|
||||
@@ -26,7 +26,7 @@ export default class Table extends PureComponent<TableProps> {
|
||||
if (e.target) {
|
||||
const link = e.target as HTMLElement;
|
||||
if (link.className === 'link') {
|
||||
const columnKey = column.Header;
|
||||
const columnKey = column.Header().props.title;
|
||||
const rowValue = rowInfo.row[columnKey];
|
||||
this.props.onClickCell(columnKey, rowValue);
|
||||
}
|
||||
|
||||
@@ -220,6 +220,7 @@ export interface LoadExploreDataSourcesPayload {
|
||||
|
||||
export interface RunQueriesPayload {
|
||||
exploreId: ExploreId;
|
||||
range: TimeRange;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -559,6 +559,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe
|
||||
supportsTable,
|
||||
datasourceError,
|
||||
containerWidth,
|
||||
range,
|
||||
} = getState().explore[exploreId];
|
||||
|
||||
if (datasourceError) {
|
||||
@@ -576,7 +577,10 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe
|
||||
// but we're using the datasource interval limit for now
|
||||
const interval = datasourceInstance.interval;
|
||||
|
||||
dispatch(runQueriesAction({ exploreId }));
|
||||
const timeZone = getTimeZone(getState().user);
|
||||
const updatedRange = getTimeRange(timeZone, range.raw);
|
||||
|
||||
dispatch(runQueriesAction({ exploreId, range: updatedRange }));
|
||||
// Keep table queries first since they need to return quickly
|
||||
const tableQueriesPromise =
|
||||
(ignoreUIState || showingTable) && supportsTable
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user