GraphNG - shared cursor (#33433)

* Initial work

* WIP add cursor in debug panel

* shared cursor.sync filter

* explicit uplot events

* explicit uplot events

* uplot events

* uplot events

* depend on master uplot

* sync sync sync

* Fix merge

* Get rid of PlotSyncContext and sync tooltip positions

* make sync optional

* Improve shared tooltip positioning

* Plugins: add level and signature badges to plugin details page (#33553)

* feat(grafana-ui): badge can accept react node for text, add shield-exclamation to icons

* feat(plugins): add PluginSignatureType type

* feat(pluginpage): introduce PluginSignatureDetailsBadge. Fix sidebar icon margin

* feat(pluginlistpage): update filterinput placeholder, introduce filter by plugin type

* Variables: Removes the never refresh option (#33533)

* Variables: Removes the never refresh option

* Tests: fixes DashboardModel repeat tests

* Tests: fixs snapshots

* Tests: fixes processVariable test

* Tests: fixes DashboardModel tests

* PageLayout: Fixes max-width breakpoint so that it triggers only when there is room for margin+ (#33558)

* Alerting: Remove datasource (name) from migration (#33544)

no longer needed as of https://github.com/grafana/grafana/pull/33416
for https://github.com/grafana/alerting-squad/issues/126

* Library panels: Adds description to library panels tab (#33428)

* CodeOwners: Set owners of unified alerting migration (#33571)

* ButtonSelect: updates component with the new theme model (#33565)

* EmptySearchResult: updates component with the new theme model (#33573)

* DashboardSettings: Slight design tweak to fix page toolbar padding and align design (#33575)

* DashboardSettings: Slight design tweak to fix page toolbar padding and align design

* Fixed font weight

* Removed comment

* Update

* gitignore: Ignore files for accesscontrol provisioning (#33577)

* Alerting/metrics (#33547)

* moves alerting metrics to their own pkg

* adds grafana_alerting_alerts (by state) metric

* alerts_received_{total,invalid}

* embed alertmanager alerting struct in ng metrics & remove duplicated notification metrics (already embed alertmanager notifier metrics)

* use silence metrics from alertmanager lib

* fix - manager has metrics

* updates ngalert tests

* comment lint
Signed-off-by: Owen Diehl <ow.diehl@gmail.com>

* cleaner prom registry code

* removes ngalert global metrics

* new registry use in all tests

* ngalert metrics impl service, hack testinfra code to prevent duplicate metric registrations

* nilmetrics unexported

* Add note to Snapshot API doc to specify that user has to provide the entire dashboard model  (#33572)

* Added note as suggested by Macus E.

* Update docs/sources/http_api/snapshot.md

Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com>

Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com>

* Alerting: backend "ng" code cleanup (#33578)

* AlertMigration: remove alert_rule UID db check (#33568)

do not believe this is needed due to uniqueness promised by shortid lib
since there is no provisioning yet. https://github.com/teris-io/shortid

* Live: persisting last message in cache for broadcast scope (#32938)

* Alerting: Load annotations from rule into State cache (#33542)

for https://github.com/grafana/alerting-squad/issues/127

* add template for dashboard url parameters  (#33549)

* Update dashboard-links.md

parameters with plain text like `var-something=value` can make confusion. 
template it to clarify .

* describe way for template link.

* AlertingMigration: Create alert_rule_version entry (#33585)

Create the alert rule version entry during the migration so it is consistent with rules created via api.
for https://github.com/grafana/alerting-squad/issues/123

* Build: Fix with cleanup call maybe? (#33590)

* add selector for code editor (#33554)

* broadcast over eventBus

* broadcasting to eventbus (but not useing it yet)

* merge master

* moved to context

* fix yarn.lock

* update snapshot

* Fix direct state mutation

* Persist location state on partial updates

* GraphNG- use getStream rather than subscribe

* Sync LegacyGraphHoverEvent with GraphNG

* Chenge plotRef signature

* use subscription

* subscription

* one fewer file

* Update types

* Remove unnecessary filtering

* Disable cursor sync when in edit mode

* GraphNG - bring back logging

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
Co-authored-by: Kyle Brandt <kyle@grafana.com>
Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com>
Co-authored-by: Uchechukwu Obasi <obasiuche62@gmail.com>
Co-authored-by: gamab <gamab@users.noreply.github.com>
Co-authored-by: Owen Diehl <ow.diehl@gmail.com>
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com>
Co-authored-by: Alexander Emelin <frvzmb@gmail.com>
Co-authored-by: Nagle Zhang <nagle.zhang@sap.com>
Co-authored-by: Erik Sundell <erik.sundell@grafana.com>
This commit is contained in:
Dominik Prokop
2021-05-10 14:24:23 +02:00
committed by GitHub
parent 4fc1810cb7
commit 9f8fa4212f
34 changed files with 1844 additions and 142 deletions

View File

@@ -0,0 +1,561 @@
{
"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": 2,
"id": 396,
"links": [],
"panels": [
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 8,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltipOptions": {
"mode": "single"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "-9.4, -8.2, -8, -8, -7, -7.9, -5.9, -6.1, -5.5, -5.8, -4.7, -4.7, -4.1, -4.5, -3.5, -4, -2.4, -3.4, -1, -1, -0.3, -0.8, 0.8, 0.5, 1.7, 1.2, 2, 1.9, 2.4, 2.3, 3.3, 3.2, 3.9, 3.8, 4.5, 4.5, 5.6, 5.1, 7.1, 6.7, 7.2, 7.1, 7.6, 7.5, 8.4, 7.9, 9, 8.5, 9.4, 9.2"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Origin",
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "degree"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "B-series"
},
"properties": [
{
"id": "unit",
"value": "tflops"
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 8,
"x": 8,
"y": 0
},
"id": 9,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltipOptions": {
"mode": "single"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"hide": false,
"max": 10,
"min": -10,
"noise": 1,
"refId": "A",
"scenarioId": "random_walk",
"spread": 2
},
{
"hide": false,
"max": 10,
"min": -10,
"noise": 1,
"refId": "B",
"scenarioId": "random_walk",
"spread": 2
},
{
"hide": false,
"max": 10,
"min": -10,
"noise": 1,
"refId": "C",
"scenarioId": "random_walk",
"spread": 2
}
],
"timeFrom": null,
"timeShift": null,
"title": "Sensor 1, ranging as Origin panel",
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "degree"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "B-series"
},
"properties": [
{
"id": "unit",
"value": "tflops"
}
]
},
{
"matcher": {
"id": "byName",
"options": "A-series"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "purple",
"mode": "fixed"
}
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 8,
"x": 16,
"y": 0
},
"id": 10,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltipOptions": {
"mode": "single"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"hide": false,
"max": 4,
"min": -4,
"noise": 0,
"refId": "B",
"scenarioId": "random_walk",
"spread": 2
},
{
"hide": false,
"max": 4,
"min": -4,
"noise": 0,
"refId": "A",
"scenarioId": "random_walk",
"spread": 2
}
],
"timeFrom": null,
"timeShift": null,
"title": "Sensor 2, ranging [-4, 4]",
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "degree"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "B-series"
},
"properties": [
{
"id": "unit",
"value": "tflops"
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 8,
"x": 8,
"y": 9
},
"id": 7,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltipOptions": {
"mode": "single"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "5, 4.6, 4.4, 3.5, 3.3, 3, 2.6, 2.6, 2.4, 2.1, 1.8, 1.8, 1.7, 1.6, 1.5, 1.5, 1.4, 0.5, 0.5, 0, -0.2, -0.2, -0.4, -0.6, -1.2, -1.6, -2.4, -2.4, -2.8, -2.9, -2.9, -3.2, -3.3, -3.4, -4.3, -4.3, -4.6, -6.1, -6.3, -6.6, -6.7, -7, -7.2, -7.3, -7.5, -7.8, -8.2, -8.3, -9.1, -9.3"
},
{
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "6,4,2"
},
{
"hide": false,
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "-3,-4,-2"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Sensor 3",
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "degree"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "B-series"
},
"properties": [
{
"id": "unit",
"value": "tflops"
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 8,
"x": 16,
"y": 9
},
"id": 8,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltipOptions": {
"mode": "single"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "6,4,2"
},
{
"hide": false,
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "-3,-4,-2"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Sensor 5",
"type": "timeseries"
}
],
"refresh": false,
"schemaVersion": 28,
"style": "dark",
"tags": [
"gdev",
"panel-tests",
"graph-ng"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Panel Tests - shared tooltips cursor positioning",
"uid": "ICiqZ1rGk",
"version": 51
}

View File

@@ -0,0 +1,780 @@
{
"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": 1,
"links": [],
"panels": [
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "celsius"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Speed"
},
"properties": [
{
"id": "unit",
"value": "velocitykmh"
}
]
}
]
},
"gridPos": {
"h": 8,
"w": 9,
"x": 0,
"y": 0
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltipOptions": {
"mode": "single"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"alias": "Temperature",
"max": 25,
"min": 1,
"refId": "A",
"scenarioId": "random_walk"
},
{
"alias": "Speed",
"hide": false,
"refId": "B",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "two units",
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "points",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "celsius"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Temperature"
},
"properties": [
{
"id": "unit",
"value": "celsius"
}
]
},
{
"matcher": {
"id": "byName",
"options": "Speed"
},
"properties": [
{
"id": "unit",
"value": "velocitykmh"
}
]
}
]
},
"gridPos": {
"h": 8,
"w": 8,
"x": 9,
"y": 0
},
"id": 13,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltipOptions": {
"mode": "single"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"max": 25,
"min": 1,
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 2
}
],
"title": "Speed vs Temperature (XY)",
"transformations": [
{
"id": "seriesToColumns",
"options": {
"byField": "time"
}
},
{
"id": "organize",
"options": {
"excludeByName": {
"time": true
},
"indexByName": {},
"renameByName": {
"A-series": "Temperature",
"A-series1": "Speed"
}
}
}
],
"type": "xychart"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 7,
"x": 17,
"y": 0
},
"id": 2,
"options": {
"counters": {
"dataChanged": true,
"render": true,
"schemaChanged": true
},
"mode": "cursor"
},
"pluginVersion": "7.5.0-pre",
"timeFrom": null,
"timeShift": null,
"title": "Cursor info",
"type": "debug"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "celsius"
},
"overrides": []
},
"gridPos": {
"h": 6,
"w": 9,
"x": 0,
"y": 8
},
"id": 5,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltipOptions": {
"mode": "single"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"alias": "Temperature",
"max": 25,
"min": 5,
"refId": "A",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Only temperature",
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "velocitykmh"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Speed"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "yellow",
"mode": "fixed"
}
}
]
}
]
},
"gridPos": {
"h": 6,
"w": 8,
"x": 9,
"y": 8
},
"id": 9,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltipOptions": {
"mode": "single"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"alias": "Speed",
"hide": false,
"refId": "B",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Only Speed",
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "accMS2"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Speed"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "yellow",
"mode": "fixed"
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "Acceleration"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "purple",
"mode": "fixed"
}
}
]
}
]
},
"gridPos": {
"h": 6,
"w": 7,
"x": 17,
"y": 8
},
"id": 11,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltipOptions": {
"mode": "single"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"alias": "Acceleration",
"hide": false,
"refId": "B",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"type": "timeseries"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fieldConfig": {
"defaults": {
"unit": "celsius"
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 6,
"w": 12,
"x": 0,
"y": 14
},
"hiddenSeries": false,
"id": 8,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.5.0-pre",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"max": 25,
"min": 1,
"refId": "A",
"scenarioId": "random_walk"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "flot panel (temperature)",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "celsius",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 6,
"w": 12,
"x": 12,
"y": 14
},
"hiddenSeries": false,
"id": 10,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.5.0-pre",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "flot panel (no units)",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
}
],
"refresh": false,
"schemaVersion": 28,
"style": "dark",
"tags": [
"gdev",
"panel-tests",
"graph-ng"
],
"templating": {
"list": []
},
"time": {
"from": "2020-09-14T16:13:20.000Z",
"to": "2020-09-15T20:00:00.000Z"
},
"timepicker": {},
"timezone": "",
"title": "Panel Tests - shared tooltips",
"uid": "TX2VU59MZ",
"version": 2
}

View File

@@ -1,20 +1,23 @@
import { DataFrame } from '../types'; import { DataFrame } from '../types';
import { BusEventWithPayload } from './types'; import { BusEventWithPayload } from './types';
/** @alpha */ /**
* When hovering over an element this will identify
*
* For performance reasons, this object will usually be mutated between updates. This
* will avoid creating new objects for events that fire frequently (ie each mouse pixel)
*
* @alpha
*/
export interface DataHoverPayload { export interface DataHoverPayload {
raw: any; // Original mouse event (includes pageX etc)
x: Record<string, any>; // { time: 5678 },
y: Record<string, any>; // { __fixed: 123, lengthft: 456 } // each axis|scale gets a value
data?: DataFrame; // source data data?: DataFrame; // source data
rowIndex?: number; // the hover row rowIndex?: number; // the hover row
columnIndex?: number; // the hover column columnIndex?: number; // the hover column
dataId?: string; // identifying string to correlate data between publishers and subscribers dataId?: string; // identifying string to correlate data between publishers and subscribers
// When dragging, this will capture the original state // When dragging, this will capture the point when the mouse was down
down?: Omit<DataHoverPayload, 'down'>; point: Record<string, any>; // { time: 5678, lengthft: 456 } // each axis|scale gets a value
down?: Record<string, any>;
} }
/** @alpha */ /** @alpha */

View File

@@ -17,6 +17,7 @@ export interface BusEvent {
export abstract class BusEventBase implements BusEvent { export abstract class BusEventBase implements BusEvent {
readonly type: string; readonly type: string;
readonly payload?: any; readonly payload?: any;
readonly origin?: EventBus;
constructor() { constructor() {
//@ts-ignore //@ts-ignore

View File

@@ -0,0 +1,5 @@
export enum DashboardCursorSync {
Off,
Crosshair,
Tooltip,
}

View File

@@ -1,6 +1,7 @@
export * from './data'; export * from './data';
export * from './dataFrame'; export * from './dataFrame';
export * from './dataLink'; export * from './dataLink';
export * from './dashboard';
export * from './annotations'; export * from './annotations';
export * from './logs'; export * from './logs';
export * from './navModel'; export * from './navModel';

View File

@@ -3,6 +3,7 @@ import { AngularPanelMenuItem } from './panel';
import { DataFrame } from './dataFrame'; import { DataFrame } from './dataFrame';
import { eventFactory } from '../events/eventFactory'; import { eventFactory } from '../events/eventFactory';
import { BusEventBase, BusEventWithPayload } from '../events/types'; import { BusEventBase, BusEventWithPayload } from '../events/types';
import { DataHoverPayload } from '../events';
export type AlertPayload = [string, string?]; export type AlertPayload = [string, string?];
export type AlertErrorPayload = [string, (string | Error)?]; export type AlertErrorPayload = [string, (string | Error)?];
@@ -28,7 +29,7 @@ export const PanelEvents = {
}; };
/** @public */ /** @public */
export interface LegacyGraphHoverEventPayload { export interface LegacyGraphHoverEventPayload extends DataHoverPayload {
pos: any; pos: any;
panel: { panel: {
id: number; id: number;
@@ -43,4 +44,5 @@ export class LegacyGraphHoverEvent extends BusEventWithPayload<LegacyGraphHoverE
/** @alpha */ /** @alpha */
export class LegacyGraphHoverClearEvent extends BusEventBase { export class LegacyGraphHoverClearEvent extends BusEventBase {
static type = 'graph-hover-clear'; static type = 'graph-hover-clear';
payload: DataHoverPayload = { point: {} };
} }

View File

@@ -1,14 +1,27 @@
import React from 'react'; import React from 'react';
import { AlignedData } from 'uplot'; import { AlignedData } from 'uplot';
import { DataFrame, FieldMatcherID, fieldMatchers, FieldType, TimeRange, TimeZone } from '@grafana/data';
import { Themeable2 } from '../../types'; import { Themeable2 } from '../../types';
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; import { findMidPointYPosition, pluginLog, preparePlotData } from '../uPlot/utils';
import { GraphNGLegendEvent, XYFieldMatchers } from './types'; import {
DataFrame,
FieldMatcherID,
fieldMatchers,
FieldType,
LegacyGraphHoverClearEvent,
LegacyGraphHoverEvent,
TimeRange,
TimeZone,
} from '@grafana/data';
import { preparePlotFrame } from './utils'; import { preparePlotFrame } from './utils';
import { preparePlotData } from '../uPlot/utils';
import { UPlotChart } from '../uPlot/Plot';
import { VizLegendOptions } from '../VizLegend/models.gen'; import { VizLegendOptions } from '../VizLegend/models.gen';
import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext';
import { Subscription } from 'rxjs';
import { throttleTime } from 'rxjs/operators';
import { GraphNGLegendEvent, XYFieldMatchers } from './types';
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
import { VizLayout } from '../VizLayout/VizLayout'; import { VizLayout } from '../VizLayout/VizLayout';
import { UPlotChart } from '../uPlot/Plot';
/** /**
* @internal -- not a public API * @internal -- not a public API
@@ -16,10 +29,10 @@ import { VizLayout } from '../VizLayout/VizLayout';
export const FIXED_UNIT = '__fixed'; export const FIXED_UNIT = '__fixed';
export interface GraphNGProps extends Themeable2 { export interface GraphNGProps extends Themeable2 {
width: number;
height: number;
frames: DataFrame[]; frames: DataFrame[];
structureRev?: number; // a number that will change when the frames[] structure changes structureRev?: number; // a number that will change when the frames[] structure changes
width: number;
height: number;
timeRange: TimeRange; timeRange: TimeRange;
timeZone: TimeZone; timeZone: TimeZone;
legend: VizLegendOptions; legend: VizLegendOptions;
@@ -55,9 +68,16 @@ export interface GraphNGState {
* "Time as X" core component, expectes ascending x * "Time as X" core component, expectes ascending x
*/ */
export class GraphNG extends React.Component<GraphNGProps, GraphNGState> { export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
static contextType = PanelContextRoot;
panelContext: PanelContext = {} as PanelContext;
private plotInstance: React.RefObject<uPlot>;
private subscription = new Subscription();
constructor(props: GraphNGProps) { constructor(props: GraphNGProps) {
super(props); super(props);
this.state = this.prepState(props); this.state = this.prepState(props);
this.plotInstance = React.createRef();
} }
getTimeRange = () => this.props.timeRange; getTimeRange = () => this.props.timeRange;
@@ -74,21 +94,76 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
y: fieldMatchers.get(FieldMatcherID.numeric).get({}), y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
} }
); );
pluginLog('GraphNG', false, 'data aligned', alignedFrame);
if (alignedFrame) { if (alignedFrame) {
state = { state = {
alignedFrame, alignedFrame,
alignedData: preparePlotData(alignedFrame, [FieldType.number]), alignedData: preparePlotData(alignedFrame, [FieldType.number]),
}; };
pluginLog('GraphNG', false, 'data prepared', state.alignedData);
if (withConfig) { if (withConfig) {
state.config = props.prepConfig(alignedFrame, this.getTimeRange); state.config = props.prepConfig(alignedFrame, this.getTimeRange);
pluginLog('GraphNG', false, 'config prepared', state.config);
} }
} }
return state; return state;
} }
componentDidMount() {
this.panelContext = this.context as PanelContext;
const { eventBus } = this.panelContext;
this.subscription.add(
eventBus
.getStream(LegacyGraphHoverEvent)
.pipe(throttleTime(50))
.subscribe({
next: (evt) => {
const u = this.plotInstance?.current;
if (u) {
// Try finding left position on time axis
const left = u.valToPos(evt.payload.point.time, 'time');
let top;
if (left) {
// find midpoint between points at current idx
top = findMidPointYPosition(u, u.posToIdx(left));
}
if (!top || !left) {
return;
}
u.setCursor({
left,
top,
});
}
},
})
);
this.subscription.add(
eventBus
.getStream(LegacyGraphHoverClearEvent)
.pipe(throttleTime(50))
.subscribe({
next: () => {
const u = this.plotInstance?.current;
if (u) {
u.setCursor({
left: -10,
top: -10,
});
}
},
})
);
}
componentDidUpdate(prevProps: GraphNGProps) { componentDidUpdate(prevProps: GraphNGProps) {
const { frames, structureRev, timeZone, propsToDiff } = this.props; const { frames, structureRev, timeZone, propsToDiff } = this.props;
@@ -107,6 +182,7 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
if (shouldReconfig) { if (shouldReconfig) {
newState.config = this.props.prepConfig(newState.alignedFrame, this.getTimeRange); newState.config = this.props.prepConfig(newState.alignedFrame, this.getTimeRange);
pluginLog('GraphNG', false, 'config recreated', newState.config);
} }
} }
@@ -114,6 +190,10 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
} }
} }
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() { render() {
const { width, height, children, timeRange, renderLegend } = this.props; const { width, height, children, timeRange, renderLegend } = this.props;
const { config, alignedFrame } = this.state; const { config, alignedFrame } = this.state;
@@ -131,6 +211,7 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
width={vizWidth} width={vizWidth}
height={vizHeight} height={vizHeight}
timeRange={timeRange} timeRange={timeRange}
plotRef={(u) => ((this.plotInstance as React.MutableRefObject<uPlot>).current = u)}
> >
{children ? children(config, alignedFrame) : null} {children ? children(config, alignedFrame) : null}
</UPlotChart> </UPlotChart>

View File

@@ -12,7 +12,7 @@ Object {
"width": 1, "width": 1,
}, },
"labelFont": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", "labelFont": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
"scale": "x", "scale": "time",
"show": true, "show": true,
"side": 2, "side": 2,
"size": [Function], "size": [Function],
@@ -79,6 +79,20 @@ Object {
"stroke": [Function], "stroke": [Function],
"width": [Function], "width": [Function],
}, },
"sync": Object {
"filters": Object {
"pub": [Function],
},
"key": "__global_",
"match": Array [
[Function],
[Function],
],
"scales": Array [
"time",
"__fixed",
],
},
}, },
"hooks": Object {}, "hooks": Object {},
"scales": Object { "scales": Object {
@@ -91,7 +105,7 @@ Object {
"range": [Function], "range": [Function],
"time": undefined, "time": undefined,
}, },
"x": Object { "time": Object {
"auto": false, "auto": false,
"dir": 1, "dir": 1,
"ori": 0, "ori": 0,

View File

@@ -2,7 +2,9 @@ import { preparePlotFrame } from './utils';
import { preparePlotConfigBuilder } from '../TimeSeries/utils'; import { preparePlotConfigBuilder } from '../TimeSeries/utils';
import { import {
createTheme, createTheme,
DashboardCursorSync,
DefaultTimeZone, DefaultTimeZone,
EventBusSrv,
FieldConfig, FieldConfig,
FieldMatcherID, FieldMatcherID,
fieldMatchers, fieldMatchers,
@@ -194,6 +196,8 @@ describe('GraphNG utils', () => {
theme: createTheme(), theme: createTheme(),
timeZone: DefaultTimeZone, timeZone: DefaultTimeZone,
getTimeRange: getDefaultTimeRange, getTimeRange: getDefaultTimeRange,
eventBus: new EventBusSrv(),
sync: DashboardCursorSync.Tooltip,
}).getConfig(); }).getConfig();
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });

View File

@@ -2,6 +2,7 @@ import { XYFieldMatchers } from './types';
import { import {
ArrayVector, ArrayVector,
DataFrame, DataFrame,
EventBus,
FieldType, FieldType,
GrafanaTheme2, GrafanaTheme2,
outerJoinDataFrames, outerJoinDataFrames,
@@ -9,13 +10,14 @@ import {
TimeZone, TimeZone,
} from '@grafana/data'; } from '@grafana/data';
import { nullToUndefThreshold } from './nullToUndefThreshold'; import { nullToUndefThreshold } from './nullToUndefThreshold';
export interface PrepConfigOpts {
export type PrepConfigOpts<T extends Record<string, any> = {}> = {
frame: DataFrame; frame: DataFrame;
theme: GrafanaTheme2; theme: GrafanaTheme2;
timeZone: TimeZone; timeZone: TimeZone;
getTimeRange: () => TimeRange; getTimeRange: () => TimeRange;
[prop: string]: any; eventBus: EventBus;
} } & T;
function applySpanNullsThresholds(frames: DataFrame[]) { function applySpanNullsThresholds(frames: DataFrame[]) {
for (const frame of frames) { for (const frame of frames) {

View File

@@ -1,4 +1,4 @@
import { EventBusSrv, EventBus } from '@grafana/data'; import { EventBusSrv, EventBus, DashboardCursorSync } from '@grafana/data';
import React from 'react'; import React from 'react';
import { SeriesVisibilityChangeMode } from '.'; import { SeriesVisibilityChangeMode } from '.';
@@ -6,6 +6,9 @@ import { SeriesVisibilityChangeMode } from '.';
export interface PanelContext { export interface PanelContext {
eventBus: EventBus; eventBus: EventBus;
/** Dashboard panels sync */
sync?: DashboardCursorSync;
/** /**
* Called when a component wants to change the color for a series * Called when a component wants to change the color for a series
* *
@@ -16,7 +19,7 @@ export interface PanelContext {
onToggleSeriesVisibility?: (label: string, mode: SeriesVisibilityChangeMode) => void; onToggleSeriesVisibility?: (label: string, mode: SeriesVisibilityChangeMode) => void;
} }
const PanelContextRoot = React.createContext<PanelContext>({ export const PanelContextRoot = React.createContext<PanelContext>({
eventBus: new EventBusSrv(), eventBus: new EventBusSrv(),
}); });

View File

@@ -6,18 +6,20 @@ import { PlotLegend } from '../uPlot/PlotLegend';
import { LegendDisplayMode } from '../VizLegend/models.gen'; import { LegendDisplayMode } from '../VizLegend/models.gen';
import { preparePlotConfigBuilder } from './utils'; import { preparePlotConfigBuilder } from './utils';
import { withTheme2 } from '../../themes/ThemeContext'; import { withTheme2 } from '../../themes/ThemeContext';
import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext';
const propsToDiff: string[] = []; const propsToDiff: string[] = [];
type TimeSeriesProps = Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend'>; type TimeSeriesProps = Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend'>;
export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> { export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> {
static contextType = PanelContextRoot;
panelContext: PanelContext = {} as PanelContext;
prepConfig = (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => { prepConfig = (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => {
return preparePlotConfigBuilder({ const { eventBus, sync } = this.context;
frame: alignedFrame, const { theme, timeZone } = this.props;
getTimeRange, return preparePlotConfigBuilder({ frame: alignedFrame, theme, timeZone, getTimeRange, eventBus, sync });
...this.props,
});
}; };
renderLegend = (config: UPlotConfigBuilder) => { renderLegend = (config: UPlotConfigBuilder) => {

View File

@@ -1,6 +1,10 @@
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { import {
DashboardCursorSync,
DataFrame, DataFrame,
DataHoverClearEvent,
DataHoverEvent,
DataHoverPayload,
FieldConfig, FieldConfig,
FieldType, FieldType,
formattedValueToString, formattedValueToString,
@@ -9,7 +13,6 @@ import {
getFieldSeriesColor, getFieldSeriesColor,
} from '@grafana/data'; } from '@grafana/data';
import { PrepConfigOpts } from '../GraphNG/utils';
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
import { FIXED_UNIT } from '../GraphNG/GraphNG'; import { FIXED_UNIT } from '../GraphNG/GraphNG';
import { import {
@@ -22,6 +25,7 @@ import {
ScaleOrientation, ScaleOrientation,
} from '../uPlot/config'; } from '../uPlot/config';
import { collectStackingGroups } from '../uPlot/utils'; import { collectStackingGroups } from '../uPlot/utils';
import { PrepConfigOpts } from '../GraphNG/utils';
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1)); const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
@@ -31,9 +35,9 @@ const defaultConfig: GraphFieldConfig = {
axisPlacement: AxisPlacement.Auto, axisPlacement: AxisPlacement.Auto,
}; };
type PrepConfig = (opts: PrepConfigOpts) => UPlotConfigBuilder; type PrepConfig = (opts: PrepConfigOpts<{ sync: DashboardCursorSync }>) => UPlotConfigBuilder;
export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, getTimeRange }) => { export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, getTimeRange, eventBus, sync }) => {
const builder = new UPlotConfigBuilder(timeZone); const builder = new UPlotConfigBuilder(timeZone);
// X is the first field in the aligned frame // X is the first field in the aligned frame
@@ -44,20 +48,24 @@ export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, g
let seriesIndex = 0; let seriesIndex = 0;
let xScaleKey = '_x';
let yScaleKey = '';
if (xField.type === FieldType.time) { if (xField.type === FieldType.time) {
xScaleKey = 'time';
builder.addScale({ builder.addScale({
scaleKey: 'x', scaleKey: xScaleKey,
orientation: ScaleOrientation.Horizontal, orientation: ScaleOrientation.Horizontal,
direction: ScaleDirection.Right, direction: ScaleDirection.Right,
isTime: true, isTime: true,
range: () => { range: () => {
const timeRange = getTimeRange(); const r = getTimeRange();
return [timeRange.from.valueOf(), timeRange.to.valueOf()]; return [r.from.valueOf(), r.to.valueOf()];
}, },
}); });
builder.addAxis({ builder.addAxis({
scaleKey: 'x', scaleKey: xScaleKey,
isTime: true, isTime: true,
placement: AxisPlacement.Bottom, placement: AxisPlacement.Bottom,
timeZone, timeZone,
@@ -65,14 +73,18 @@ export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, g
}); });
} else { } else {
// Not time! // Not time!
if (xField.config.unit) {
xScaleKey = xField.config.unit;
}
builder.addScale({ builder.addScale({
scaleKey: 'x', scaleKey: xScaleKey,
orientation: ScaleOrientation.Horizontal, orientation: ScaleOrientation.Horizontal,
direction: ScaleDirection.Right, direction: ScaleDirection.Right,
}); });
builder.addAxis({ builder.addAxis({
scaleKey: 'x', scaleKey: xScaleKey,
placement: AxisPlacement.Bottom, placement: AxisPlacement.Bottom,
theme, theme,
}); });
@@ -82,7 +94,7 @@ export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, g
let indexByName: Map<string, number> | undefined = undefined; let indexByName: Map<string, number> | undefined = undefined;
for (let i = 0; i < frame.fields.length; i++) { for (let i = 1; i < frame.fields.length; i++) {
const field = frame.fields[i]; const field = frame.fields[i];
const config = field.config as FieldConfig<GraphFieldConfig>; const config = field.config as FieldConfig<GraphFieldConfig>;
const customConfig: GraphFieldConfig = { const customConfig: GraphFieldConfig = {
@@ -114,6 +126,10 @@ export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, g
softMax: customConfig.axisSoftMax, softMax: customConfig.axisSoftMax,
}); });
if (!yScaleKey) {
yScaleKey = scaleKey;
}
if (customConfig.axisPlacement !== AxisPlacement.Hidden) { if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
builder.addAxis({ builder.addAxis({
scaleKey, scaleKey,
@@ -183,7 +199,6 @@ export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, g
}); });
} }
} }
collectStackingGroups(field, stackingGroups, seriesIndex); collectStackingGroups(field, stackingGroups, seriesIndex);
} }
@@ -197,6 +212,45 @@ export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, g
} }
} }
} }
builder.scaleKeys = [xScaleKey, yScaleKey];
if (sync !== DashboardCursorSync.Off) {
const payload: DataHoverPayload = {
point: {
[xScaleKey]: null,
[yScaleKey]: null,
},
data: frame,
};
const hoverEvent = new DataHoverEvent(payload);
builder.setCursor({
sync: {
key: '__global_',
filters: {
pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => {
payload.columnIndex = dataIdx;
if (x < 0 && y < 0) {
payload.point[xScaleKey] = null;
payload.point[yScaleKey] = null;
eventBus.publish(new DataHoverClearEvent(payload));
} else {
// convert the points
payload.point[xScaleKey] = src.posToVal(x, xScaleKey);
payload.point[yScaleKey] = src.posToVal(y, yScaleKey);
eventBus.publish(hoverEvent);
hoverEvent.payload.down = undefined;
}
return true;
},
},
// ??? setSeries: syncMode === DashboardCursorSync.Tooltip,
scales: builder.scaleKeys,
match: [() => true, () => true],
},
});
}
return builder; return builder;
}; };

View File

@@ -5,6 +5,7 @@ import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG';
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
import { preparePlotConfigBuilder } from './utils'; import { preparePlotConfigBuilder } from './utils';
import { BarValueVisibility, TimelineMode } from './types'; import { BarValueVisibility, TimelineMode } from './types';
import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext';
/** /**
* @alpha * @alpha
@@ -19,10 +20,17 @@ export interface TimelineProps extends Omit<GraphNGProps, 'prepConfig' | 'propsT
const propsToDiff = ['mode', 'rowHeight', 'colWidth', 'showValue']; const propsToDiff = ['mode', 'rowHeight', 'colWidth', 'showValue'];
export class UnthemedTimelineChart extends React.Component<TimelineProps> { export class UnthemedTimelineChart extends React.Component<TimelineProps> {
static contextType = PanelContextRoot;
panelContext: PanelContext = {} as PanelContext;
prepConfig = (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => { prepConfig = (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => {
this.panelContext = this.context as PanelContext;
const { eventBus } = this.panelContext;
return preparePlotConfigBuilder({ return preparePlotConfigBuilder({
frame: alignedFrame, frame: alignedFrame,
getTimeRange, getTimeRange,
eventBus,
...this.props, ...this.props,
}); });
}; };

View File

@@ -42,14 +42,14 @@ export function preparePlotFrame(data: DataFrame[], dimFields: XYFieldMatchers)
}); });
} }
interface PrepConfigOptsTimeline extends PrepConfigOpts { type PrepConfig = (
mode: TimelineMode; opts: PrepConfigOpts<{
rowHeight: number; mode: TimelineMode;
colWidth?: number; rowHeight: number;
showValue: BarValueVisibility; colWidth?: number;
} showValue: BarValueVisibility;
}>
type PrepConfig = (opts: PrepConfigOptsTimeline) => UPlotConfigBuilder; ) => UPlotConfigBuilder;
export const preparePlotConfigBuilder: PrepConfig = ({ export const preparePlotConfigBuilder: PrepConfig = ({
frame, frame,

View File

@@ -94,6 +94,7 @@ export const VizTooltipContainer: React.FC<VizTooltipContainerProps> = ({
left: 0, left: 0,
top: 0, top: 0,
transform: `translate3d(${placement.x}px, ${placement.y}px, 0)`, transform: `translate3d(${placement.x}px, ${placement.y}px, 0)`,
transition: 'all ease-out 0.1s',
}} }}
{...otherProps} {...otherProps}
className={cx(styles.wrapper, className)} className={cx(styles.wrapper, className)}

View File

@@ -1,4 +1,4 @@
import React, { createRef } from 'react'; import React, { createRef, MutableRefObject } from 'react';
import uPlot, { Options } from 'uplot'; import uPlot, { Options } from 'uplot';
import { PlotContext, PlotContextType } from './context'; import { PlotContext, PlotContextType } from './context';
import { DEFAULT_PLOT_CONFIG } from './utils'; import { DEFAULT_PLOT_CONFIG } from './utils';
@@ -28,6 +28,7 @@ type UPlotChartState = {
*/ */
export class UPlotChart extends React.Component<PlotProps, UPlotChartState> { export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
plotContainer = createRef<HTMLDivElement>(); plotContainer = createRef<HTMLDivElement>();
plotCanvasBBox = createRef<DOMRect>();
constructor(props: PlotProps) { constructor(props: PlotProps) {
super(props); super(props);
@@ -35,20 +36,27 @@ export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
this.state = { this.state = {
ctx: { ctx: {
plot: null, plot: null,
getCanvasBoundingBox: () => this.plotCanvasBBox.current,
}, },
}; };
} }
reinitPlot() { reinitPlot() {
let { ctx } = this.state; let { ctx } = this.state;
let { width, height, plotRef } = this.props;
ctx.plot?.destroy(); ctx.plot?.destroy();
let { width, height } = this.props;
if (width === 0 && height === 0) { if (width === 0 && height === 0) {
return; return;
} }
this.props.config.addHook('setSize', (u) => {
const canvas = u.root.querySelector<HTMLDivElement>('.u-over');
if (!canvas) {
return;
}
(this.plotCanvasBBox as MutableRefObject<any>).current = canvas.getBoundingClientRect();
});
const config: Options = { const config: Options = {
...DEFAULT_PLOT_CONFIG, ...DEFAULT_PLOT_CONFIG,
@@ -58,11 +66,19 @@ export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
...this.props.config.getConfig(), ...this.props.config.getConfig(),
}; };
this.setState({ const plot = new uPlot(config, this.props.data, this.plotContainer!.current!);
if (plotRef) {
plotRef(plot);
}
this.setState((s) => ({
...s,
ctx: { ctx: {
plot: new uPlot(config, this.props.data, this.plotContainer!.current!), ...s.ctx,
plot,
}, },
}); }));
} }
componentDidMount() { componentDidMount() {
@@ -82,7 +98,7 @@ export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
); );
} }
componentDidUpdate(prevProps: PlotProps, prevState: object) { componentDidUpdate(prevProps: PlotProps) {
let { ctx } = this.state; let { ctx } = this.state;
if (!sameDims(prevProps, this.props)) { if (!sameDims(prevProps, this.props)) {

View File

@@ -28,6 +28,9 @@ export class UPlotConfigBuilder {
this.tz = getTimeZoneInfo(timeZone, Date.now())?.ianaName; this.tz = getTimeZoneInfo(timeZone, Date.now())?.ianaName;
} }
// Exposed to let the container know the primary scale keys
scaleKeys: [string, string] = ['', ''];
addHook<T extends keyof Hooks.Defs>(type: T, hook: Hooks.Defs[T]) { addHook<T extends keyof Hooks.Defs>(type: T, hook: Hooks.Defs[T]) {
pluginLog('UPlotConfigBuilder', false, 'addHook', type); pluginLog('UPlotConfigBuilder', false, 'addHook', type);
@@ -81,7 +84,7 @@ export class UPlotConfigBuilder {
} }
setCursor(cursor?: Cursor) { setCursor(cursor?: Cursor) {
this.cursor = cursor; this.cursor = { ...this.cursor, ...cursor };
} }
setSelect(select: Select) { setSelect(select: Select) {

View File

@@ -1,8 +1,8 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import uPlot from 'uplot'; import uPlot from 'uplot';
export interface PlotContextType { export interface PlotContextType {
plot: uPlot | null; plot: uPlot | null;
getCanvasBoundingBox: () => DOMRect | null;
} }
/** /**

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Portal } from '../../Portal/Portal'; import { Portal } from '../../Portal/Portal';
import { usePlotContext } from '../context'; import { usePlotContext } from '../context';
import { import {
@@ -12,8 +12,9 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import { SeriesTable, SeriesTableRowProps, TooltipDisplayMode, VizTooltipContainer } from '../../VizTooltip'; import { SeriesTable, SeriesTableRowProps, TooltipDisplayMode, VizTooltipContainer } from '../../VizTooltip';
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder'; import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
import { pluginLog } from '../utils'; import { findMidPointYPosition, pluginLog } from '../utils';
import { useTheme2 } from '../../../themes/ThemeContext'; import { useTheme2 } from '../../../themes/ThemeContext';
import uPlot from 'uplot';
interface TooltipPluginProps { interface TooltipPluginProps {
mode?: TooltipDisplayMode; mode?: TooltipDisplayMode;
@@ -22,6 +23,8 @@ interface TooltipPluginProps {
config: UPlotConfigBuilder; config: UPlotConfigBuilder;
} }
const TOOLTIP_OFFSET = 10;
/** /**
* @alpha * @alpha
*/ */
@@ -33,46 +36,41 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
}) => { }) => {
const theme = useTheme2(); const theme = useTheme2();
const plotCtx = usePlotContext(); const plotCtx = usePlotContext();
const plotCanvas = useRef<HTMLDivElement>();
const plotCanvasBBox = useRef<any>({ left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 });
const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null); const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null);
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null); const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);
const [coords, setCoords] = useState<{ viewport: CartesianCoords2D; plotCanvas: CartesianCoords2D } | null>(null);
const [coords, setCoords] = useState<CartesianCoords2D | null>(null);
const pluginId = `TooltipPlugin`;
// Debug logs // Debug logs
useEffect(() => { useEffect(() => {
pluginLog('TooltipPlugin', true, `Focused series: ${focusedSeriesIdx}, focused point: ${focusedPointIdx}`); pluginLog(pluginId, true, `Focused series: ${focusedSeriesIdx}, focused point: ${focusedPointIdx}`);
}, [focusedPointIdx, focusedSeriesIdx]); }, [focusedPointIdx, focusedSeriesIdx]);
// Add uPlot hooks to the config, or re-add when the config changed // Add uPlot hooks to the config, or re-add when the config changed
useLayoutEffect(() => { useLayoutEffect(() => {
const onMouseCapture = (e: MouseEvent) => {
setCoords({
plotCanvas: {
x: e.clientX - plotCanvasBBox.current.left,
y: e.clientY - plotCanvasBBox.current.top,
},
viewport: {
x: e.clientX,
y: e.clientY,
},
});
};
config.addHook('init', (u) => {
const canvas = u.root.querySelector<HTMLDivElement>('.u-over');
plotCanvas.current = canvas || undefined;
plotCanvas.current?.addEventListener('mousemove', onMouseCapture);
plotCanvas.current?.addEventListener('mouseleave', () => {});
});
config.addHook('setCursor', (u) => { config.addHook('setCursor', (u) => {
setFocusedPointIdx(u.cursor.idx === undefined ? null : u.cursor.idx); const bbox = plotCtx.getCanvasBoundingBox();
if (!bbox) {
return;
}
const { x, y } = positionTooltip(u, bbox);
if (x !== undefined && y !== undefined) {
setCoords({ x, y });
} else {
setCoords(null);
}
setFocusedPointIdx(u.cursor.idx === undefined ? u.posToIdx(u.cursor.left || 0) : u.cursor.idx);
}); });
config.addHook('setSeries', (_, idx) => { config.addHook('setSeries', (_, idx) => {
setFocusedSeriesIdx(idx); setFocusedSeriesIdx(idx);
}); });
}, [config]); }, [plotCtx, config]);
const plotInstance = plotCtx.plot; const plotInstance = plotCtx.plot;
if (!plotInstance || focusedPointIdx === null) { if (!plotInstance || focusedPointIdx === null) {
@@ -142,15 +140,52 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
tooltip = <SeriesTable series={series} timestamp={xVal} />; tooltip = <SeriesTable series={series} timestamp={xVal} />;
} }
if (!tooltip || !coords) {
return null;
}
return ( return (
<Portal> <Portal>
<VizTooltipContainer position={{ x: coords.viewport.x, y: coords.viewport.y }} offset={{ x: 10, y: 10 }}> {tooltip && coords && (
{tooltip} <VizTooltipContainer position={{ x: coords.x, y: coords.y }} offset={{ x: TOOLTIP_OFFSET, y: TOOLTIP_OFFSET }}>
</VizTooltipContainer> {tooltip}
</VizTooltipContainer>
)}
</Portal> </Portal>
); );
}; };
function isCursourOutsideCanvas({ left, top }: uPlot.Cursor, canvas: DOMRect) {
if (left === undefined || top === undefined) {
return false;
}
return left < 0 || left > canvas.width || top < 0 || top > canvas.height;
}
/**
* Given uPlot cursor position, figure out position of the tooltip withing the canvas bbox
* Tooltip is positioned relatively to a viewport
* @internal
**/
export function positionTooltip(u: uPlot, bbox: DOMRect) {
let x, y;
const cL = u.cursor.left || 0;
const cT = u.cursor.top || 0;
if (isCursourOutsideCanvas(u.cursor, bbox)) {
const idx = u.posToIdx(cL);
// when cursor outside of uPlot's canvas
if (cT < 0 || cT > bbox.height) {
let pos = findMidPointYPosition(u, idx);
if (pos) {
y = bbox.top + pos;
if (cL >= 0 && cL <= bbox.width) {
// find x-scale position for a current cursor left position
x = bbox.left + u.valToPos(u.data[0][u.posToIdx(cL)], u.series[0].scale!);
}
}
}
} else {
x = bbox.left + cL;
y = bbox.top + cT;
}
return { x, y };
}

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Options, AlignedData } from 'uplot'; import uPlot, { Options, AlignedData } from 'uplot';
import { TimeRange } from '@grafana/data'; import { TimeRange } from '@grafana/data';
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder'; import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
@@ -19,6 +19,8 @@ export interface PlotProps {
config: UPlotConfigBuilder; config: UPlotConfigBuilder;
timeRange: TimeRange; timeRange: TimeRange;
children?: React.ReactNode; children?: React.ReactNode;
// Reference to uPlot instance
plotRef?: (u: uPlot) => void;
} }
export abstract class PlotConfigBuilder<P, T> { export abstract class PlotConfigBuilder<P, T> {

View File

@@ -106,6 +106,57 @@ export function collectStackingGroups(f: Field, groups: Map<string, number[]>, s
} }
} }
/**
* Finds y axis midpoind for point at given idx (css pixels relative to uPlot canvas)
* @internal
**/
export function findMidPointYPosition(u: uPlot, idx: number) {
let y;
let sMaxIdx = 1;
let sMinIdx = 1;
// assume min/max being values of 1st series
let max = u.data[1][idx];
let min = u.data[1][idx];
// find min max values AND ids of the corresponding series to get the scales
for (let i = 1; i < u.data.length; i++) {
const sData = u.data[i];
const sVal = sData[idx];
if (sVal !== null) {
if (max === null) {
max = sVal;
} else {
if (sVal > max) {
max = u.data[i][idx];
sMaxIdx = i;
}
}
if (min === null) {
min = sVal;
} else {
if (sVal < min) {
min = u.data[i][idx];
sMinIdx = i;
}
}
}
}
if (min === null && max === null) {
// no tooltip to show
y = undefined;
} else if (min !== null && max !== null) {
// find median position
y = (u.valToPos(min, u.series[sMinIdx].scale!) + u.valToPos(max, u.series[sMaxIdx].scale!)) / 2;
} else {
// snap tooltip to min OR max point, one of thos is not null :)
y = u.valToPos((min || max)!, u.series[(sMaxIdx || sMinIdx)!].scale!);
}
return y;
}
// Dev helpers // Dev helpers
/** @internal */ /** @internal */

View File

@@ -14,6 +14,7 @@ export interface Logger {
logger: (...t: any[]) => void; logger: (...t: any[]) => void;
enable: () => void; enable: () => void;
disable: () => void; disable: () => void;
isEnabled: () => boolean;
} }
/** @internal */ /** @internal */
@@ -29,5 +30,6 @@ export const createLogger = (name: string): Logger => {
}, },
enable: () => (LOGGIN_ENABLED = true), enable: () => (LOGGIN_ENABLED = true),
disable: () => (LOGGIN_ENABLED = false), disable: () => (LOGGIN_ENABLED = false),
isEnabled: () => LOGGIN_ENABLED,
}; };
}; };

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { SelectableValue, TimeZone } from '@grafana/data'; import { TimeZone } from '@grafana/data';
import { Select, TagsInput, Input, Field, CollapsableSection, RadioButtonGroup } from '@grafana/ui'; import { TagsInput, Input, Field, CollapsableSection, RadioButtonGroup } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { DashboardModel } from '../../state/DashboardModel'; import { DashboardModel } from '../../state/DashboardModel';
@@ -30,8 +30,8 @@ export const GeneralSettings: React.FC<Props> = ({ dashboard }) => {
dashboard[event.currentTarget.name as 'title' | 'description'] = event.currentTarget.value; dashboard[event.currentTarget.name as 'title' | 'description'] = event.currentTarget.value;
}; };
const onTooltipChange = (graphTooltip: SelectableValue<number>) => { const onTooltipChange = (graphTooltip: number) => {
dashboard.graphTooltip = graphTooltip.value; dashboard.graphTooltip = graphTooltip;
setRenderCounter(renderCounter + 1); setRenderCounter(renderCounter + 1);
}; };
@@ -117,12 +117,7 @@ export const GeneralSettings: React.FC<Props> = ({ dashboard }) => {
label="Graph tooltip" label="Graph tooltip"
description="Controls tooltip and hover highlight behavior across different panels" description="Controls tooltip and hover highlight behavior across different panels"
> >
<Select <RadioButtonGroup onChange={onTooltipChange} options={GRAPH_TOOLTIP_OPTIONS} value={dashboard.graphTooltip} />
onChange={onTooltipChange}
options={GRAPH_TOOLTIP_OPTIONS}
width={40}
value={dashboard.graphTooltip}
/>
</Field> </Field>
</CollapsableSection> </CollapsableSection>

View File

@@ -15,6 +15,7 @@ import { DashboardModel, PanelModel } from '../state';
import { PANEL_BORDER } from 'app/core/constants'; import { PANEL_BORDER } from 'app/core/constants';
import { import {
AbsoluteTimeRange, AbsoluteTimeRange,
DashboardCursorSync,
EventBusSrv, EventBusSrv,
EventFilterOptions, EventFilterOptions,
FieldConfigSource, FieldConfigSource,
@@ -76,6 +77,7 @@ export class PanelChrome extends Component<Props, State> {
renderCounter: 0, renderCounter: 0,
refreshWhenInView: false, refreshWhenInView: false,
context: { context: {
sync: props.isEditing ? DashboardCursorSync.Off : props.dashboard.graphTooltip,
eventBus, eventBus,
onSeriesColorChange: this.onSeriesColorChange, onSeriesColorChange: this.onSeriesColorChange,
onToggleSeriesVisibility: this.onSeriesVisibilityChange, onToggleSeriesVisibility: this.onSeriesVisibilityChange,
@@ -139,7 +141,23 @@ export class PanelChrome extends Component<Props, State> {
} }
componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props) {
const { isInView } = this.props; const { isInView, isEditing } = this.props;
if (prevProps.dashboard.graphTooltip !== this.props.dashboard.graphTooltip) {
this.setState((s) => {
return {
context: { ...s.context, sync: isEditing ? DashboardCursorSync.Off : this.props.dashboard.graphTooltip },
};
});
}
if (isEditing !== prevProps.isEditing) {
this.setState((s) => {
return {
context: { ...s.context, sync: isEditing ? DashboardCursorSync.Off : this.props.dashboard.graphTooltip },
};
});
}
// View state has changed // View state has changed
if (isInView !== prevProps.isInView) { if (isInView !== prevProps.isInView) {

View File

@@ -25,6 +25,7 @@ import { DashboardMigrator } from './DashboardMigrator';
import { import {
AnnotationQuery, AnnotationQuery,
AppEvent, AppEvent,
DashboardCursorSync,
dateTimeFormat, dateTimeFormat,
dateTimeFormatTimeAgo, dateTimeFormatTimeAgo,
DateTimeInput, DateTimeInput,
@@ -74,7 +75,7 @@ export class DashboardModel {
style: any; style: any;
timezone: any; timezone: any;
editable: any; editable: any;
graphTooltip: any; graphTooltip: DashboardCursorSync;
time: any; time: any;
private originalTime: any; private originalTime: any;
timepicker: any; timepicker: any;

View File

@@ -0,0 +1,69 @@
import {
EventBus,
LegacyGraphHoverEvent,
LegacyGraphHoverClearEvent,
DataHoverEvent,
DataHoverClearEvent,
DataHoverPayload,
BusEventWithPayload,
} from '@grafana/data';
import React, { Component } from 'react';
import { Subscription } from 'rxjs';
interface Props {
eventBus: EventBus;
}
interface State {
event?: BusEventWithPayload<DataHoverPayload>;
}
export class CursorView extends Component<Props, State> {
subscription = new Subscription();
state: State = {};
componentDidMount() {
const { eventBus } = this.props;
this.subscription.add(
eventBus.subscribe(DataHoverEvent, (event) => {
this.setState({ event });
})
);
this.subscription.add(
eventBus.subscribe(DataHoverClearEvent, (event) => {
this.setState({ event });
})
);
this.subscription.add(
eventBus.subscribe(LegacyGraphHoverEvent, (event) => {
this.setState({ event });
})
);
this.subscription.add(
eventBus.subscribe(LegacyGraphHoverClearEvent, (event) => {
this.setState({ event });
})
);
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
const { event } = this.state;
if (!event) {
return <div>no events yet</div>;
}
return (
<div>
<h2>Origin: {(event.origin as any)?.path}</h2>
<span>Type: {event.type}</span>
<pre>{JSON.stringify(event.payload.point, null, ' ')}</pre>
</div>
);
}
}

View File

@@ -1,50 +1,22 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { PanelProps } from '@grafana/data'; import { PanelProps } from '@grafana/data';
import { DebugPanelOptions, DebugMode, UpdateCounters } from './types'; import { DebugPanelOptions, DebugMode } from './types';
import { EventBusLoggerPanel } from './EventBusLogger'; import { EventBusLoggerPanel } from './EventBusLogger';
import { RenderInfoViewer } from './RenderInfoViewer'; import { RenderInfoViewer } from './RenderInfoViewer';
import { CursorView } from './CursorView';
type Props = PanelProps<DebugPanelOptions>; type Props = PanelProps<DebugPanelOptions>;
export class DebugPanel extends Component<Props> { export class DebugPanel extends Component<Props> {
// Intentionally not state to avoid overhead -- yes, things will be 1 tick behind
lastRender = Date.now();
counters: UpdateCounters = {
render: 0,
dataChanged: 0,
schemaChanged: 0,
};
shouldComponentUpdate(prevProps: Props) {
const { data, options } = this.props;
if (prevProps.data !== data) {
this.counters.dataChanged++;
if (options.counters?.schemaChanged) {
if (data.structureRev !== prevProps.data.structureRev) {
this.counters.schemaChanged++;
}
}
}
return true; // always render?
}
resetCounters = () => {
this.counters = {
render: 0,
dataChanged: 0,
schemaChanged: 0,
};
this.setState(this.state); // force update
};
render() { render() {
const { options } = this.props; const { options } = this.props;
if (options.mode === DebugMode.Events) { if (options.mode === DebugMode.Events) {
return <EventBusLoggerPanel eventBus={this.props.eventBus} />; return <EventBusLoggerPanel eventBus={this.props.eventBus} />;
} }
if (options.mode === DebugMode.Cursor) {
return <CursorView eventBus={this.props.eventBus} />;
}
return <RenderInfoViewer {...this.props} />; return <RenderInfoViewer {...this.props} />;
} }

View File

@@ -12,6 +12,7 @@ export const plugin = new PanelPlugin<DebugPanelOptions>(DebugPanel).useFieldCon
options: [ options: [
{ label: 'Render', value: DebugMode.Render }, { label: 'Render', value: DebugMode.Render },
{ label: 'Events', value: DebugMode.Events }, { label: 'Events', value: DebugMode.Events },
{ label: 'Cursor', value: DebugMode.Cursor },
], ],
}, },
}) })

View File

@@ -11,6 +11,7 @@ export type UpdateCounters = {
export enum DebugMode { export enum DebugMode {
Render = 'render', Render = 'render',
Events = 'events', Events = 'events',
Cursor = 'cursor',
} }
export interface DebugPanelOptions { export interface DebugPanelOptions {

View File

@@ -7,6 +7,7 @@ export default function GraphTooltip(this: any, elem: any, dashboard: any, scope
const self = this; const self = this;
const ctrl = scope.ctrl; const ctrl = scope.ctrl;
const panel = ctrl.panel; const panel = ctrl.panel;
const hoverEvent = new LegacyGraphHoverEvent({ pos: {}, point: {}, panel: this.panel });
const $tooltip = $('<div class="graph-tooltip">'); const $tooltip = $('<div class="graph-tooltip">');
@@ -159,7 +160,10 @@ export default function GraphTooltip(this: any, elem: any, dashboard: any, scope
// broadcast to other graph panels that we are hovering! // broadcast to other graph panels that we are hovering!
pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height(); pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height();
dashboard.events.publish(new LegacyGraphHoverEvent({ pos: pos, panel: panel })); hoverEvent.payload.pos = pos;
hoverEvent.payload.panel = panel;
hoverEvent.payload.point['time'] = (pos as any).x;
dashboard.events.publish(hoverEvent);
}); });
elem.bind('plotclick', (event: any, pos: any, item: any) => { elem.bind('plotclick', (event: any, pos: any, item: any) => {

View File

@@ -62,6 +62,8 @@ export class HeatmapRenderer {
margin: any; margin: any;
dataRangeWidingFactor: number; dataRangeWidingFactor: number;
hoverEvent: LegacyGraphHoverEvent;
constructor(private scope: any, private elem: any, attrs: any, private ctrl: any) { constructor(private scope: any, private elem: any, attrs: any, private ctrl: any) {
// $heatmap is JQuery object, but heatmap is D3 // $heatmap is JQuery object, but heatmap is D3
this.$heatmap = this.elem.find('.heatmap-panel'); this.$heatmap = this.elem.find('.heatmap-panel');
@@ -91,6 +93,8 @@ export class HeatmapRenderer {
this.$heatmap.on('mousedown', this.onMouseDown.bind(this)); this.$heatmap.on('mousedown', this.onMouseDown.bind(this));
this.$heatmap.on('mousemove', this.onMouseMove.bind(this)); this.$heatmap.on('mousemove', this.onMouseMove.bind(this));
this.$heatmap.on('mouseleave', this.onMouseLeave.bind(this)); this.$heatmap.on('mouseleave', this.onMouseLeave.bind(this));
this.hoverEvent = new LegacyGraphHoverEvent({ pos: {}, point: {}, panel: this.panel });
} }
onGraphHoverClear() { onGraphHoverClear() {
@@ -740,7 +744,10 @@ export class HeatmapRenderer {
// Set minimum offset to prevent showing legend from another panel // Set minimum offset to prevent showing legend from another panel
pos.panelRelY = Math.max(pos.offset.y / this.height, 0.001); pos.panelRelY = Math.max(pos.offset.y / this.height, 0.001);
// broadcast to other graph panels that we are hovering // broadcast to other graph panels that we are hovering
this.ctrl.dashboard.events.publish(new LegacyGraphHoverEvent({ pos: pos, panel: this.panel })); this.hoverEvent.payload.pos = pos;
this.hoverEvent.payload.panel = this.panel;
this.hoverEvent.payload.point['time'] = (pos as any).x;
this.ctrl.dashboard.events.publish(this.hoverEvent);
} }
limitSelection(x2: number) { limitSelection(x2: number) {

View File

@@ -1,5 +1,5 @@
import { Field, PanelProps } from '@grafana/data'; import { DashboardCursorSync, Field, PanelProps } from '@grafana/data';
import { TimeSeries, TooltipPlugin, ZoomPlugin } from '@grafana/ui'; import { TooltipDisplayMode, usePanelContext, TimeSeries, TooltipPlugin, ZoomPlugin } from '@grafana/ui';
import { getFieldLinksForExplore } from 'app/features/explore/utils/links'; import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
import React from 'react'; import React from 'react';
import { AnnotationsPlugin } from './plugins/AnnotationsPlugin'; import { AnnotationsPlugin } from './plugins/AnnotationsPlugin';
@@ -11,6 +11,7 @@ interface TimeSeriesPanelProps extends PanelProps<Options> {}
export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
data, data,
id,
timeRange, timeRange,
timeZone, timeZone,
width, width,
@@ -23,6 +24,8 @@ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
return getFieldLinksForExplore({ field, rowIndex, range: timeRange }); return getFieldLinksForExplore({ field, rowIndex, range: timeRange });
}; };
const { sync } = usePanelContext();
if (!data || !data.series?.length) { if (!data || !data.series?.length) {
return ( return (
<div className="panel-empty"> <div className="panel-empty">
@@ -48,7 +51,7 @@ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
<TooltipPlugin <TooltipPlugin
data={alignedDataFrame} data={alignedDataFrame}
config={config} config={config}
mode={options.tooltipOptions.mode} mode={sync === DashboardCursorSync.Tooltip ? TooltipDisplayMode.Multi : options.tooltipOptions.mode}
timeZone={timeZone} timeZone={timeZone}
/> />
<ContextMenuPlugin <ContextMenuPlugin