Geomap: Sync v9.1.x with main (#55428)
* sync geomap * Geomap: Add devenv dashboard for v9.1 of geomap (#53638) * Geomap: Spatial operations location options transform e2e test (#54162) * Devenv: Geomap global vs local field coloring (#54253) * Geomap: cleanup (#54328) * Geomap: Layer types e2e (#54666) * Geomap: Map control options e2e (#54809) * update betterer * add missing icon for measure tools * Geomap: Update language to match documentation and remove beta (#53703) Co-authored-by: nmarrs <nathanielmarrs@gmail.com> Co-authored-by: Ryan McKinley <ryantxu@gmail.com> Co-authored-by: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Co-authored-by: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com>
This commit is contained in:
co-authored by
Ryan McKinley
Adela Almasan
Drew Slobodnjak
parent
89410ffd0c
commit
90364a9173
+21
-79
@@ -2792,11 +2792,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/core/components/Layers/LayerDragDropList.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/core/components/Layers/LayerName.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
@@ -8674,40 +8670,17 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/GeomapPanel.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "6"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "7"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "9"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "10"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "11"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "14"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "17"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/components/DataHoverRows.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/components/DataHoverView.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/components/MarkersLegend.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "5"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "6"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/components/MeasureVectorLayer.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/editor/FrameSelectionEditor.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
@@ -8715,59 +8688,26 @@ exports[`better eslint`] = {
|
||||
],
|
||||
"public/app/plugins/panel/geomap/editor/GeomapStyleRulesEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/editor/LayersEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/editor/MapViewEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/editor/StyleEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "6"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "7"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "8"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "9"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "10"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "11"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "13"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "15"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "17"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "19"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "20"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "21"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "22"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "23"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "24"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "25"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "26"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "12"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/editor/layerEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/layers/basemaps/carto.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
@@ -8790,18 +8730,20 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/module.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/style/utils.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/types.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
"public/app/plugins/panel/geomap/utils/layers.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/utils/selection.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/utils/tootltip.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/geomap/view.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 17,
|
||||
"w": 9,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"basemap": {
|
||||
"config": {
|
||||
"server": "ocean"
|
||||
},
|
||||
"name": "Layer 0",
|
||||
"type": "esri-xyz"
|
||||
},
|
||||
"controls": {
|
||||
"mouseWheelZoom": true,
|
||||
"showAttribution": true,
|
||||
"showDebug": false,
|
||||
"showMeasure": false,
|
||||
"showScale": false,
|
||||
"showZoom": true
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"config": {
|
||||
"server": "usa-topo"
|
||||
},
|
||||
"name": "Topographic map",
|
||||
"opacity": 0.8,
|
||||
"tooltip": true,
|
||||
"type": "esri-xyz"
|
||||
}
|
||||
],
|
||||
"tooltip": {
|
||||
"mode": "details"
|
||||
},
|
||||
"view": {
|
||||
"id": "coords",
|
||||
"lat": 26.076593,
|
||||
"lon": 113.075128,
|
||||
"zoom": 3.76
|
||||
}
|
||||
},
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvFileName": "flight_info_by_state.csv",
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_file"
|
||||
},
|
||||
{
|
||||
"csvFileName": "population_by_state.csv",
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "B",
|
||||
"scenarioId": "csv_file"
|
||||
}
|
||||
],
|
||||
"title": "Multiple map layers",
|
||||
"type": "geomap"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 17,
|
||||
"w": 15,
|
||||
"x": 9,
|
||||
"y": 0
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"basemap": {
|
||||
"config": {
|
||||
"server": "world-imagery"
|
||||
},
|
||||
"name": "Layer 0",
|
||||
"type": "esri-xyz"
|
||||
},
|
||||
"controls": {
|
||||
"mouseWheelZoom": true,
|
||||
"showAttribution": true,
|
||||
"showDebug": false,
|
||||
"showMeasure": false,
|
||||
"showScale": false,
|
||||
"showZoom": true
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"config": {
|
||||
"nightColor": "#000000",
|
||||
"show": "to",
|
||||
"sun": true
|
||||
},
|
||||
"location": {
|
||||
"mode": "auto"
|
||||
},
|
||||
"name": "Day / night",
|
||||
"opacity": 0.5,
|
||||
"tooltip": true,
|
||||
"type": "dayNight"
|
||||
}
|
||||
],
|
||||
"tooltip": {
|
||||
"mode": "details"
|
||||
},
|
||||
"view": {
|
||||
"id": "coords",
|
||||
"lat": 29.633972,
|
||||
"lon": -17.661858,
|
||||
"zoom": 2.39
|
||||
}
|
||||
},
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"title": "Day / night layer",
|
||||
"type": "geomap"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 16,
|
||||
"w": 14,
|
||||
"x": 5,
|
||||
"y": 17
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"basemap": {
|
||||
"config": {},
|
||||
"name": "Layer 0",
|
||||
"type": "default"
|
||||
},
|
||||
"controls": {
|
||||
"mouseWheelZoom": true,
|
||||
"showAttribution": true,
|
||||
"showDebug": false,
|
||||
"showMeasure": true,
|
||||
"showScale": false,
|
||||
"showZoom": true
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"config": {
|
||||
"style": {
|
||||
"color": {
|
||||
"fixed": "dark-green"
|
||||
},
|
||||
"lineWidth": 2,
|
||||
"opacity": 1,
|
||||
"rotation": {
|
||||
"fixed": 0,
|
||||
"max": 360,
|
||||
"min": -360,
|
||||
"mode": "mod"
|
||||
},
|
||||
"size": {
|
||||
"fixed": 5,
|
||||
"max": 15,
|
||||
"min": 2
|
||||
},
|
||||
"symbol": {
|
||||
"fixed": "img/icons/marker/circle.svg",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"textConfig": {
|
||||
"fontSize": 12,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"textAlign": "center",
|
||||
"textBaseline": "middle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Flight path",
|
||||
"tooltip": true,
|
||||
"type": "route"
|
||||
}
|
||||
],
|
||||
"tooltip": {
|
||||
"mode": "details"
|
||||
},
|
||||
"view": {
|
||||
"id": "coords",
|
||||
"lat": 37.829114,
|
||||
"lon": -122.439462,
|
||||
"zoom": 11.92
|
||||
}
|
||||
},
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "simulation",
|
||||
"sim": {
|
||||
"key": {
|
||||
"tick": 10,
|
||||
"type": "flight"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "Route layer",
|
||||
"type": "geomap"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 37,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"geomap"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-5m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Panel Tests - Geomap 9.1",
|
||||
"uid": "KVxZKdmVz",
|
||||
"version": 10,
|
||||
"weekStart": ""
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "continuous-BlYlRd"
|
||||
},
|
||||
"custom": {
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 15,
|
||||
"w": 10,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"basemap": {
|
||||
"config": {},
|
||||
"name": "Layer 0",
|
||||
"type": "default"
|
||||
},
|
||||
"controls": {
|
||||
"mouseWheelZoom": true,
|
||||
"showAttribution": true,
|
||||
"showDebug": false,
|
||||
"showMeasure": false,
|
||||
"showScale": false,
|
||||
"showZoom": true
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"config": {
|
||||
"showLegend": true,
|
||||
"style": {
|
||||
"color": {
|
||||
"field": "Count",
|
||||
"fixed": "dark-green"
|
||||
},
|
||||
"opacity": 0.4,
|
||||
"rotation": {
|
||||
"fixed": 0,
|
||||
"max": 360,
|
||||
"min": -360,
|
||||
"mode": "mod"
|
||||
},
|
||||
"size": {
|
||||
"fixed": 5,
|
||||
"max": 15,
|
||||
"min": 2
|
||||
},
|
||||
"symbol": {
|
||||
"fixed": "img/icons/marker/circle.svg",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"textConfig": {
|
||||
"fontSize": 12,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"textAlign": "center",
|
||||
"textBaseline": "middle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"mode": "auto"
|
||||
},
|
||||
"name": "Layer 1",
|
||||
"tooltip": true,
|
||||
"type": "markers"
|
||||
}
|
||||
],
|
||||
"tooltip": {
|
||||
"mode": "details"
|
||||
},
|
||||
"view": {
|
||||
"id": "zero",
|
||||
"lat": 0,
|
||||
"lon": 0,
|
||||
"zoom": 1
|
||||
}
|
||||
},
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvFileName": "flight_info_by_state.csv",
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_file"
|
||||
}
|
||||
],
|
||||
"title": "Geomap - auto min/max",
|
||||
"type": "geomap"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "continuous-BlYlRd"
|
||||
},
|
||||
"custom": {
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"max": 15,
|
||||
"min": 1,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 15,
|
||||
"w": 10,
|
||||
"x": 10,
|
||||
"y": 0
|
||||
},
|
||||
"id": 7,
|
||||
"options": {
|
||||
"basemap": {
|
||||
"config": {},
|
||||
"name": "Layer 0",
|
||||
"type": "default"
|
||||
},
|
||||
"controls": {
|
||||
"mouseWheelZoom": true,
|
||||
"showAttribution": true,
|
||||
"showDebug": false,
|
||||
"showMeasure": false,
|
||||
"showScale": false,
|
||||
"showZoom": true
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"config": {
|
||||
"showLegend": true,
|
||||
"style": {
|
||||
"color": {
|
||||
"field": "Count",
|
||||
"fixed": "dark-green"
|
||||
},
|
||||
"opacity": 0.4,
|
||||
"rotation": {
|
||||
"fixed": 0,
|
||||
"max": 360,
|
||||
"min": -360,
|
||||
"mode": "mod"
|
||||
},
|
||||
"size": {
|
||||
"fixed": 5,
|
||||
"max": 15,
|
||||
"min": 2
|
||||
},
|
||||
"symbol": {
|
||||
"fixed": "img/icons/marker/circle.svg",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"textConfig": {
|
||||
"fontSize": 12,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"textAlign": "center",
|
||||
"textBaseline": "middle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"mode": "auto"
|
||||
},
|
||||
"name": "Layer 1",
|
||||
"tooltip": true,
|
||||
"type": "markers"
|
||||
}
|
||||
],
|
||||
"tooltip": {
|
||||
"mode": "details"
|
||||
},
|
||||
"view": {
|
||||
"id": "zero",
|
||||
"lat": 0,
|
||||
"lon": 0,
|
||||
"zoom": 1
|
||||
}
|
||||
},
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 2,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Geomap - min/max",
|
||||
"type": "geomap"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "continuous-BlYlRd"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 15,
|
||||
"w": 2,
|
||||
"x": 20,
|
||||
"y": 0
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"min"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 2,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Min values",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "continuous-BlYlRd"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 15,
|
||||
"w": 2,
|
||||
"x": 22,
|
||||
"y": 0
|
||||
},
|
||||
"id": 5,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"max"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 2,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Max values",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "continuous-BlYlRd"
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"displayMode": "auto",
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Count"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.displayMode",
|
||||
"value": "color-background"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Price"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.displayMode",
|
||||
"value": "color-background"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 11,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 15
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"footer": {
|
||||
"fields": "",
|
||||
"reducer": [
|
||||
"max"
|
||||
],
|
||||
"show": true
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 2,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Panel Title",
|
||||
"type": "table"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 37,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"geomap"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Geomap - color field Copy",
|
||||
"uid": "XMuLlpZ4k",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"controls": {
|
||||
"mouseWheelZoom": true,
|
||||
"showAttribution": false,
|
||||
"showDebug": false,
|
||||
"showMeasure": false,
|
||||
"showScale": false,
|
||||
"showZoom": false
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "details"
|
||||
},
|
||||
"view": {
|
||||
"id": "zero",
|
||||
"lat": 0,
|
||||
"lon": 0,
|
||||
"zoom": 1
|
||||
}
|
||||
},
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvFileName": "flight_info_by_state.csv",
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_file"
|
||||
}
|
||||
],
|
||||
"title": "Geomap/Geohash",
|
||||
"type": "geomap"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 37,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"geomap"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-5m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Panel Tests - Geomap geohash transformer",
|
||||
"uid": "P2jR04WVk",
|
||||
"version": 10,
|
||||
"weekStart": ""
|
||||
}
|
||||
@@ -440,8 +440,8 @@
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"geomap",
|
||||
"panel-tests"
|
||||
"panel-tests",
|
||||
"geomap"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 83,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
@@ -50,7 +49,8 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -78,6 +78,7 @@
|
||||
"mouseWheelZoom": true,
|
||||
"showAttribution": true,
|
||||
"showDebug": false,
|
||||
"showMeasure": false,
|
||||
"showScale": false,
|
||||
"showZoom": true
|
||||
},
|
||||
@@ -107,6 +108,9 @@
|
||||
"type": "markers"
|
||||
}
|
||||
],
|
||||
"tooltip": {
|
||||
"mode": "details"
|
||||
},
|
||||
"view": {
|
||||
"id": "coords",
|
||||
"lat": 38.297683,
|
||||
@@ -115,7 +119,7 @@
|
||||
"zoom": 3.98
|
||||
}
|
||||
},
|
||||
"pluginVersion": "9.0.0-pre",
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvFileName": "flight_info_by_state.csv",
|
||||
@@ -148,7 +152,8 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -180,6 +185,7 @@
|
||||
"mouseWheelZoom": true,
|
||||
"showAttribution": true,
|
||||
"showDebug": false,
|
||||
"showMeasure": false,
|
||||
"showScale": false,
|
||||
"showZoom": true
|
||||
},
|
||||
@@ -209,6 +215,9 @@
|
||||
"type": "markers"
|
||||
}
|
||||
],
|
||||
"tooltip": {
|
||||
"mode": "details"
|
||||
},
|
||||
"view": {
|
||||
"id": "coords",
|
||||
"lat": 38.297683,
|
||||
@@ -217,7 +226,7 @@
|
||||
"zoom": 3.98
|
||||
}
|
||||
},
|
||||
"pluginVersion": "9.0.0-pre",
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvFileName": "flight_info_by_state.csv",
|
||||
@@ -250,7 +259,8 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -278,6 +288,7 @@
|
||||
"mouseWheelZoom": true,
|
||||
"showAttribution": true,
|
||||
"showDebug": false,
|
||||
"showMeasure": false,
|
||||
"showScale": false,
|
||||
"showZoom": true
|
||||
},
|
||||
@@ -302,6 +313,9 @@
|
||||
"type": "heatmap"
|
||||
}
|
||||
],
|
||||
"tooltip": {
|
||||
"mode": "details"
|
||||
},
|
||||
"view": {
|
||||
"id": "coords",
|
||||
"lat": 38.251497,
|
||||
@@ -310,7 +324,7 @@
|
||||
"zoom": 4.15
|
||||
}
|
||||
},
|
||||
"pluginVersion": "9.0.0-pre",
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvFileName": "flight_info_by_state.csv",
|
||||
@@ -344,7 +358,8 @@
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
@@ -374,6 +389,7 @@
|
||||
"mouseWheelZoom": true,
|
||||
"showAttribution": true,
|
||||
"showDebug": false,
|
||||
"showMeasure": false,
|
||||
"showScale": false,
|
||||
"showZoom": true
|
||||
},
|
||||
@@ -402,6 +418,9 @@
|
||||
"type": "markers"
|
||||
}
|
||||
],
|
||||
"tooltip": {
|
||||
"mode": "details"
|
||||
},
|
||||
"view": {
|
||||
"id": "coords",
|
||||
"lat": 40.159084,
|
||||
@@ -410,7 +429,7 @@
|
||||
"zoom": 3.83
|
||||
}
|
||||
},
|
||||
"pluginVersion": "9.0.0-pre",
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvFileName": "flight_info_by_state.csv",
|
||||
@@ -423,11 +442,12 @@
|
||||
}
|
||||
],
|
||||
"refresh": "",
|
||||
"schemaVersion": 36,
|
||||
"schemaVersion": 37,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests"
|
||||
"panel-tests",
|
||||
"geomap"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
@@ -452,6 +472,6 @@
|
||||
"timezone": "",
|
||||
"title": "Panel Tests - Geomap",
|
||||
"uid": "2xuwrgV7z",
|
||||
"version": 2,
|
||||
"version": 4,
|
||||
"weekStart": ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "grafana"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 1348,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"code": {
|
||||
"language": "plaintext",
|
||||
"showLineNumbers": false,
|
||||
"showMiniMap": false
|
||||
},
|
||||
"content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod\n\n---\ntext = $Text",
|
||||
"mode": "markdown"
|
||||
},
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"title": "Markdown (with variables)",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 5,
|
||||
"options": {
|
||||
"code": {
|
||||
"language": "json",
|
||||
"showLineNumbers": true,
|
||||
"showMiniMap": false
|
||||
},
|
||||
"content": "{\n \"datacenter\": $datacenter,\n \"server\": $server,\n \"pod\": $pod\n \"text\": $Text\n}\n",
|
||||
"mode": "code"
|
||||
},
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"title": "JSON (with variables)",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 9
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"code": {
|
||||
"language": "plaintext",
|
||||
"showLineNumbers": false,
|
||||
"showMiniMap": false
|
||||
},
|
||||
"content": "<h3>Data center</h3>\n<p>$datacenter</p>\n\n<h3>server</h3>\n<p>$server</p>\n\n<h3>pod</h3>\n<p>$pod</p>\n\n<h3>Text</h3>\n<p>$Text</p>",
|
||||
"mode": "html"
|
||||
},
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"title": "HTML (with variables)",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 9
|
||||
},
|
||||
"id": 7,
|
||||
"options": {
|
||||
"code": {
|
||||
"language": "markdown",
|
||||
"showLineNumbers": true,
|
||||
"showMiniMap": true
|
||||
},
|
||||
"content": "## Data center\n$datacenter\n\n### server\n$server\n\n#### pod = \n$pod\n",
|
||||
"mode": "code"
|
||||
},
|
||||
"pluginVersion": "9.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
}
|
||||
],
|
||||
"title": "Markdown (code w/ with variables)",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
"schemaVersion": 37,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {
|
||||
"selected": false,
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"definition": "*",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"multi": true,
|
||||
"name": "datacenter",
|
||||
"options": [],
|
||||
"query": {
|
||||
"query": "*",
|
||||
"refId": "gdev-testdata-datacenter-Variable-Query"
|
||||
},
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"tagValuesQuery": "",
|
||||
"tagsQuery": "",
|
||||
"type": "query",
|
||||
"useTags": false
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"selected": false,
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"definition": "$datacenter.*",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"multi": true,
|
||||
"name": "server",
|
||||
"options": [],
|
||||
"query": {
|
||||
"query": "$datacenter.*",
|
||||
"refId": "gdev-testdata-server-Variable-Query"
|
||||
},
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"tagValuesQuery": "",
|
||||
"tagsQuery": "",
|
||||
"type": "query",
|
||||
"useTags": false
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"selected": false,
|
||||
"text": [
|
||||
"AAA",
|
||||
"ACB"
|
||||
],
|
||||
"value": [
|
||||
"AAA",
|
||||
"ACB"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"definition": "$datacenter.$server.*",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"multi": true,
|
||||
"name": "pod",
|
||||
"options": [],
|
||||
"query": {
|
||||
"query": "$datacenter.$server.*",
|
||||
"refId": "gdev-testdata-pod-Variable-Query"
|
||||
},
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"tagValuesQuery": "",
|
||||
"tagsQuery": "",
|
||||
"type": "query",
|
||||
"useTags": false
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"selected": false,
|
||||
"text": "temp",
|
||||
"value": "temp"
|
||||
},
|
||||
"hide": 0,
|
||||
"name": "Text",
|
||||
"options": [
|
||||
{
|
||||
"selected": true,
|
||||
"text": "temp",
|
||||
"value": "temp"
|
||||
}
|
||||
],
|
||||
"query": "temp",
|
||||
"skipUrlSync": false,
|
||||
"type": "textbox"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Text options",
|
||||
"uid": "WZ7AhQiVz",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
const DASHBOARD_ID = 'P2jR04WVk';
|
||||
|
||||
const MAP_LAYERS_TYPE = 'Map layers Layer type';
|
||||
const MAP_LAYERS_DATA = 'Map layers Data';
|
||||
const MAP_LAYERS_GEOJSON = 'Map layers GeoJSON URL';
|
||||
|
||||
e2e.scenario({
|
||||
describeName: 'Geomap layer types',
|
||||
itName: 'Tests changing the layer type',
|
||||
addScenarioDataSource: false,
|
||||
addScenarioDashBoard: false,
|
||||
skipScenario: false,
|
||||
scenario: () => {
|
||||
e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } });
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').should('be.visible');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).should('be.visible');
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').contains('markers');
|
||||
|
||||
// Heatmap
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Heatmap{enter}');
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').contains('heatmap');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible');
|
||||
e2e.components.PanelEditor.General.content().should('be.visible');
|
||||
|
||||
// GeoJSON
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('GeoJSON{enter}');
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').contains('geojson');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('not.exist');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('be.visible');
|
||||
e2e.components.PanelEditor.General.content().should('be.visible');
|
||||
|
||||
// Open Street Map
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Open Street Map{enter}');
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').contains('osm-standard');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('not.exist');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('not.exist');
|
||||
e2e.components.PanelEditor.General.content().should('be.visible');
|
||||
|
||||
// CARTO basemap
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('CARTO basemap{enter}');
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').contains('carto');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show labels').should('be.visible');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Theme').should('be.visible');
|
||||
e2e.components.PanelEditor.General.content().should('be.visible');
|
||||
|
||||
// ArcGIS MapServer
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('ArcGIS MapServer{enter}');
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').contains('esri-xyz');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Server instance').should('be.visible');
|
||||
e2e.components.PanelEditor.General.content().should('be.visible');
|
||||
|
||||
// XYZ Tile layer
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('XYZ Tile layer{enter}');
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').contains('xyz');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers URL template').should('be.visible');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Attribution').should('be.visible');
|
||||
e2e.components.PanelEditor.General.content().should('be.visible');
|
||||
},
|
||||
});
|
||||
|
||||
e2e.scenario({
|
||||
describeName: 'Geomap layer types (alpha)',
|
||||
itName: 'Tests changing the layer type (alpha)',
|
||||
addScenarioDataSource: false,
|
||||
addScenarioDashBoard: false,
|
||||
skipScenario: true,
|
||||
scenario: () => {
|
||||
e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } });
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').should('be.visible');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).should('be.visible');
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').contains('markers');
|
||||
|
||||
// Icon at last point (Alpha)
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Icon at last point{enter}');
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').contains('last-point-tracker');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible');
|
||||
e2e.components.PanelEditor.General.content().should('be.visible');
|
||||
|
||||
// Dynamic GeoJSON (Alpha)
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Dynamic GeoJSON{enter}');
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').contains('dynamic-geojson');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('be.visible');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers ID Field').should('be.visible');
|
||||
e2e.components.PanelEditor.General.content().should('be.visible');
|
||||
|
||||
// Night / Day (Alpha)
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Night / Day{enter}');
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').contains('dayNight');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show').should('be.visible');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Night region color').should('be.visible');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Display sun').should('be.visible');
|
||||
e2e.components.PanelEditor.General.content().should('be.visible');
|
||||
|
||||
// Route (Alpha)
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Route{enter}');
|
||||
cy.get('[data-testid="layer-drag-drop-list"]').contains('route');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Location').should('be.visible');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Style').should('be.visible');
|
||||
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Line width').should('be.visible');
|
||||
e2e.components.PanelEditor.General.content().should('be.visible');
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { e2e } from '@grafana/e2e';
|
||||
const DASHBOARD_ID = 'P2jR04WVk';
|
||||
|
||||
e2e.scenario({
|
||||
describeName: 'Geomap map controls options',
|
||||
itName: 'Tests map controls options',
|
||||
addScenarioDataSource: false,
|
||||
addScenarioDashBoard: false,
|
||||
skipScenario: false,
|
||||
scenario: () => {
|
||||
e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } });
|
||||
|
||||
// Show zoom
|
||||
e2e.components.OptionsGroup.group('Map controls').scrollIntoView().should('exist');
|
||||
cy.get('input[id="controls.showZoom"]').check({ force: true });
|
||||
cy.contains('+');
|
||||
cy.get('.ol-zoom').should('exist');
|
||||
|
||||
// Show attribution
|
||||
cy.get('input[id="controls.showAttribution"]').check({ force: true });
|
||||
cy.get('.ol-attribution').should('exist');
|
||||
|
||||
// Show scale
|
||||
cy.get('input[id="controls.showScale"]').check({ force: true });
|
||||
cy.get('.ol-scale-line').should('exist');
|
||||
|
||||
// Show measure tool
|
||||
cy.get('input[id="controls.showMeasure"]').check({ force: true });
|
||||
e2e.components.PanelEditor.measureButton().should('exist');
|
||||
|
||||
// Show debug
|
||||
cy.get('input[id="controls.showDebug"]').check({ force: true });
|
||||
e2e.components.DebugOverlay.wrapper().should('exist');
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
const DASHBOARD_ID = 'P2jR04WVk';
|
||||
|
||||
e2e.scenario({
|
||||
describeName: 'Geomap spatial operations - auto transforms',
|
||||
itName: 'Tests location auto option',
|
||||
addScenarioDataSource: false,
|
||||
addScenarioDashBoard: false,
|
||||
skipScenario: false,
|
||||
scenario: () => {
|
||||
e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } });
|
||||
e2e.components.Tab.title('Transform').should('be.visible').click();
|
||||
|
||||
e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click();
|
||||
e2e.components.Transforms.SpatialOperations.actionLabel().type('Prepare spatial field{enter}');
|
||||
e2e.components.Transforms.SpatialOperations.locationLabel().should('be.visible');
|
||||
|
||||
e2e.components.Transforms.SpatialOperations.location.autoOption().check({ force: true });
|
||||
e2e.components.Transforms.SpatialOperations.location.autoOption().should('be.checked');
|
||||
|
||||
e2e.components.PanelEditor.toggleTableView().click({ force: true });
|
||||
e2e.components.Panels.Visualization.Table.header()
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
cy.contains('Point').should('be.visible');
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
e2e.scenario({
|
||||
describeName: 'Geomap spatial operations - coords transforms ',
|
||||
itName: 'Tests location coords option',
|
||||
addScenarioDataSource: false,
|
||||
addScenarioDashBoard: false,
|
||||
skipScenario: false,
|
||||
scenario: () => {
|
||||
e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } });
|
||||
e2e.components.Tab.title('Transform').should('be.visible').click();
|
||||
|
||||
e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click();
|
||||
e2e.components.Transforms.SpatialOperations.actionLabel().type('Prepare spatial field{enter}');
|
||||
e2e.components.Transforms.SpatialOperations.locationLabel().should('be.visible');
|
||||
e2e.components.Transforms.SpatialOperations.location.coords.option().check({ force: true });
|
||||
e2e.components.Transforms.SpatialOperations.location.coords.option().should('be.checked');
|
||||
|
||||
e2e.components.Transforms.SpatialOperations.location.coords.latitudeFieldLabel().should('be.visible');
|
||||
e2e.components.Transforms.SpatialOperations.location.coords.latitudeFieldLabel().type('Lat{enter}');
|
||||
|
||||
e2e.components.Transforms.SpatialOperations.location.coords.longitudeFieldLabel().should('be.visible');
|
||||
e2e.components.Transforms.SpatialOperations.location.coords.longitudeFieldLabel().type('Lng{enter}');
|
||||
|
||||
e2e.components.PanelEditor.toggleTableView().click({ force: true });
|
||||
e2e.components.Panels.Visualization.Table.header()
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
cy.contains('Point').should('be.visible');
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
e2e.scenario({
|
||||
describeName: 'Geomap spatial operations - geohash transforms ',
|
||||
itName: 'Tests geoshash field column appears in table view',
|
||||
addScenarioDataSource: false,
|
||||
addScenarioDashBoard: false,
|
||||
skipScenario: false,
|
||||
scenario: () => {
|
||||
e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } });
|
||||
e2e.components.Tab.title('Transform').should('be.visible').click();
|
||||
|
||||
e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click();
|
||||
e2e.components.Transforms.SpatialOperations.actionLabel().type('Prepare spatial field{enter}');
|
||||
e2e.components.Transforms.SpatialOperations.locationLabel().should('be.visible');
|
||||
e2e.components.Transforms.SpatialOperations.location.geohash.option().check({ force: true });
|
||||
e2e.components.Transforms.SpatialOperations.location.geohash
|
||||
.geohashFieldLabel()
|
||||
.should('be.visible')
|
||||
.type('State{enter}');
|
||||
|
||||
e2e.components.PanelEditor.toggleTableView().click({ force: true });
|
||||
e2e.components.Panels.Visualization.Table.header()
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
cy.contains('State 1').should('be.visible');
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
e2e.scenario({
|
||||
describeName: 'Geomap spatial operations - lookup transforms ',
|
||||
itName: 'Tests location lookup option',
|
||||
addScenarioDataSource: false,
|
||||
addScenarioDashBoard: false,
|
||||
skipScenario: false,
|
||||
scenario: () => {
|
||||
e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } });
|
||||
e2e.components.Tab.title('Transform').should('be.visible').click();
|
||||
|
||||
e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click();
|
||||
e2e.components.Transforms.SpatialOperations.actionLabel().type('Prepare spatial field{enter}');
|
||||
e2e.components.Transforms.SpatialOperations.locationLabel().should('be.visible');
|
||||
e2e.components.Transforms.SpatialOperations.location.lookup.option().check({ force: true });
|
||||
e2e.components.Transforms.SpatialOperations.location.lookup.option().should('be.checked');
|
||||
|
||||
e2e.components.Transforms.SpatialOperations.location.lookup.lookupFieldLabel().should('be.visible');
|
||||
e2e.components.Transforms.SpatialOperations.location.lookup.lookupFieldLabel().type('State{enter}');
|
||||
|
||||
e2e.components.Transforms.SpatialOperations.location.lookup.gazetteerFieldLabel().should('be.visible');
|
||||
e2e.components.Transforms.SpatialOperations.location.lookup.gazetteerFieldLabel().type('USA States{enter}');
|
||||
|
||||
e2e.components.PanelEditor.toggleTableView().click({ force: true });
|
||||
e2e.components.Panels.Visualization.Table.header()
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
cy.contains('Geometry').should('be.visible');
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -127,6 +127,9 @@ export const Components = {
|
||||
toggleVizPicker: 'toggle-viz-picker',
|
||||
toggleVizOptions: 'toggle-viz-options',
|
||||
toggleTableView: 'toggle-table-view',
|
||||
|
||||
// [Geomap] Map controls
|
||||
measureButton: 'show measure tools',
|
||||
},
|
||||
PanelInspector: {
|
||||
Data: {
|
||||
@@ -199,6 +202,27 @@ export const Components = {
|
||||
modeLabel: 'Transform mode label',
|
||||
calculationsLabel: 'Transform calculations label',
|
||||
},
|
||||
SpatialOperations: {
|
||||
actionLabel: 'root Action field property editor',
|
||||
locationLabel: 'root Location field property editor',
|
||||
location: {
|
||||
autoOption: 'Auto location option',
|
||||
coords: {
|
||||
option: 'Coords location option',
|
||||
latitudeFieldLabel: 'root Latitude field field property editor',
|
||||
longitudeFieldLabel: 'root Longitude field field property editor',
|
||||
},
|
||||
geohash: {
|
||||
option: 'Geohash location option',
|
||||
geohashFieldLabel: 'root Geohash field field property editor',
|
||||
},
|
||||
lookup: {
|
||||
option: 'Lookup location option',
|
||||
lookupFieldLabel: 'root Lookup field field property editor',
|
||||
gazetteerFieldLabel: 'root Gazetteer field property editor',
|
||||
},
|
||||
},
|
||||
},
|
||||
searchInput: 'search transformations',
|
||||
},
|
||||
PageToolbar: {
|
||||
@@ -347,4 +371,7 @@ export const Components = {
|
||||
inputField: 'data-testid-file-upload-input-field',
|
||||
fileNameSpan: 'data-testid-file-upload-file-name',
|
||||
},
|
||||
DebugOverlay: {
|
||||
wrapper: 'debug-overlay',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -148,6 +148,7 @@ export const getAvailableIcons = () =>
|
||||
'record-audio',
|
||||
'repeat',
|
||||
'rocket',
|
||||
'ruler-combined',
|
||||
'save',
|
||||
'search',
|
||||
'search-minus',
|
||||
|
||||
@@ -458,7 +458,7 @@
|
||||
"hasUpdate": false,
|
||||
"defaultNavUrl": "/plugins/geomap/",
|
||||
"category": "",
|
||||
"state": "beta",
|
||||
"state": "",
|
||||
"signature": "internal",
|
||||
"signatureType": "",
|
||||
"signatureOrg": ""
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { AddLayerButton, AddLayerButtonProps } from './AddLayerButton';
|
||||
|
||||
describe('AddLayerButton', () => {
|
||||
const testLabel = 'Add Layer';
|
||||
it('renders AddLayerButton', () => {
|
||||
renderScenario({});
|
||||
|
||||
const button = screen.getByLabelText(selectors.components.ValuePicker.button(testLabel));
|
||||
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
function renderScenario(overrides: Partial<AddLayerButtonProps>) {
|
||||
const dummyOptions = [{ description: 'Use markers to render each data point', label: 'Markers', value: 'markers' }];
|
||||
const props: AddLayerButtonProps = {
|
||||
onChange: jest.fn(),
|
||||
options: dummyOptions,
|
||||
label: testLabel,
|
||||
};
|
||||
|
||||
Object.assign(props, overrides);
|
||||
|
||||
return {
|
||||
props,
|
||||
renderResult: render(<AddLayerButton {...props} />),
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { ValuePicker } from '@grafana/ui';
|
||||
|
||||
type AddLayerButtonProps = {
|
||||
export type AddLayerButtonProps = {
|
||||
onChange: (sel: SelectableValue<string>) => void;
|
||||
options: Array<SelectableValue<string>>;
|
||||
label: string;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { DATA_TEST_ID, LayerDragDropList, LayerDragDropListProps } from './LayerDragDropList';
|
||||
|
||||
describe('LayerDragDropList', () => {
|
||||
type testLayer = { name: string; getName: () => string };
|
||||
const layerOneName = 'rocket12345';
|
||||
const layerTwoName = 'man6789';
|
||||
it('renders LayerDragDropList', () => {
|
||||
renderScenario({});
|
||||
|
||||
screen.getByTestId(DATA_TEST_ID);
|
||||
screen.getByText(layerOneName);
|
||||
screen.getByText(layerTwoName);
|
||||
});
|
||||
|
||||
it('excludeBaseLayer', () => {
|
||||
renderScenario({ excludeBaseLayer: true });
|
||||
|
||||
screen.getByText(layerTwoName);
|
||||
expect(screen.queryByTestId(layerOneName)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('showActions', () => {
|
||||
renderScenario({ showActions: () => true });
|
||||
|
||||
expect(screen.getAllByLabelText('Duplicate button').length).toEqual(2);
|
||||
expect(screen.getAllByLabelText('Remove button').length).toEqual(2);
|
||||
});
|
||||
|
||||
it('showActions - no duplicate', () => {
|
||||
renderScenario({ showActions: () => true, onDuplicate: undefined });
|
||||
|
||||
expect(screen.getAllByLabelText('Remove button').length).toEqual(2);
|
||||
expect(screen.queryAllByLabelText('Duplicate button').length).toEqual(0);
|
||||
});
|
||||
|
||||
it('renders draggable icon', () => {
|
||||
renderScenario({});
|
||||
|
||||
expect(screen.getAllByLabelText('Drag and drop icon').length).toEqual(2);
|
||||
});
|
||||
|
||||
it('does not render draggable icon', () => {
|
||||
renderScenario({ excludeBaseLayer: true });
|
||||
|
||||
expect(screen.queryAllByLabelText('Drag and drop icon').length).toEqual(0);
|
||||
});
|
||||
|
||||
function renderScenario(overrides: Partial<LayerDragDropListProps<testLayer>>) {
|
||||
const testLayers = [
|
||||
{ name: layerOneName, getName: () => layerOneName },
|
||||
{ name: layerTwoName, getName: () => layerTwoName },
|
||||
];
|
||||
const props: LayerDragDropListProps<testLayer> = {
|
||||
layers: testLayers,
|
||||
getLayerInfo: jest.fn(),
|
||||
onDragEnd: jest.fn(),
|
||||
onSelect: jest.fn(),
|
||||
onDelete: jest.fn(),
|
||||
onDuplicate: jest.fn(),
|
||||
showActions: jest.fn(),
|
||||
selection: [],
|
||||
excludeBaseLayer: false,
|
||||
onNameChange: jest.fn(),
|
||||
verifyLayerNameUniqueness: undefined,
|
||||
};
|
||||
|
||||
Object.assign(props, overrides);
|
||||
|
||||
return {
|
||||
props,
|
||||
renderResult: render(<LayerDragDropList {...props} />),
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -9,17 +9,19 @@ import { Icon, IconButton, stylesFactory } from '@grafana/ui';
|
||||
import { LayerName } from './LayerName';
|
||||
import { LayerElement } from './types';
|
||||
|
||||
type LayerDragDropListProps<T extends LayerElement> = {
|
||||
export const DATA_TEST_ID = 'layer-drag-drop-list';
|
||||
|
||||
export type LayerDragDropListProps<T extends LayerElement> = {
|
||||
layers: T[];
|
||||
getLayerInfo: (element: T) => string;
|
||||
onDragEnd: (result: DropResult) => void;
|
||||
onSelect: (element: T) => any;
|
||||
onDelete: (element: T) => any;
|
||||
onDuplicate?: (element: T) => any;
|
||||
onSelect: (element: T) => void;
|
||||
onDelete: (element: T) => void;
|
||||
onDuplicate?: (element: T) => void;
|
||||
showActions: (element: T) => boolean;
|
||||
selection?: string[]; // list of unique ids (names)
|
||||
excludeBaseLayer?: boolean;
|
||||
onNameChange: (element: T, newName: string) => any;
|
||||
onNameChange: (element: T, newName: string) => void;
|
||||
verifyLayerNameUniqueness?: (nameToCheck: string) => boolean;
|
||||
};
|
||||
|
||||
@@ -46,7 +48,7 @@ export const LayerDragDropList = <T extends LayerElement>({
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided, snapshot) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
<div {...provided.droppableProps} ref={provided.innerRef} data-testid={DATA_TEST_ID}>
|
||||
{(() => {
|
||||
// reverse order
|
||||
const rows: any = [];
|
||||
@@ -80,6 +82,7 @@ export const LayerDragDropList = <T extends LayerElement>({
|
||||
<IconButton
|
||||
name="copy"
|
||||
title={'Duplicate'}
|
||||
ariaLabel={'Duplicate button'}
|
||||
className={style.actionIcon}
|
||||
onClick={() => onDuplicate(element)}
|
||||
/>
|
||||
@@ -88,6 +91,7 @@ export const LayerDragDropList = <T extends LayerElement>({
|
||||
<IconButton
|
||||
name="trash-alt"
|
||||
title={'remove'}
|
||||
ariaLabel={'Remove button'}
|
||||
className={cx(style.actionIcon, style.dragIcon)}
|
||||
onClick={() => onDelete(element)}
|
||||
/>
|
||||
@@ -95,6 +99,7 @@ export const LayerDragDropList = <T extends LayerElement>({
|
||||
)}
|
||||
{layers.length > shouldRenderDragIconLengthThreshold && (
|
||||
<Icon
|
||||
aria-label="Drag and drop icon"
|
||||
title="Drag and drop to reorder"
|
||||
name="draggabledots"
|
||||
size="lg"
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
FrameGeometrySourceMode,
|
||||
PanelOptionsEditorBuilder,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors/src';
|
||||
import { GazetteerPathEditor } from 'app/features/geo/editor/GazetteerPathEditor';
|
||||
|
||||
export function addLocationFields<TOptions>(
|
||||
@@ -20,10 +21,26 @@ export function addLocationFields<TOptions>(
|
||||
defaultValue: FrameGeometrySourceMode.Auto,
|
||||
settings: {
|
||||
options: [
|
||||
{ value: FrameGeometrySourceMode.Auto, label: 'Auto' },
|
||||
{ value: FrameGeometrySourceMode.Coords, label: 'Coords' },
|
||||
{ value: FrameGeometrySourceMode.Geohash, label: 'Geohash' },
|
||||
{ value: FrameGeometrySourceMode.Lookup, label: 'Lookup' },
|
||||
{
|
||||
value: FrameGeometrySourceMode.Auto,
|
||||
label: 'Auto',
|
||||
ariaLabel: selectors.components.Transforms.SpatialOperations.location.autoOption,
|
||||
},
|
||||
{
|
||||
value: FrameGeometrySourceMode.Coords,
|
||||
label: 'Coords',
|
||||
ariaLabel: selectors.components.Transforms.SpatialOperations.location.coords.option,
|
||||
},
|
||||
{
|
||||
value: FrameGeometrySourceMode.Geohash,
|
||||
label: 'Geohash',
|
||||
ariaLabel: selectors.components.Transforms.SpatialOperations.location.geohash.option,
|
||||
},
|
||||
{
|
||||
value: FrameGeometrySourceMode.Lookup,
|
||||
label: 'Lookup',
|
||||
ariaLabel: selectors.components.Transforms.SpatialOperations.location.lookup.option,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,39 +1,33 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { CSSProperties, PureComponent } from 'react';
|
||||
import React, { CSSProperties } from 'react';
|
||||
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { stylesFactory } from '@grafana/ui';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
export interface OverlayProps {
|
||||
topRight?: React.ReactNode[];
|
||||
topRight1?: React.ReactNode[];
|
||||
topRight2?: React.ReactNode[];
|
||||
bottomLeft?: React.ReactNode[];
|
||||
blStyle?: CSSProperties;
|
||||
}
|
||||
|
||||
export class GeomapOverlay extends PureComponent<OverlayProps> {
|
||||
style = getStyles(config.theme);
|
||||
export const GeomapOverlay = ({ topRight1, topRight2, bottomLeft, blStyle }: OverlayProps) => {
|
||||
const topRight1Exists = (topRight1 && topRight1.length > 0) ?? false;
|
||||
const styles = useStyles2(getStyles(topRight1Exists));
|
||||
return (
|
||||
<div className={styles.overlay}>
|
||||
{Boolean(topRight1?.length) && <div className={styles.TR1}>{topRight1}</div>}
|
||||
{Boolean(topRight2?.length) && <div className={styles.TR2}>{topRight2}</div>}
|
||||
{Boolean(bottomLeft?.length) && (
|
||||
<div className={styles.BL} style={blStyle}>
|
||||
{bottomLeft}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
constructor(props: OverlayProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { topRight, bottomLeft } = this.props;
|
||||
return (
|
||||
<div className={this.style.overlay}>
|
||||
{Boolean(topRight?.length) && <div className={this.style.TR}>{topRight}</div>}
|
||||
{Boolean(bottomLeft?.length) && (
|
||||
<div className={this.style.BL} style={this.props.blStyle}>
|
||||
{bottomLeft}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
const getStyles = (topRight1Exists: boolean) => (theme: GrafanaTheme2) => ({
|
||||
overlay: css`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
@@ -41,9 +35,15 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
z-index: 500;
|
||||
pointer-events: none;
|
||||
`,
|
||||
TR: css`
|
||||
TR1: css`
|
||||
right: 0.5em;
|
||||
pointer-events: auto;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
top: 0.5em;
|
||||
`,
|
||||
TR2: css`
|
||||
position: absolute;
|
||||
top: ${topRight1Exists ? '80' : '8'}px;
|
||||
right: 8px;
|
||||
pointer-events: auto;
|
||||
`,
|
||||
@@ -53,4 +53,4 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
left: 8px;
|
||||
pointer-events: auto;
|
||||
`,
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -1,32 +1,14 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Global } from '@emotion/react';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { Collection, Map as OpenLayersMap, MapBrowserEvent, PluggableMap, View } from 'ol';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import { Map as OpenLayersMap, MapBrowserEvent, View } from 'ol';
|
||||
import Attribution from 'ol/control/Attribution';
|
||||
import ScaleLine from 'ol/control/ScaleLine';
|
||||
import Zoom from 'ol/control/Zoom';
|
||||
import { Coordinate } from 'ol/coordinate';
|
||||
import { isEmpty } from 'ol/extent';
|
||||
import { defaults as interactionDefaults } from 'ol/interaction';
|
||||
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
|
||||
import BaseLayer from 'ol/layer/Base';
|
||||
import { fromLonLat, toLonLat } from 'ol/proj';
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import {
|
||||
DataFrame,
|
||||
DataHoverClearEvent,
|
||||
DataHoverEvent,
|
||||
FrameGeometrySourceMode,
|
||||
getFrameMatchers,
|
||||
GrafanaTheme,
|
||||
MapLayerHandler,
|
||||
MapLayerOptions,
|
||||
PanelData,
|
||||
PanelProps,
|
||||
} from '@grafana/data';
|
||||
import { DataHoverEvent, GrafanaTheme, PanelData, PanelProps } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { PanelContext, PanelContextRoot, stylesFactory } from '@grafana/ui';
|
||||
import { PanelEditExitedEvent } from 'app/types/events';
|
||||
@@ -34,13 +16,18 @@ import { PanelEditExitedEvent } from 'app/types/events';
|
||||
import { GeomapOverlay, OverlayProps } from './GeomapOverlay';
|
||||
import { GeomapTooltip } from './GeomapTooltip';
|
||||
import { DebugOverlay } from './components/DebugOverlay';
|
||||
import { GeomapHoverPayload, GeomapLayerHover } from './event';
|
||||
import { MeasureOverlay } from './components/MeasureOverlay';
|
||||
import { MeasureVectorLayer } from './components/MeasureVectorLayer';
|
||||
import { GeomapHoverPayload } from './event';
|
||||
import { getGlobalStyles } from './globalStyles';
|
||||
import { defaultMarkersConfig, MARKERS_LAYER_ID } from './layers/data/markersLayer';
|
||||
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from './layers/registry';
|
||||
import { ControlsOptions, GeomapPanelOptions, MapLayerState, MapViewConfig, TooltipMode } from './types';
|
||||
import { getLayersExtent } from './utils/getLayersExtent';
|
||||
import { centerPointRegistry, MapCenterID } from './view';
|
||||
import { defaultMarkersConfig } from './layers/data/markersLayer';
|
||||
import { DEFAULT_BASEMAP_CONFIG } from './layers/registry';
|
||||
import { ControlsOptions, GeomapPanelOptions, MapLayerState, TooltipMode } from './types';
|
||||
import { getActions } from './utils/actions';
|
||||
import { applyLayerFilter, initLayer } from './utils/layers';
|
||||
import { pointerClickListener, pointerMoveListener, setTooltipListeners } from './utils/tootltip';
|
||||
import { updateMap, getNewOpenLayersMap, notifyPanelEditor } from './utils/utils';
|
||||
import { initMapView, initViewExtent } from './utils/view';
|
||||
|
||||
// Allows multiple panels to share the same view instance
|
||||
let sharedView: View | undefined = undefined;
|
||||
@@ -50,26 +37,12 @@ interface State extends OverlayProps {
|
||||
ttip?: GeomapHoverPayload;
|
||||
ttipOpen: boolean;
|
||||
legends: ReactNode[];
|
||||
}
|
||||
|
||||
export interface GeomapLayerActions {
|
||||
selectLayer: (uid: string) => void;
|
||||
deleteLayer: (uid: string) => void;
|
||||
addlayer: (type: string) => void;
|
||||
reorder: (src: number, dst: number) => void;
|
||||
canRename: (v: string) => boolean;
|
||||
}
|
||||
|
||||
export interface GeomapInstanceState {
|
||||
map?: OpenLayersMap;
|
||||
layers: MapLayerState[];
|
||||
selected: number;
|
||||
actions: GeomapLayerActions;
|
||||
measureMenuActive?: boolean;
|
||||
}
|
||||
|
||||
export class GeomapPanel extends Component<Props, State> {
|
||||
static contextType = PanelContextRoot;
|
||||
panelContext: PanelContext = {} as PanelContext;
|
||||
panelContext: PanelContext | undefined = undefined;
|
||||
private subs = new Subscription();
|
||||
|
||||
globalCSS = getGlobalStyles(config.theme2);
|
||||
@@ -97,7 +70,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.panelContext = this.context as PanelContext;
|
||||
this.panelContext = this.context;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -141,103 +114,26 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
}
|
||||
|
||||
/** This function will actually update the JSON model */
|
||||
private doOptionsUpdate(selected: number) {
|
||||
doOptionsUpdate(selected: number) {
|
||||
const { options, onOptionsChange } = this.props;
|
||||
const layers = this.layers;
|
||||
this.map?.getLayers().forEach((l) => {
|
||||
if (l instanceof MeasureVectorLayer) {
|
||||
this.map?.removeLayer(l);
|
||||
this.map?.addLayer(l);
|
||||
}
|
||||
});
|
||||
onOptionsChange({
|
||||
...options,
|
||||
basemap: layers[0].options,
|
||||
layers: layers.slice(1).map((v) => v.options),
|
||||
});
|
||||
|
||||
// Notify the panel editor
|
||||
if (this.panelContext.onInstanceStateChange) {
|
||||
this.panelContext.onInstanceStateChange({
|
||||
map: this.map,
|
||||
layers: layers,
|
||||
selected,
|
||||
actions: this.actions,
|
||||
});
|
||||
}
|
||||
|
||||
notifyPanelEditor(this, layers, selected);
|
||||
this.setState({ legends: this.getLegends() });
|
||||
}
|
||||
|
||||
getNextLayerName = () => {
|
||||
let idx = this.layers.length; // since basemap is 0, this looks right
|
||||
while (true && idx < 100) {
|
||||
const name = `Layer ${idx++}`;
|
||||
if (!this.byName.has(name)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return `Layer ${Date.now()}`;
|
||||
};
|
||||
|
||||
actions: GeomapLayerActions = {
|
||||
selectLayer: (uid: string) => {
|
||||
const selected = this.layers.findIndex((v) => v.options.name === uid);
|
||||
if (this.panelContext.onInstanceStateChange) {
|
||||
this.panelContext.onInstanceStateChange({
|
||||
map: this.map,
|
||||
layers: this.layers,
|
||||
selected,
|
||||
actions: this.actions,
|
||||
});
|
||||
}
|
||||
},
|
||||
canRename: (v: string) => {
|
||||
return !this.byName.has(v);
|
||||
},
|
||||
deleteLayer: (uid: string) => {
|
||||
const layers: MapLayerState[] = [];
|
||||
for (const lyr of this.layers) {
|
||||
if (lyr.options.name === uid) {
|
||||
this.map?.removeLayer(lyr.layer);
|
||||
} else {
|
||||
layers.push(lyr);
|
||||
}
|
||||
}
|
||||
this.layers = layers;
|
||||
this.doOptionsUpdate(0);
|
||||
},
|
||||
addlayer: (type: string) => {
|
||||
const item = geomapLayerRegistry.getIfExists(type);
|
||||
if (!item) {
|
||||
return; // ignore empty request
|
||||
}
|
||||
this.initLayer(
|
||||
this.map!,
|
||||
{
|
||||
type: item.id,
|
||||
name: this.getNextLayerName(),
|
||||
config: cloneDeep(item.defaultOptions),
|
||||
location: item.showLocation ? { mode: FrameGeometrySourceMode.Auto } : undefined,
|
||||
tooltip: true,
|
||||
},
|
||||
false
|
||||
).then((lyr) => {
|
||||
this.layers = this.layers.slice(0);
|
||||
this.layers.push(lyr);
|
||||
this.map?.addLayer(lyr.layer);
|
||||
|
||||
this.doOptionsUpdate(this.layers.length - 1);
|
||||
});
|
||||
},
|
||||
reorder: (startIndex: number, endIndex: number) => {
|
||||
const result = Array.from(this.layers);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
result.splice(endIndex, 0, removed);
|
||||
this.layers = result;
|
||||
|
||||
this.doOptionsUpdate(endIndex);
|
||||
|
||||
// Add the layers in the right order
|
||||
const group = this.map?.getLayers()!;
|
||||
group.clear();
|
||||
this.layers.forEach((v) => group.push(v.layer));
|
||||
},
|
||||
};
|
||||
actions = getActions(this);
|
||||
|
||||
/**
|
||||
* Called when the panel options change
|
||||
@@ -246,15 +142,14 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
*/
|
||||
optionsChanged(options: GeomapPanelOptions) {
|
||||
const oldOptions = this.props.options;
|
||||
console.log('options changed!', options);
|
||||
|
||||
if (options.view !== oldOptions.view) {
|
||||
console.log('View changed');
|
||||
this.map!.setView(this.initMapView(options.view, this.map!.getLayers()));
|
||||
const [updatedSharedView, view] = initMapView(options.view, sharedView, this.map!.getLayers());
|
||||
sharedView = updatedSharedView;
|
||||
// eslint-disable-next-line
|
||||
this.map!.setView(view as View);
|
||||
}
|
||||
|
||||
if (options.controls !== oldOptions.controls) {
|
||||
console.log('Controls changed');
|
||||
this.initControls(options.controls ?? { showZoom: true, showAttribution: true });
|
||||
}
|
||||
}
|
||||
@@ -266,7 +161,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
// Only update if panel data matches component data
|
||||
if (data === this.props.data) {
|
||||
for (const state of this.layers) {
|
||||
this.applyLayerFilter(state.handler, state.options);
|
||||
applyLayerFilter(state.handler, state.options, this.props.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,35 +173,27 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
}
|
||||
|
||||
if (!div) {
|
||||
// eslint-disable-next-line
|
||||
this.map = undefined as unknown as OpenLayersMap;
|
||||
return;
|
||||
}
|
||||
const { options } = this.props;
|
||||
|
||||
const map = (this.map = new OpenLayersMap({
|
||||
view: this.initMapView(options.view, undefined),
|
||||
pixelRatio: 1, // or zoom?
|
||||
layers: [], // loaded explicitly below
|
||||
controls: [],
|
||||
target: div,
|
||||
interactions: interactionDefaults({
|
||||
mouseWheelZoom: false, // managed by initControls
|
||||
}),
|
||||
}));
|
||||
const map = getNewOpenLayersMap(this, options, div);
|
||||
|
||||
this.byName.clear();
|
||||
const layers: MapLayerState[] = [];
|
||||
try {
|
||||
layers.push(await this.initLayer(map, options.basemap ?? DEFAULT_BASEMAP_CONFIG, true));
|
||||
layers.push(await initLayer(this, map, options.basemap ?? DEFAULT_BASEMAP_CONFIG, true));
|
||||
|
||||
// Default layer values
|
||||
const layerOptions = options.layers ?? [defaultMarkersConfig];
|
||||
|
||||
for (const lyr of layerOptions) {
|
||||
layers.push(await this.initLayer(map, lyr, false));
|
||||
layers.push(await initLayer(this, map, lyr, false));
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error('error loading layers', ex);
|
||||
console.error('error loading layers', ex); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
for (const lyr of layers) {
|
||||
@@ -314,29 +201,14 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
}
|
||||
this.layers = layers;
|
||||
this.map = map; // redundant
|
||||
this.initViewExtent(map.getView(), options.view, map.getLayers());
|
||||
initViewExtent(map.getView(), options.view, map.getLayers());
|
||||
|
||||
this.mouseWheelZoom = new MouseWheelZoom();
|
||||
this.map.addInteraction(this.mouseWheelZoom);
|
||||
this.initControls(options.controls);
|
||||
this.forceUpdate(); // first render
|
||||
this.map?.addInteraction(this.mouseWheelZoom);
|
||||
|
||||
// Tooltip listener
|
||||
this.map.on('singleclick', this.pointerClickListener);
|
||||
this.map.on('pointermove', this.pointerMoveListener);
|
||||
this.map.getViewport().addEventListener('mouseout', (evt) => {
|
||||
this.props.eventBus.publish(new DataHoverClearEvent());
|
||||
});
|
||||
|
||||
// Notify the panel editor
|
||||
if (this.panelContext.onInstanceStateChange) {
|
||||
this.panelContext.onInstanceStateChange({
|
||||
map: this.map,
|
||||
layers: layers,
|
||||
selected: layers.length - 1, // the top layer
|
||||
actions: this.actions,
|
||||
});
|
||||
}
|
||||
updateMap(this, options);
|
||||
setTooltipListeners(this);
|
||||
notifyPanelEditor(this, layers, layers.length - 1);
|
||||
|
||||
this.setState({ legends: this.getLegends() });
|
||||
};
|
||||
@@ -352,280 +224,13 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
};
|
||||
|
||||
pointerClickListener = (evt: MapBrowserEvent<UIEvent>) => {
|
||||
if (this.pointerMoveListener(evt)) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.mapDiv!.style.cursor = 'auto';
|
||||
this.setState({ ttipOpen: true });
|
||||
}
|
||||
pointerClickListener(evt, this);
|
||||
};
|
||||
|
||||
pointerMoveListener = (evt: MapBrowserEvent<UIEvent>) => {
|
||||
if (!this.map || this.state.ttipOpen) {
|
||||
return false;
|
||||
}
|
||||
const mouse = evt.originalEvent as any;
|
||||
const pixel = this.map.getEventPixel(mouse);
|
||||
const hover = toLonLat(this.map.getCoordinateFromPixel(pixel));
|
||||
|
||||
const { hoverPayload } = this;
|
||||
hoverPayload.pageX = mouse.pageX;
|
||||
hoverPayload.pageY = mouse.pageY;
|
||||
hoverPayload.point = {
|
||||
lat: hover[1],
|
||||
lon: hover[0],
|
||||
};
|
||||
hoverPayload.data = undefined;
|
||||
hoverPayload.columnIndex = undefined;
|
||||
hoverPayload.rowIndex = undefined;
|
||||
hoverPayload.layers = undefined;
|
||||
|
||||
const layers: GeomapLayerHover[] = [];
|
||||
const layerLookup = new Map<MapLayerState, GeomapLayerHover>();
|
||||
|
||||
let ttip: GeomapHoverPayload = {} as GeomapHoverPayload;
|
||||
this.map.forEachFeatureAtPixel(
|
||||
pixel,
|
||||
(feature, layer, geo) => {
|
||||
const s: MapLayerState = (layer as any).__state;
|
||||
//match hover layer to layer in layers
|
||||
//check if the layer show tooltip is enabled
|
||||
//then also pass the list of tooltip fields if exists
|
||||
//this is used as the generic hover event
|
||||
if (!hoverPayload.data) {
|
||||
const props = feature.getProperties();
|
||||
const frame = props['frame'];
|
||||
if (frame) {
|
||||
hoverPayload.data = ttip.data = frame as DataFrame;
|
||||
hoverPayload.rowIndex = ttip.rowIndex = props['rowIndex'];
|
||||
}
|
||||
|
||||
if (s?.mouseEvents) {
|
||||
s.mouseEvents.next(feature);
|
||||
}
|
||||
}
|
||||
|
||||
if (s) {
|
||||
let h = layerLookup.get(s);
|
||||
if (!h) {
|
||||
h = { layer: s, features: [] };
|
||||
layerLookup.set(s, h);
|
||||
layers.push(h);
|
||||
}
|
||||
h.features.push(feature);
|
||||
}
|
||||
},
|
||||
{
|
||||
layerFilter: (l) => {
|
||||
const hoverLayerState = (l as any).__state as MapLayerState;
|
||||
return hoverLayerState?.options?.tooltip !== false;
|
||||
},
|
||||
}
|
||||
);
|
||||
this.hoverPayload.layers = layers.length ? layers : undefined;
|
||||
this.props.eventBus.publish(this.hoverEvent);
|
||||
|
||||
this.setState({ ttip: { ...hoverPayload } });
|
||||
|
||||
if (!layers.length) {
|
||||
// clear mouse events
|
||||
this.layers.forEach((layer) => {
|
||||
layer.mouseEvents.next(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
const found = layers.length ? true : false;
|
||||
this.mapDiv!.style.cursor = found ? 'pointer' : 'auto';
|
||||
return found;
|
||||
pointerMoveListener(evt, this);
|
||||
};
|
||||
|
||||
private updateLayer = async (uid: string, newOptions: MapLayerOptions): Promise<boolean> => {
|
||||
if (!this.map) {
|
||||
return false;
|
||||
}
|
||||
const current = this.byName.get(uid);
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let layerIndex = -1;
|
||||
const group = this.map?.getLayers()!;
|
||||
for (let i = 0; i < group?.getLength(); i++) {
|
||||
if (group.item(i) === current.layer) {
|
||||
layerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for rename
|
||||
if (newOptions.name !== uid) {
|
||||
if (!newOptions.name) {
|
||||
newOptions.name = uid;
|
||||
} else if (this.byName.has(newOptions.name)) {
|
||||
return false;
|
||||
}
|
||||
console.log('Layer name changed', uid, '>>>', newOptions.name);
|
||||
this.byName.delete(uid);
|
||||
|
||||
uid = newOptions.name;
|
||||
this.byName.set(uid, current);
|
||||
}
|
||||
|
||||
// Type changed -- requires full re-initalization
|
||||
if (current.options.type !== newOptions.type) {
|
||||
// full init
|
||||
} else {
|
||||
// just update options
|
||||
}
|
||||
|
||||
const layers = this.layers.slice(0);
|
||||
try {
|
||||
const info = await this.initLayer(this.map, newOptions, current.isBasemap);
|
||||
layers[layerIndex]?.handler.dispose?.();
|
||||
layers[layerIndex] = info;
|
||||
group.setAt(layerIndex, info.layer);
|
||||
|
||||
// initialize with new data
|
||||
this.applyLayerFilter(info.handler, newOptions);
|
||||
} catch (err) {
|
||||
console.warn('ERROR', err);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Just to trigger a state update
|
||||
this.setState({ legends: [] });
|
||||
|
||||
this.layers = layers;
|
||||
this.doOptionsUpdate(layerIndex);
|
||||
return true;
|
||||
};
|
||||
|
||||
async initLayer(map: PluggableMap, options: MapLayerOptions, isBasemap?: boolean): Promise<MapLayerState> {
|
||||
if (isBasemap && (!options?.type || config.geomapDisableCustomBaseLayer)) {
|
||||
options = DEFAULT_BASEMAP_CONFIG;
|
||||
}
|
||||
|
||||
// Use default makers layer
|
||||
if (!options?.type) {
|
||||
options = {
|
||||
type: MARKERS_LAYER_ID,
|
||||
name: this.getNextLayerName(),
|
||||
config: {},
|
||||
};
|
||||
}
|
||||
|
||||
const item = geomapLayerRegistry.getIfExists(options.type);
|
||||
if (!item) {
|
||||
return Promise.reject('unknown layer: ' + options.type);
|
||||
}
|
||||
|
||||
const handler = await item.create(map, options, this.props.eventBus, config.theme2);
|
||||
const layer = handler.init();
|
||||
if (options.opacity != null) {
|
||||
layer.setOpacity(options.opacity);
|
||||
}
|
||||
|
||||
if (!options.name) {
|
||||
options.name = this.getNextLayerName();
|
||||
}
|
||||
|
||||
const UID = options.name;
|
||||
const state: MapLayerState<any> = {
|
||||
// UID, // unique name when added to the map (it may change and will need special handling)
|
||||
isBasemap,
|
||||
options,
|
||||
layer,
|
||||
handler,
|
||||
mouseEvents: new Subject<FeatureLike | undefined>(),
|
||||
|
||||
getName: () => UID,
|
||||
|
||||
// Used by the editors
|
||||
onChange: (cfg: MapLayerOptions) => {
|
||||
this.updateLayer(UID, cfg);
|
||||
},
|
||||
};
|
||||
|
||||
this.byName.set(UID, state);
|
||||
(state.layer as any).__state = state;
|
||||
|
||||
this.applyLayerFilter(handler, options);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
applyLayerFilter(handler: MapLayerHandler<any>, options: MapLayerOptions<any>): void {
|
||||
if (handler.update) {
|
||||
let panelData = this.props.data;
|
||||
if (options.filterData) {
|
||||
const matcherFunc = getFrameMatchers(options.filterData);
|
||||
panelData = {
|
||||
...panelData,
|
||||
series: panelData.series.filter(matcherFunc),
|
||||
};
|
||||
}
|
||||
handler.update(panelData);
|
||||
}
|
||||
}
|
||||
|
||||
initMapView(config: MapViewConfig, layers?: Collection<BaseLayer>): View {
|
||||
let view = new View({
|
||||
center: [0, 0],
|
||||
zoom: 1,
|
||||
showFullExtent: true, // allows zooming so the full range is visible
|
||||
});
|
||||
|
||||
// With shared views, all panels use the same view instance
|
||||
if (config.shared) {
|
||||
if (!sharedView) {
|
||||
sharedView = view;
|
||||
} else {
|
||||
view = sharedView;
|
||||
}
|
||||
}
|
||||
if (layers) {
|
||||
this.initViewExtent(view, config, layers);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
initViewExtent(view: View, config: MapViewConfig, layers: Collection<BaseLayer>) {
|
||||
const v = centerPointRegistry.getIfExists(config.id);
|
||||
if (v) {
|
||||
let coord: Coordinate | undefined = undefined;
|
||||
if (v.lat == null) {
|
||||
if (v.id === MapCenterID.Coordinates) {
|
||||
coord = [config.lon ?? 0, config.lat ?? 0];
|
||||
} else if (v.id === MapCenterID.Fit) {
|
||||
const extent = getLayersExtent(layers);
|
||||
if (!isEmpty(extent)) {
|
||||
view.fit(extent, {
|
||||
padding: [30, 30, 30, 30],
|
||||
maxZoom: config.zoom ?? config.maxZoom,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log('TODO, view requires special handling', v);
|
||||
}
|
||||
} else {
|
||||
coord = [v.lon ?? 0, v.lat ?? 0];
|
||||
}
|
||||
if (coord) {
|
||||
view.setCenter(fromLonLat(coord));
|
||||
}
|
||||
}
|
||||
|
||||
if (config.maxZoom) {
|
||||
view.setMaxZoom(config.maxZoom);
|
||||
}
|
||||
if (config.minZoom) {
|
||||
view.setMaxZoom(config.minZoom);
|
||||
}
|
||||
if (config.zoom && v?.id !== MapCenterID.Fit) {
|
||||
view.setZoom(config.zoom);
|
||||
}
|
||||
}
|
||||
|
||||
initControls(options: ControlsOptions) {
|
||||
if (!this.map) {
|
||||
return;
|
||||
@@ -652,12 +257,26 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
}
|
||||
|
||||
// Update the react overlays
|
||||
let topRight: ReactNode[] = [];
|
||||
if (options.showDebug) {
|
||||
topRight = [<DebugOverlay key="debug" map={this.map} />];
|
||||
let topRight1: ReactNode[] = [];
|
||||
if (options.showMeasure) {
|
||||
topRight1 = [
|
||||
<MeasureOverlay
|
||||
key="measure"
|
||||
map={this.map}
|
||||
// Lifts menuActive state and resets tooltip state upon close
|
||||
menuActiveState={(value: boolean) => {
|
||||
this.setState({ ttipOpen: value, measureMenuActive: value });
|
||||
}}
|
||||
/>,
|
||||
];
|
||||
}
|
||||
|
||||
this.setState({ topRight });
|
||||
let topRight2: ReactNode[] = [];
|
||||
if (options.showDebug) {
|
||||
topRight2 = [<DebugOverlay key="debug" map={this.map} />];
|
||||
}
|
||||
|
||||
this.setState({ topRight1, topRight2 });
|
||||
}
|
||||
|
||||
getLegends() {
|
||||
@@ -672,7 +291,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
let { ttip, ttipOpen, topRight, legends } = this.state;
|
||||
let { ttip, ttipOpen, topRight1, legends, topRight2 } = this.state;
|
||||
const { options } = this.props;
|
||||
const showScale = options.controls.showScale;
|
||||
if (!ttipOpen && options.tooltip?.mode === TooltipMode.None) {
|
||||
@@ -684,7 +303,12 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
<Global styles={this.globalCSS} />
|
||||
<div className={this.style.wrap} onMouseLeave={this.clearTooltip}>
|
||||
<div className={this.style.map} ref={this.initMapRef}></div>
|
||||
<GeomapOverlay bottomLeft={legends} topRight={topRight} blStyle={{ bottom: showScale ? '35px' : '8px' }} />
|
||||
<GeomapOverlay
|
||||
bottomLeft={legends}
|
||||
topRight1={topRight1}
|
||||
topRight2={topRight2}
|
||||
blStyle={{ bottom: showScale ? '35px' : '8px' }}
|
||||
/>
|
||||
</div>
|
||||
<GeomapTooltip ttip={ttip} isOpen={ttipOpen} onClose={this.tooltipPopupClosed} />
|
||||
</>
|
||||
|
||||
@@ -63,7 +63,7 @@ export const generateLabel = (feature: FeatureLike, idx: number): string => {
|
||||
const names = ['Name', 'name', 'Title', 'ID', 'id'];
|
||||
let props = feature.getProperties();
|
||||
let first = '';
|
||||
const frame = feature.get('frame') as DataFrame;
|
||||
const frame = feature.get('frame') as DataFrame; // eslint-disable-line
|
||||
if (frame) {
|
||||
const rowIndex = feature.get('rowIndex');
|
||||
for (const f of frame.fields) {
|
||||
|
||||
@@ -34,7 +34,7 @@ export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode }:
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayValues: Array<[string, any, string]> = [];
|
||||
const displayValues: Array<[string, unknown, string]> = [];
|
||||
const links: Array<LinkModel<Field>> = [];
|
||||
const linkLookup = new Set<string>();
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import React, { PureComponent } from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors/src';
|
||||
import { stylesFactory } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
@@ -43,7 +44,7 @@ export class DebugOverlay extends PureComponent<Props, State> {
|
||||
const { zoom, center } = this.state;
|
||||
|
||||
return (
|
||||
<div className={this.style.infoWrap}>
|
||||
<div className={this.style.infoWrap} aria-label={selectors.components.DebugOverlay.wrapper}>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
|
||||
@@ -36,13 +36,13 @@ export function MarkersLegend(props: MarkersLegendProps) {
|
||||
}
|
||||
|
||||
const props = hoverEvent.getProperties();
|
||||
const frame = props.frame as DataFrame;
|
||||
const frame = props.frame as DataFrame; // eslint-disable-line
|
||||
|
||||
if (!frame) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rowIndex = props.rowIndex as number;
|
||||
const rowIndex = props.rowIndex as number; // eslint-disable-line
|
||||
return colorField.values.get(rowIndex);
|
||||
}, [hoverEvent, colorField]);
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { css } from '@emotion/css';
|
||||
import Map from 'ol/Map';
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Button, IconButton, RadioButtonGroup, Select, stylesFactory } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
import { MapMeasure, MapMeasureOptions, measures } from '../utils/measure';
|
||||
|
||||
import { MeasureVectorLayer } from './MeasureVectorLayer';
|
||||
|
||||
type Props = {
|
||||
map: Map;
|
||||
menuActiveState: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const MeasureOverlay = ({ map, menuActiveState }: Props) => {
|
||||
const vector = useRef(new MeasureVectorLayer());
|
||||
const measureStyle = getStyles(config.theme2);
|
||||
|
||||
// Menu State Management
|
||||
const [firstLoad, setFirstLoad] = useState<boolean>(true);
|
||||
const [menuActive, setMenuActive] = useState<boolean>(false);
|
||||
|
||||
// Options State
|
||||
const [options, setOptions] = useState<MapMeasureOptions>({
|
||||
action: measures[0].value!,
|
||||
unit: measures[0].units[0].value!,
|
||||
});
|
||||
const unit = useMemo(() => {
|
||||
const action = measures.find((m: MapMeasure) => m.value === options.action) ?? measures[0];
|
||||
const current = action.getUnit(options.unit);
|
||||
vector.current.setOptions(options);
|
||||
return {
|
||||
current,
|
||||
options: action.units,
|
||||
};
|
||||
}, [options]);
|
||||
|
||||
const clearPrevious = true;
|
||||
const showSegments = false;
|
||||
|
||||
function toggleMenu() {
|
||||
setMenuActive(!menuActive);
|
||||
// Lift menu state
|
||||
// TODO: consolidate into one state
|
||||
menuActiveState(!menuActive);
|
||||
if (menuActive) {
|
||||
map.removeInteraction(vector.current.draw);
|
||||
vector.current.setVisible(false);
|
||||
} else {
|
||||
if (firstLoad) {
|
||||
// Initialize on first load
|
||||
setFirstLoad(false);
|
||||
vector.current.setZIndex(1);
|
||||
map.addLayer(vector.current);
|
||||
map.addInteraction(vector.current.modify);
|
||||
}
|
||||
vector.current.setVisible(true);
|
||||
map.removeInteraction(vector.current.draw); // Remove last interaction
|
||||
const a = measures.find((v: MapMeasure) => v.value === options.action) ?? measures[0];
|
||||
vector.current.addInteraction(map, a.geometry, showSegments, clearPrevious);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${measureStyle.infoWrap} ${!menuActive ? measureStyle.infoWrapClosed : null}`}>
|
||||
{menuActive ? (
|
||||
<div>
|
||||
<div className={measureStyle.rowGroup}>
|
||||
<RadioButtonGroup
|
||||
value={options.action}
|
||||
options={measures}
|
||||
size="md"
|
||||
fullWidth={false}
|
||||
onChange={(e: string) => {
|
||||
map.removeInteraction(vector.current.draw);
|
||||
const m = measures.find((v: MapMeasure) => v.value === e) ?? measures[0];
|
||||
const unit = m.getUnit(options.unit);
|
||||
setOptions({ ...options, action: m.value!, unit: unit.value! });
|
||||
vector.current.addInteraction(map, m.geometry, showSegments, clearPrevious);
|
||||
}}
|
||||
/>
|
||||
<Button className={measureStyle.button} icon="times" variant="secondary" size="sm" onClick={toggleMenu} />
|
||||
</div>
|
||||
<Select
|
||||
className={measureStyle.unitSelect}
|
||||
value={unit.current}
|
||||
options={unit.options}
|
||||
isSearchable={false}
|
||||
onChange={(v: SelectableValue<string>) => {
|
||||
const a = measures.find((v: SelectableValue<string>) => v.value === options.action) ?? measures[0];
|
||||
const unit = a.getUnit(v.value) ?? a.units[0];
|
||||
setOptions({ ...options, unit: unit.value! });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<IconButton
|
||||
className={measureStyle.icon}
|
||||
name="ruler-combined"
|
||||
tooltip="show measure tools"
|
||||
tooltipPlacement="left"
|
||||
onClick={toggleMenu}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
|
||||
button: css`
|
||||
margin-left: auto;
|
||||
`,
|
||||
icon: css`
|
||||
background-color: ${theme.colors.secondary.main};
|
||||
display: inline-block;
|
||||
height: 19.25px;
|
||||
margin: 1px;
|
||||
width: 19.25px;
|
||||
`,
|
||||
infoWrap: css`
|
||||
color: ${theme.colors.text};
|
||||
background-color: ${theme.colors.background.secondary};
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
`,
|
||||
infoWrapClosed: css`
|
||||
height: 25.25px;
|
||||
width: 25.25px;
|
||||
`,
|
||||
rowGroup: css`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`,
|
||||
unitSelect: css`
|
||||
min-width: 200px;
|
||||
`,
|
||||
}));
|
||||
@@ -0,0 +1,255 @@
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import Map from 'ol/Map';
|
||||
import { Coordinate } from 'ol/coordinate';
|
||||
import { Geometry, LineString, Point, Polygon } from 'ol/geom';
|
||||
import { Type } from 'ol/geom/Geometry';
|
||||
import { Draw, Modify } from 'ol/interaction';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import VectorSource from 'ol/source/Vector';
|
||||
import { getArea, getLength } from 'ol/sphere';
|
||||
import { Circle as CircleStyle, Fill, RegularShape, Stroke, Style, Text } from 'ol/style';
|
||||
|
||||
import { formattedValueToString } from '@grafana/data';
|
||||
|
||||
import { MapMeasureOptions, measures } from '../utils/measure';
|
||||
|
||||
export class MeasureVectorLayer extends VectorLayer<VectorSource> {
|
||||
opts: MapMeasureOptions = {
|
||||
action: 'length',
|
||||
unit: 'm',
|
||||
};
|
||||
constructor() {
|
||||
super({
|
||||
source: new VectorSource(),
|
||||
});
|
||||
this.setStyle((feature) => {
|
||||
return this.styleFunction(feature, false);
|
||||
});
|
||||
this.setVisible(true);
|
||||
}
|
||||
|
||||
setOptions(options: MapMeasureOptions) {
|
||||
this.opts = options;
|
||||
this.getSource()?.refresh();
|
||||
}
|
||||
|
||||
getMapMeasurement(geo: Geometry): string {
|
||||
let v = 0;
|
||||
let action = measures[0];
|
||||
if (this.opts.action === 'area') {
|
||||
action = measures[1];
|
||||
v = getArea(geo);
|
||||
} else {
|
||||
v = getLength(geo);
|
||||
}
|
||||
return formattedValueToString(action.getUnit(this.opts.unit).format(v));
|
||||
}
|
||||
|
||||
segmentStyle = new Style({
|
||||
text: new Text({
|
||||
font: '12px Calibri,sans-serif',
|
||||
fill: new Fill({
|
||||
color: 'rgba(255, 255, 255, 1)',
|
||||
}),
|
||||
backgroundFill: new Fill({
|
||||
color: 'rgba(0, 0, 0, 0.4)',
|
||||
}),
|
||||
padding: [2, 2, 2, 2],
|
||||
textBaseline: 'bottom',
|
||||
offsetY: -12,
|
||||
}),
|
||||
image: new RegularShape({
|
||||
radius: 6,
|
||||
points: 3,
|
||||
angle: Math.PI,
|
||||
displacement: [0, 8],
|
||||
fill: new Fill({
|
||||
color: 'rgba(0, 0, 0, 0.4)',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
segmentStyles = [this.segmentStyle];
|
||||
|
||||
// Open Layer styles
|
||||
shapeStyle = [
|
||||
new Style({
|
||||
fill: new Fill({
|
||||
color: 'rgba(255, 255, 255, 0.2)',
|
||||
}),
|
||||
image: new CircleStyle({
|
||||
radius: 5,
|
||||
stroke: new Stroke({
|
||||
color: 'rgba(0, 0, 0, 0.7)',
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: 'rgba(255, 255, 255, 0.2)',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: [0, 0, 0, 1],
|
||||
width: 2,
|
||||
lineDash: [4, 8],
|
||||
lineDashOffset: 6,
|
||||
}),
|
||||
}),
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: [255, 255, 255, 1],
|
||||
width: 2,
|
||||
lineDash: [4, 8],
|
||||
}),
|
||||
}),
|
||||
];
|
||||
|
||||
labelStyle = new Style({
|
||||
text: new Text({
|
||||
font: '14px Calibri,sans-serif',
|
||||
fill: new Fill({
|
||||
color: 'rgba(255, 255, 255, 1)',
|
||||
}),
|
||||
backgroundFill: new Fill({
|
||||
color: 'rgba(0, 0, 0, 0.7)',
|
||||
}),
|
||||
padding: [3, 3, 3, 3],
|
||||
textBaseline: 'bottom',
|
||||
offsetY: -15,
|
||||
}),
|
||||
image: new RegularShape({
|
||||
radius: 8,
|
||||
points: 3,
|
||||
angle: Math.PI,
|
||||
displacement: [0, 10],
|
||||
fill: new Fill({
|
||||
color: 'rgba(0, 0, 0, 0.7)',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
tipStyle = new Style({
|
||||
text: new Text({
|
||||
font: '12px Calibri,sans-serif',
|
||||
fill: new Fill({
|
||||
color: 'rgba(255, 255, 255, 1)',
|
||||
}),
|
||||
backgroundFill: new Fill({
|
||||
color: 'rgba(0, 0, 0, 0.4)',
|
||||
}),
|
||||
padding: [2, 2, 2, 2],
|
||||
textAlign: 'left',
|
||||
offsetX: 15,
|
||||
}),
|
||||
});
|
||||
|
||||
modifyStyle = new Style({
|
||||
image: new CircleStyle({
|
||||
radius: 5,
|
||||
stroke: new Stroke({
|
||||
color: 'rgba(0, 0, 0, 0.7)',
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: 'rgba(0, 0, 0, 0.4)',
|
||||
}),
|
||||
}),
|
||||
text: new Text({
|
||||
text: 'Drag to modify',
|
||||
font: '12px Calibri,sans-serif',
|
||||
fill: new Fill({
|
||||
color: 'rgba(255, 255, 255, 1)',
|
||||
}),
|
||||
backgroundFill: new Fill({
|
||||
color: 'rgba(0, 0, 0, 0.7)',
|
||||
}),
|
||||
padding: [2, 2, 2, 2],
|
||||
textAlign: 'left',
|
||||
offsetX: 15,
|
||||
}),
|
||||
});
|
||||
|
||||
readonly modify = new Modify({ source: this.getSource()!, style: this.modifyStyle });
|
||||
tipPoint!: Geometry;
|
||||
draw!: Draw; // global so we can remove it later
|
||||
|
||||
styleFunction(feature: FeatureLike, segments: boolean, drawType?: string, tip?: string): Style[] {
|
||||
const styles = [...this.shapeStyle];
|
||||
const geometry = feature.getGeometry() as Geometry;
|
||||
if (geometry) {
|
||||
const type = geometry.getType();
|
||||
let point: Point;
|
||||
let label: string;
|
||||
let line: LineString;
|
||||
if (!drawType || drawType === type) {
|
||||
if (type === 'Polygon') {
|
||||
const poly = geometry as Polygon;
|
||||
point = poly.getInteriorPoint();
|
||||
label = this.getMapMeasurement(geometry);
|
||||
line = new LineString(poly.getCoordinates()[0]);
|
||||
} else if (type === 'LineString') {
|
||||
line = geometry as LineString;
|
||||
point = new Point(line.getLastCoordinate());
|
||||
label = this.getMapMeasurement(geometry);
|
||||
}
|
||||
}
|
||||
if (segments && line!) {
|
||||
let count = 0;
|
||||
line.forEachSegment((a: Coordinate, b: Coordinate) => {
|
||||
const segment = new LineString([a, b]);
|
||||
const label = this.getMapMeasurement(segment);
|
||||
if (this.segmentStyles.length - 1 < count) {
|
||||
this.segmentStyles.push(this.segmentStyle.clone());
|
||||
}
|
||||
const segmentPoint = new Point(segment.getCoordinateAt(0.5));
|
||||
this.segmentStyles[count].setGeometry(segmentPoint);
|
||||
this.segmentStyles[count].getText().setText(label);
|
||||
styles.push(this.segmentStyles[count]);
|
||||
count++;
|
||||
});
|
||||
}
|
||||
if (label!) {
|
||||
this.labelStyle.setGeometry(point!);
|
||||
this.labelStyle.getText().setText(label);
|
||||
styles.push(this.labelStyle);
|
||||
}
|
||||
if (tip && type === 'Point' && !this.modify.getOverlay().getSource().getFeatures().length) {
|
||||
this.tipPoint = geometry;
|
||||
this.tipStyle.getText().setText(tip);
|
||||
styles.push(this.tipStyle);
|
||||
}
|
||||
}
|
||||
return styles;
|
||||
}
|
||||
|
||||
addInteraction(map: Map, typeSelect: Type, showSegments: boolean, clearPrevious: boolean) {
|
||||
const drawType = typeSelect;
|
||||
const activeTip =
|
||||
' Click to continue ' + (drawType === 'Polygon' ? 'polygon' : 'line') + ' \n (double-click to end) ';
|
||||
const idleTip = ' Click to start ';
|
||||
let tip = idleTip;
|
||||
this.draw = new Draw({
|
||||
source: this.getSource()!,
|
||||
type: drawType,
|
||||
style: (feature) => {
|
||||
return this.styleFunction(feature, showSegments, drawType, tip);
|
||||
},
|
||||
});
|
||||
this.draw.on('drawstart', () => {
|
||||
if (clearPrevious) {
|
||||
this.getSource()!.clear();
|
||||
}
|
||||
this.modify.setActive(false);
|
||||
tip = activeTip;
|
||||
});
|
||||
this.draw.on('drawend', () => {
|
||||
this.modifyStyle.setGeometry(this.tipPoint);
|
||||
this.modify.setActive(true);
|
||||
map.once('pointermove', () => {
|
||||
this.modifyStyle.setGeometry('');
|
||||
});
|
||||
tip = idleTip;
|
||||
});
|
||||
this.modify.setActive(true);
|
||||
map.addInteraction(this.draw);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ interface State<T> {
|
||||
subProps: T;
|
||||
}
|
||||
|
||||
export class ObservablePropsWrapper<T> extends Component<Props<T>, State<T>> {
|
||||
export class ObservablePropsWrapper<T extends {}> extends Component<Props<T>, State<T>> {
|
||||
sub?: Unsubscribable;
|
||||
|
||||
constructor(props: Props<T>) {
|
||||
@@ -24,15 +24,10 @@ export class ObservablePropsWrapper<T> extends Component<Props<T>, State<T>> {
|
||||
componentDidMount() {
|
||||
this.sub = this.props.watch.subscribe({
|
||||
next: (subProps: T) => {
|
||||
//console.log('ObservablePropsWrapper:NEXT', subProps);
|
||||
this.setState({ subProps });
|
||||
},
|
||||
complete: () => {
|
||||
//console.log('ObservablePropsWrapper:complete');
|
||||
},
|
||||
error: (err) => {
|
||||
//console.log('ObservablePropsWrapper:error', err);
|
||||
},
|
||||
complete: () => {},
|
||||
error: (err) => {},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { FeatureStyleConfig } from '../types';
|
||||
|
||||
import { StyleRuleEditor, StyleRuleEditorSettings } from './StyleRuleEditor';
|
||||
|
||||
export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[], any, any>> = (props) => {
|
||||
export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[], unknown, unknown>> = (props) => {
|
||||
const { value, onChange, context, item } = props;
|
||||
const theme = useTheme2();
|
||||
|
||||
@@ -44,7 +44,7 @@ export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[]
|
||||
value.map((style, idx: number) => {
|
||||
const itemSettings: StandardEditorsRegistryItem<any, StyleRuleEditorSettings> = {
|
||||
settings,
|
||||
} as any;
|
||||
} as StandardEditorsRegistryItem;
|
||||
|
||||
return (
|
||||
<StyleRuleEditor
|
||||
|
||||
@@ -6,11 +6,10 @@ import { Container } from '@grafana/ui';
|
||||
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
|
||||
import { LayerDragDropList } from 'app/core/components/Layers/LayerDragDropList';
|
||||
|
||||
import { GeomapInstanceState } from '../GeomapPanel';
|
||||
import { getLayersOptions } from '../layers/registry';
|
||||
import { GeomapPanelOptions, MapLayerState } from '../types';
|
||||
import { GeomapPanelOptions, MapLayerState, GeomapInstanceState } from '../types';
|
||||
|
||||
type LayersEditorProps = StandardEditorProps<any, any, GeomapPanelOptions, GeomapInstanceState>;
|
||||
type LayersEditorProps = StandardEditorProps<unknown, unknown, GeomapPanelOptions, GeomapInstanceState>;
|
||||
|
||||
export const LayersEditor = (props: LayersEditorProps) => {
|
||||
const { layers, selected, actions } = props.context.instanceState ?? {};
|
||||
@@ -36,19 +35,19 @@ export const LayersEditor = (props: LayersEditorProps) => {
|
||||
actions.reorder(src, dst);
|
||||
};
|
||||
|
||||
const onSelect = (element: MapLayerState<any>) => {
|
||||
const onSelect = (element: MapLayerState<unknown>) => {
|
||||
actions.selectLayer(element.options.name);
|
||||
};
|
||||
|
||||
const onDelete = (element: MapLayerState<any>) => {
|
||||
const onDelete = (element: MapLayerState<unknown>) => {
|
||||
actions.deleteLayer(element.options.name);
|
||||
};
|
||||
|
||||
const getLayerInfo = (element: MapLayerState<any>) => {
|
||||
const getLayerInfo = (element: MapLayerState<unknown>) => {
|
||||
return element.options.type;
|
||||
};
|
||||
|
||||
const onNameChange = (element: MapLayerState<any>, name: string) => {
|
||||
const onNameChange = (element: MapLayerState<unknown>, name: string) => {
|
||||
element.onChange({ ...element.options, name });
|
||||
};
|
||||
|
||||
|
||||
@@ -5,15 +5,12 @@ import { StandardEditorProps, SelectableValue } from '@grafana/data';
|
||||
import { Button, InlineField, InlineFieldRow, Select, VerticalGroup } from '@grafana/ui';
|
||||
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
|
||||
|
||||
import { GeomapInstanceState } from '../GeomapPanel';
|
||||
import { GeomapPanelOptions, MapViewConfig } from '../types';
|
||||
import { GeomapPanelOptions, MapViewConfig, GeomapInstanceState } from '../types';
|
||||
import { centerPointRegistry, MapCenterID } from '../view';
|
||||
|
||||
export const MapViewEditor: FC<StandardEditorProps<MapViewConfig, any, GeomapPanelOptions, GeomapInstanceState>> = ({
|
||||
value,
|
||||
onChange,
|
||||
context,
|
||||
}) => {
|
||||
export const MapViewEditor: FC<
|
||||
StandardEditorProps<MapViewConfig, unknown, GeomapPanelOptions, GeomapInstanceState>
|
||||
> = ({ value, onChange, context }) => {
|
||||
const labelWidth = 10;
|
||||
|
||||
const views = useMemo(() => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { FC } from 'react';
|
||||
import { useObservable } from 'react-use';
|
||||
import { Observable, of } from 'rxjs';
|
||||
|
||||
import { StandardEditorProps } from '@grafana/data';
|
||||
import { FieldConfigPropertyItem, StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
|
||||
import {
|
||||
ColorPicker,
|
||||
Field,
|
||||
@@ -86,12 +86,12 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
onChange({ ...value, textConfig: { ...value.textConfig, offsetY } });
|
||||
};
|
||||
|
||||
const onTextAlignChange = (textAlign: unknown) => {
|
||||
onChange({ ...value, textConfig: { ...value.textConfig, textAlign: textAlign as TextAlignment } });
|
||||
const onTextAlignChange = (textAlign: TextAlignment) => {
|
||||
onChange({ ...value, textConfig: { ...value.textConfig, textAlign: textAlign } });
|
||||
};
|
||||
|
||||
const onTextBaselineChange = (textBaseline: unknown) => {
|
||||
onChange({ ...value, textConfig: { ...value.textConfig, textBaseline: textBaseline as TextBaseline } });
|
||||
const onTextBaselineChange = (textBaseline: TextBaseline) => {
|
||||
onChange({ ...value, textConfig: { ...value.textConfig, textBaseline: textBaseline } });
|
||||
};
|
||||
|
||||
const propertyOptions = useObservable(settings?.layerInfo ?? of());
|
||||
@@ -119,7 +119,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
placeholderValue: defaultStyleConfig.symbol.fixed,
|
||||
showSourceRadio: false,
|
||||
},
|
||||
} as any
|
||||
} as StandardEditorsRegistryItem
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
@@ -135,7 +135,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
min: defaultStyleConfig.rotation.min,
|
||||
max: defaultStyleConfig.rotation.max,
|
||||
},
|
||||
} as any
|
||||
} as StandardEditorsRegistryItem
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
@@ -166,7 +166,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
},
|
||||
} as any
|
||||
} as FieldConfigPropertyItem
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
@@ -188,7 +188,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
min: 1,
|
||||
max: 100,
|
||||
},
|
||||
} as any
|
||||
} as StandardEditorsRegistryItem
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
@@ -206,7 +206,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
placeholderValue: defaultStyleConfig.symbol.fixed,
|
||||
showSourceRadio: false,
|
||||
},
|
||||
} as any
|
||||
} as StandardEditorsRegistryItem
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
@@ -215,7 +215,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
value={value?.color ?? defaultStyleConfig.color}
|
||||
context={context}
|
||||
onChange={onColorChange}
|
||||
item={{} as any}
|
||||
item={{} as StandardEditorsRegistryItem}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={'Fill opacity'}>
|
||||
@@ -230,7 +230,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
},
|
||||
} as any
|
||||
} as FieldConfigPropertyItem
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
@@ -246,7 +246,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
min: defaultStyleConfig.rotation.min,
|
||||
max: defaultStyleConfig.rotation.max,
|
||||
},
|
||||
} as any
|
||||
} as StandardEditorsRegistryItem
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
@@ -256,7 +256,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
value={value?.text ?? defaultTextConfig}
|
||||
context={context}
|
||||
onChange={onTextChange}
|
||||
item={{} as any}
|
||||
item={{} as StandardEditorsRegistryItem}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -268,7 +268,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
value={value?.textConfig?.fontSize ?? defaultStyleConfig.textConfig.fontSize}
|
||||
context={context}
|
||||
onChange={onTextFontSizeChange}
|
||||
item={{} as any}
|
||||
item={{} as FieldConfigPropertyItem}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={'X offset'}>
|
||||
@@ -276,7 +276,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
value={value?.textConfig?.offsetX ?? defaultStyleConfig.textConfig.offsetX}
|
||||
context={context}
|
||||
onChange={onTextOffsetXChange}
|
||||
item={{} as any}
|
||||
item={{} as FieldConfigPropertyItem}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={'Y offset'}>
|
||||
@@ -284,7 +284,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
value={value?.textConfig?.offsetY ?? defaultStyleConfig.textConfig.offsetY}
|
||||
context={context}
|
||||
onChange={onTextOffsetYChange}
|
||||
item={{} as any}
|
||||
item={{} as FieldConfigPropertyItem}
|
||||
/>
|
||||
</Field>
|
||||
</HorizontalGroup>
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { FC, useCallback, useMemo } from 'react';
|
||||
import { useObservable } from 'react-use';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||
import { GrafanaTheme2, SelectableValue, StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
|
||||
import { Button, InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui';
|
||||
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
|
||||
|
||||
@@ -30,7 +30,7 @@ const comparators = [
|
||||
{ label: '<=', value: ComparisonOperation.LTE },
|
||||
];
|
||||
|
||||
export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, any, StyleRuleEditorSettings>> = (
|
||||
export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, unknown, StyleRuleEditorSettings>> = (
|
||||
props
|
||||
) => {
|
||||
const { value, onChange, item, context } = props;
|
||||
@@ -199,7 +199,7 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
||||
simpleFixedValues: true,
|
||||
layerInfo,
|
||||
},
|
||||
} as any
|
||||
} as StandardEditorsRegistryItem
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -27,13 +27,12 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<Map
|
||||
return { ...parent, options: opts.state.options, instanceState: opts.state };
|
||||
},
|
||||
getValue: (path: string) => lodashGet(opts.state.options, path),
|
||||
onChange: (path: string, value: any) => {
|
||||
onChange: (path: string, value: string) => {
|
||||
const { state } = opts;
|
||||
const { options } = state;
|
||||
if (path === 'type' && value) {
|
||||
const layer = geomapLayerRegistry.getIfExists(value);
|
||||
if (layer) {
|
||||
console.log('Change layer type:', value, state);
|
||||
const opts = {
|
||||
...options, // keep current shared options
|
||||
type: layer.id,
|
||||
@@ -55,7 +54,6 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<Map
|
||||
}),
|
||||
build: (builder, context) => {
|
||||
if (!opts.state) {
|
||||
console.log('MISSING LAYER!!!', opts);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,10 +56,10 @@ export function getGlobalStyles(theme: GrafanaTheme2) {
|
||||
color: ${theme.colors.text.primary}; // #eee;
|
||||
}
|
||||
.ol-control {
|
||||
background-color: ${theme.colors.background.secondary}; //rgba(255,255,255,0.4);
|
||||
background-color: ${theme.colors.background.primary}; //rgba(255,255,255,0.4);
|
||||
}
|
||||
.ol-control:hover {
|
||||
background-color: ${theme.colors.action.hover}; // rgba(255,255,255,0.6);
|
||||
background-color: ${theme.colors.background.secondary}; // rgba(255,255,255,0.6);
|
||||
}
|
||||
.ol-control button {
|
||||
color: ${theme.colors.secondary.text}; // white;
|
||||
|
||||
@@ -4,12 +4,12 @@ import { PanelPlugin } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { commonOptionsBuilder } from '@grafana/ui';
|
||||
|
||||
import { GeomapInstanceState, GeomapPanel } from './GeomapPanel';
|
||||
import { GeomapPanel } from './GeomapPanel';
|
||||
import { LayersEditor } from './editor/LayersEditor';
|
||||
import { MapViewEditor } from './editor/MapViewEditor';
|
||||
import { getLayerEditor } from './editor/layerEditor';
|
||||
import { mapPanelChangedHandler, mapMigrationHandler } from './migrations';
|
||||
import { defaultView, GeomapPanelOptions, TooltipMode } from './types';
|
||||
import { defaultView, GeomapPanelOptions, TooltipMode, GeomapInstanceState } from './types';
|
||||
|
||||
export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
||||
.setNoPadding()
|
||||
@@ -40,12 +40,15 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
||||
defaultValue: defaultView.shared,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line
|
||||
const state = context.instanceState as GeomapInstanceState;
|
||||
if (!state?.layers) {
|
||||
// TODO? show spinner?
|
||||
} else {
|
||||
const layersCategory = ['Map layers'];
|
||||
const basemapCategory = ['Basemap layer'];
|
||||
builder.addCustomEditor({
|
||||
category: ['Data layer'],
|
||||
category: layersCategory,
|
||||
id: 'layers',
|
||||
path: '',
|
||||
name: '',
|
||||
@@ -57,7 +60,7 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
||||
builder.addNestedOptions(
|
||||
getLayerEditor({
|
||||
state: selected,
|
||||
category: ['Data layer'],
|
||||
category: layersCategory,
|
||||
basemaps: false,
|
||||
})
|
||||
);
|
||||
@@ -66,18 +69,18 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
||||
const baselayer = state.layers[0];
|
||||
if (config.geomapDisableCustomBaseLayer) {
|
||||
builder.addCustomEditor({
|
||||
category: ['Base layer'],
|
||||
category: basemapCategory,
|
||||
id: 'layers',
|
||||
path: '',
|
||||
name: '',
|
||||
// eslint-disable-next-line react/display-name
|
||||
editor: () => <div>The base layer is configured by the server admin.</div>,
|
||||
editor: () => <div>The basemap layer is configured by the server admin.</div>,
|
||||
});
|
||||
} else if (baselayer) {
|
||||
builder.addNestedOptions(
|
||||
getLayerEditor({
|
||||
state: baselayer,
|
||||
category: ['Base layer'],
|
||||
category: basemapCategory,
|
||||
basemaps: true,
|
||||
})
|
||||
);
|
||||
@@ -115,6 +118,13 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
||||
description: 'Indicate map scale',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
category,
|
||||
path: 'controls.showMeasure',
|
||||
name: 'Show measure tools',
|
||||
description: 'Show tools for making measurements on the map',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
category,
|
||||
path: 'controls.showDebug',
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"type": "panel",
|
||||
"name": "Geomap",
|
||||
"id": "geomap",
|
||||
"state": "beta",
|
||||
|
||||
"info": {
|
||||
"description": "Geomap panel",
|
||||
|
||||
@@ -267,7 +267,7 @@ async function prepareSVG(url: string, size?: number): Promise<string> {
|
||||
return `data:image/svg+xml,${svgURI}`;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Map as OpenLayersMap } from 'ol';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import BaseLayer from 'ol/layer/Base';
|
||||
import Units from 'ol/proj/Units';
|
||||
@@ -26,6 +27,9 @@ export interface ControlsOptions {
|
||||
|
||||
// Show debug
|
||||
showDebug?: boolean;
|
||||
|
||||
// Show measure
|
||||
showMeasure?: boolean;
|
||||
}
|
||||
|
||||
export enum TooltipMode {
|
||||
@@ -66,16 +70,33 @@ export interface GeomapPanelOptions {
|
||||
layers: MapLayerOptions[];
|
||||
tooltip: TooltipOptions;
|
||||
}
|
||||
|
||||
export interface FeatureStyleConfig {
|
||||
style?: StyleConfig;
|
||||
check?: FeatureRuleConfig;
|
||||
}
|
||||
|
||||
export interface FeatureRuleConfig {
|
||||
property: string;
|
||||
operation: ComparisonOperation;
|
||||
value: string | boolean | number;
|
||||
}
|
||||
|
||||
export interface GeomapLayerActions {
|
||||
selectLayer: (uid: string) => void;
|
||||
deleteLayer: (uid: string) => void;
|
||||
addlayer: (type: string) => void;
|
||||
reorder: (src: number, dst: number) => void;
|
||||
canRename: (v: string) => boolean;
|
||||
}
|
||||
|
||||
export interface GeomapInstanceState {
|
||||
map?: OpenLayersMap;
|
||||
layers: MapLayerState[];
|
||||
selected: number;
|
||||
actions: GeomapLayerActions;
|
||||
}
|
||||
|
||||
export enum ComparisonOperation {
|
||||
EQ = 'eq',
|
||||
NEQ = 'neq',
|
||||
@@ -88,7 +109,7 @@ export enum ComparisonOperation {
|
||||
//-------------------
|
||||
// Runtime model
|
||||
//-------------------
|
||||
export interface MapLayerState<TConfig = any> extends LayerElement {
|
||||
export interface MapLayerState<TConfig = unknown> extends LayerElement {
|
||||
options: MapLayerOptions<TConfig>;
|
||||
handler: MapLayerHandler;
|
||||
layer: BaseLayer; // the openlayers instance
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { FrameGeometrySourceMode } from '@grafana/data/src';
|
||||
|
||||
import { GeomapPanel } from '../GeomapPanel';
|
||||
import { geomapLayerRegistry } from '../layers/registry';
|
||||
import { GeomapLayerActions, MapLayerState } from '../types';
|
||||
|
||||
import { initLayer } from './layers';
|
||||
import { getNextLayerName } from './utils';
|
||||
|
||||
export const getActions = (panel: GeomapPanel) => {
|
||||
const actions: GeomapLayerActions = {
|
||||
selectLayer: (uid: string) => {
|
||||
const selected = panel.layers.findIndex((v) => v.options.name === uid);
|
||||
if (panel.panelContext && panel.panelContext.onInstanceStateChange) {
|
||||
panel.panelContext.onInstanceStateChange({
|
||||
map: panel.map,
|
||||
layers: panel.layers,
|
||||
selected,
|
||||
actions: panel.actions,
|
||||
});
|
||||
}
|
||||
},
|
||||
canRename: (v: string) => {
|
||||
return !panel.byName.has(v);
|
||||
},
|
||||
deleteLayer: (uid: string) => {
|
||||
const layers: MapLayerState[] = [];
|
||||
for (const lyr of panel.layers) {
|
||||
if (lyr.options.name === uid) {
|
||||
panel.map?.removeLayer(lyr.layer);
|
||||
} else {
|
||||
layers.push(lyr);
|
||||
}
|
||||
}
|
||||
panel.layers = layers;
|
||||
panel.doOptionsUpdate(0);
|
||||
},
|
||||
addlayer: (type: string) => {
|
||||
const item = geomapLayerRegistry.getIfExists(type);
|
||||
if (!item) {
|
||||
return; // ignore empty request
|
||||
}
|
||||
initLayer(
|
||||
panel,
|
||||
panel.map!,
|
||||
{
|
||||
type: item.id,
|
||||
name: getNextLayerName(panel),
|
||||
config: cloneDeep(item.defaultOptions),
|
||||
location: item.showLocation ? { mode: FrameGeometrySourceMode.Auto } : undefined,
|
||||
tooltip: true,
|
||||
},
|
||||
false
|
||||
).then((lyr) => {
|
||||
panel.layers = panel.layers.slice(0);
|
||||
panel.layers.push(lyr);
|
||||
panel.map?.addLayer(lyr.layer);
|
||||
|
||||
panel.doOptionsUpdate(panel.layers.length - 1);
|
||||
});
|
||||
},
|
||||
reorder: (startIndex: number, endIndex: number) => {
|
||||
const result = Array.from(panel.layers);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
result.splice(endIndex, 0, removed);
|
||||
panel.layers = result;
|
||||
|
||||
panel.doOptionsUpdate(endIndex);
|
||||
|
||||
// Add the layers in the right order
|
||||
const group = panel.map?.getLayers()!;
|
||||
group.clear();
|
||||
panel.layers.forEach((v) => group.push(v.layer));
|
||||
},
|
||||
};
|
||||
|
||||
return actions;
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
import { PluggableMap } from 'ol';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { getFrameMatchers, MapLayerHandler, MapLayerOptions, PanelData } from '@grafana/data/src';
|
||||
import { config } from '@grafana/runtime/src';
|
||||
|
||||
import { GeomapPanel } from '../GeomapPanel';
|
||||
import { MARKERS_LAYER_ID } from '../layers/data/markersLayer';
|
||||
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from '../layers/registry';
|
||||
import { MapLayerState } from '../types';
|
||||
|
||||
import { getNextLayerName } from './utils';
|
||||
|
||||
export const applyLayerFilter = (
|
||||
handler: MapLayerHandler<unknown>,
|
||||
options: MapLayerOptions<unknown>,
|
||||
panelDataProps: PanelData
|
||||
): void => {
|
||||
if (handler.update) {
|
||||
let panelData = panelDataProps;
|
||||
if (options.filterData) {
|
||||
const matcherFunc = getFrameMatchers(options.filterData);
|
||||
panelData = {
|
||||
...panelData,
|
||||
series: panelData.series.filter(matcherFunc),
|
||||
};
|
||||
}
|
||||
handler.update(panelData);
|
||||
}
|
||||
};
|
||||
|
||||
export async function updateLayer(panel: GeomapPanel, uid: string, newOptions: MapLayerOptions): Promise<boolean> {
|
||||
if (!panel.map) {
|
||||
return false;
|
||||
}
|
||||
const current = panel.byName.get(uid);
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let layerIndex = -1;
|
||||
const group = panel.map?.getLayers()!;
|
||||
for (let i = 0; i < group?.getLength(); i++) {
|
||||
if (group.item(i) === current.layer) {
|
||||
layerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for rename
|
||||
if (newOptions.name !== uid) {
|
||||
if (!newOptions.name) {
|
||||
newOptions.name = uid;
|
||||
} else if (panel.byName.has(newOptions.name)) {
|
||||
return false;
|
||||
}
|
||||
panel.byName.delete(uid);
|
||||
|
||||
uid = newOptions.name;
|
||||
panel.byName.set(uid, current);
|
||||
}
|
||||
|
||||
// Type changed -- requires full re-initalization
|
||||
if (current.options.type !== newOptions.type) {
|
||||
// full init
|
||||
} else {
|
||||
// just update options
|
||||
}
|
||||
|
||||
const layers = panel.layers.slice(0);
|
||||
try {
|
||||
const info = await initLayer(panel, panel.map, newOptions, current.isBasemap);
|
||||
layers[layerIndex]?.handler.dispose?.();
|
||||
layers[layerIndex] = info;
|
||||
group.setAt(layerIndex, info.layer);
|
||||
|
||||
// initialize with new data
|
||||
applyLayerFilter(info.handler, newOptions, panel.props.data);
|
||||
} catch (err) {
|
||||
console.warn('ERROR', err); // eslint-disable-line no-console
|
||||
return false;
|
||||
}
|
||||
|
||||
// Just to trigger a state update
|
||||
panel.setState({ legends: [] });
|
||||
|
||||
panel.layers = layers;
|
||||
panel.doOptionsUpdate(layerIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function initLayer(
|
||||
panel: GeomapPanel,
|
||||
map: PluggableMap,
|
||||
options: MapLayerOptions,
|
||||
isBasemap?: boolean
|
||||
): Promise<MapLayerState> {
|
||||
if (isBasemap && (!options?.type || config.geomapDisableCustomBaseLayer)) {
|
||||
options = DEFAULT_BASEMAP_CONFIG;
|
||||
}
|
||||
|
||||
// Use default makers layer
|
||||
if (!options?.type) {
|
||||
options = {
|
||||
type: MARKERS_LAYER_ID,
|
||||
name: getNextLayerName(panel),
|
||||
config: {},
|
||||
};
|
||||
}
|
||||
|
||||
const item = geomapLayerRegistry.getIfExists(options.type);
|
||||
if (!item) {
|
||||
return Promise.reject('unknown layer: ' + options.type);
|
||||
}
|
||||
|
||||
const handler = await item.create(map, options, panel.props.eventBus, config.theme2);
|
||||
const layer = handler.init(); // eslint-disable-line
|
||||
if (options.opacity != null) {
|
||||
layer.setOpacity(options.opacity);
|
||||
}
|
||||
|
||||
if (!options.name) {
|
||||
options.name = getNextLayerName(panel);
|
||||
}
|
||||
|
||||
const UID = options.name;
|
||||
const state: MapLayerState<unknown> = {
|
||||
// UID, // unique name when added to the map (it may change and will need special handling)
|
||||
isBasemap,
|
||||
options,
|
||||
layer,
|
||||
handler,
|
||||
mouseEvents: new Subject<FeatureLike | undefined>(),
|
||||
|
||||
getName: () => UID,
|
||||
|
||||
// Used by the editors
|
||||
onChange: (cfg: MapLayerOptions) => {
|
||||
updateLayer(panel, UID, cfg);
|
||||
},
|
||||
};
|
||||
|
||||
panel.byName.set(UID, state);
|
||||
// eslint-disable-next-line
|
||||
(state.layer as any).__state = state;
|
||||
|
||||
applyLayerFilter(handler, options, panel.props.data);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export const getMapLayerState = (l: any) => {
|
||||
return l?.__state as MapLayerState;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { measures } from './measure';
|
||||
|
||||
describe('get measure utils', () => {
|
||||
it('switch from length to area', () => {
|
||||
const length = measures[0];
|
||||
expect(length.value).toBe('length');
|
||||
expect(length.getUnit('ft').value).toBe('ft');
|
||||
expect(length.getUnit('ft2').value).toBe('ft');
|
||||
});
|
||||
it('switch from area to length', () => {
|
||||
const area = measures[1];
|
||||
expect(area.value).toBe('area');
|
||||
expect(area.getUnit('ft2').value).toBe('ft2');
|
||||
expect(area.getUnit('ft').value).toBe('ft2');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import { FormattedValue, getValueFormat, SelectableValue, toFixedUnit } from '@grafana/data';
|
||||
|
||||
type MeasureAction = 'area' | 'length';
|
||||
|
||||
export interface MapMeasureOptions {
|
||||
action: MeasureAction;
|
||||
unit: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface MapMeasure extends SelectableValue<MeasureAction> {
|
||||
geometry: 'Polygon' | 'LineString';
|
||||
units: MapUnit[];
|
||||
|
||||
/**
|
||||
* This will get the best unit for the selected string or a default
|
||||
* This is helpful when converting from area<>length, we should try to stay
|
||||
* in the same category of unit if possible
|
||||
*/
|
||||
getUnit: (v?: string) => MapUnit;
|
||||
}
|
||||
|
||||
export interface MapUnit extends SelectableValue<string> {
|
||||
format: (si: number) => FormattedValue;
|
||||
}
|
||||
|
||||
export const measures: MapMeasure[] = [
|
||||
{
|
||||
value: 'length',
|
||||
label: 'Length',
|
||||
geometry: 'LineString',
|
||||
units: [
|
||||
{
|
||||
label: 'Metric (m/km)',
|
||||
value: 'm',
|
||||
format: (m: number) => getValueFormat('lengthm')(m),
|
||||
},
|
||||
{
|
||||
label: 'Feet (ft)',
|
||||
value: 'ft',
|
||||
format: (m: number) => getValueFormat('lengthft')(m * 3.28084),
|
||||
},
|
||||
{
|
||||
label: 'Miles (mi)',
|
||||
value: 'mi',
|
||||
format: (m: number) => getValueFormat('lengthmi')(m / 1609.0),
|
||||
},
|
||||
{
|
||||
label: 'Nautical miles (nmi)',
|
||||
value: 'nmi',
|
||||
format: (m: number) => getValueFormat('nmi')(m / 1852.0),
|
||||
},
|
||||
],
|
||||
getUnit: (v?: string) => {
|
||||
const units = measures[0].units;
|
||||
if (v?.endsWith('2')) {
|
||||
v = v.substring(0, v.length - 1);
|
||||
}
|
||||
return units.find((u) => u.value === v) ?? units[0];
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'area',
|
||||
label: 'Area',
|
||||
geometry: 'Polygon',
|
||||
units: [
|
||||
{
|
||||
label: 'Square Meters (m²)',
|
||||
value: 'm2',
|
||||
format: (m2: number) => getValueFormat('areaM2')(m2),
|
||||
},
|
||||
{
|
||||
label: 'Square Kilometers (km²)',
|
||||
value: 'km2',
|
||||
format: (m2: number) => toFixedUnit('km²')(m2 * 1e-6),
|
||||
},
|
||||
{
|
||||
label: 'Square Feet (ft²)',
|
||||
value: 'ft2',
|
||||
format: (m2: number) => getValueFormat('areaF2')(m2 * 10.76391),
|
||||
},
|
||||
{
|
||||
label: 'Square Miles (mi²)',
|
||||
value: 'mi2',
|
||||
format: (m2: number) => getValueFormat('areaMI2')(m2 * 3.861e-7),
|
||||
},
|
||||
{
|
||||
label: 'Acres',
|
||||
value: 'acre2',
|
||||
format: (m2: number) => toFixedUnit('acre')(m2 * 2.47105e-4),
|
||||
},
|
||||
{
|
||||
label: 'Hectare',
|
||||
value: 'hectare2',
|
||||
format: (m2: number) => toFixedUnit('ha')(m2 * 1e-4),
|
||||
},
|
||||
],
|
||||
getUnit: (v?: string) => {
|
||||
const units = measures[1].units;
|
||||
if (!v?.endsWith('2')) {
|
||||
v += '2';
|
||||
}
|
||||
return units.find((u) => u.value === v) ?? units[0];
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,111 @@
|
||||
import { MapBrowserEvent } from 'ol';
|
||||
import { toLonLat } from 'ol/proj';
|
||||
|
||||
import { DataFrame, DataHoverClearEvent } from '@grafana/data/src';
|
||||
|
||||
import { GeomapPanel } from '../GeomapPanel';
|
||||
import { GeomapHoverPayload, GeomapLayerHover } from '../event';
|
||||
import { MapLayerState } from '../types';
|
||||
|
||||
import { getMapLayerState } from './layers';
|
||||
|
||||
export const setTooltipListeners = (panel: GeomapPanel) => {
|
||||
// Tooltip listener
|
||||
panel.map?.on('singleclick', panel.pointerClickListener);
|
||||
panel.map?.on('pointermove', panel.pointerMoveListener);
|
||||
panel.map?.getViewport().addEventListener('mouseout', (evt: MouseEvent) => {
|
||||
panel.props.eventBus.publish(new DataHoverClearEvent());
|
||||
});
|
||||
};
|
||||
|
||||
export const pointerClickListener = (evt: MapBrowserEvent<UIEvent>, panel: GeomapPanel) => {
|
||||
if (pointerMoveListener(evt, panel)) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
panel.mapDiv!.style.cursor = 'auto';
|
||||
panel.setState({ ttipOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
export const pointerMoveListener = (evt: MapBrowserEvent<UIEvent>, panel: GeomapPanel) => {
|
||||
// If measure menu is open, bypass tooltip logic and display measuring mouse events
|
||||
if (panel.state.measureMenuActive) {
|
||||
return true;
|
||||
}
|
||||
if (!panel.map || panel.state.ttipOpen) {
|
||||
return false;
|
||||
}
|
||||
const mouse = evt.originalEvent as MouseEvent; // eslint-disable-line
|
||||
const pixel = panel.map.getEventPixel(mouse);
|
||||
const hover = toLonLat(panel.map.getCoordinateFromPixel(pixel));
|
||||
|
||||
const { hoverPayload } = panel;
|
||||
hoverPayload.pageX = mouse.pageX;
|
||||
hoverPayload.pageY = mouse.pageY;
|
||||
hoverPayload.point = {
|
||||
lat: hover[1],
|
||||
lon: hover[0],
|
||||
};
|
||||
hoverPayload.data = undefined;
|
||||
hoverPayload.columnIndex = undefined;
|
||||
hoverPayload.rowIndex = undefined;
|
||||
hoverPayload.layers = undefined;
|
||||
|
||||
const layers: GeomapLayerHover[] = [];
|
||||
const layerLookup = new Map<MapLayerState, GeomapLayerHover>();
|
||||
|
||||
let ttip: GeomapHoverPayload = {} as GeomapHoverPayload;
|
||||
panel.map.forEachFeatureAtPixel(
|
||||
pixel,
|
||||
(feature, layer, geo) => {
|
||||
const s: MapLayerState = getMapLayerState(layer);
|
||||
//match hover layer to layer in layers
|
||||
//check if the layer show tooltip is enabled
|
||||
//then also pass the list of tooltip fields if exists
|
||||
//this is used as the generic hover event
|
||||
if (!hoverPayload.data) {
|
||||
const props = feature.getProperties();
|
||||
const frame = props['frame'];
|
||||
if (frame) {
|
||||
hoverPayload.data = ttip.data = frame as DataFrame;
|
||||
hoverPayload.rowIndex = ttip.rowIndex = props['rowIndex'];
|
||||
}
|
||||
|
||||
if (s?.mouseEvents) {
|
||||
s.mouseEvents.next(feature);
|
||||
}
|
||||
}
|
||||
|
||||
if (s) {
|
||||
let h = layerLookup.get(s);
|
||||
if (!h) {
|
||||
h = { layer: s, features: [] };
|
||||
layerLookup.set(s, h);
|
||||
layers.push(h);
|
||||
}
|
||||
h.features.push(feature);
|
||||
}
|
||||
},
|
||||
{
|
||||
layerFilter: (l) => {
|
||||
const hoverLayerState = getMapLayerState(l);
|
||||
return hoverLayerState?.options?.tooltip !== false;
|
||||
},
|
||||
}
|
||||
);
|
||||
panel.hoverPayload.layers = layers.length ? layers : undefined;
|
||||
panel.props.eventBus.publish(panel.hoverEvent);
|
||||
|
||||
panel.setState({ ttip: { ...hoverPayload } });
|
||||
|
||||
if (!layers.length) {
|
||||
// clear mouse events
|
||||
panel.layers.forEach((layer) => {
|
||||
layer.mouseEvents.next(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
const found = Boolean(layers.length);
|
||||
panel.mapDiv!.style.cursor = found ? 'pointer' : 'auto';
|
||||
return found;
|
||||
};
|
||||
@@ -1,9 +1,16 @@
|
||||
import { Map as OpenLayersMap } from 'ol';
|
||||
import { defaults as interactionDefaults } from 'ol/interaction';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data/src';
|
||||
import { getColorDimension, getScalarDimension, getScaledDimension, getTextDimension } from 'app/features/dimensions';
|
||||
import { getGrafanaDatasource } from 'app/plugins/datasource/grafana/datasource';
|
||||
|
||||
import { GeomapPanel } from '../GeomapPanel';
|
||||
import { defaultStyleConfig, StyleConfig, StyleConfigState, StyleDimensions } from '../style/types';
|
||||
import { GeomapPanelOptions, MapLayerState } from '../types';
|
||||
|
||||
import { initMapView } from './view';
|
||||
|
||||
export function getStyleDimension(
|
||||
frame: DataFrame | undefined,
|
||||
@@ -68,3 +75,46 @@ async function initGeojsonFiles() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const getNewOpenLayersMap = (panel: GeomapPanel, options: GeomapPanelOptions, div: HTMLDivElement) => {
|
||||
const [view] = initMapView(options.view, undefined, undefined);
|
||||
return (panel.map = new OpenLayersMap({
|
||||
view: view,
|
||||
pixelRatio: 1, // or zoom?
|
||||
layers: [], // loaded explicitly below
|
||||
controls: [],
|
||||
target: div,
|
||||
interactions: interactionDefaults({
|
||||
mouseWheelZoom: false, // managed by initControls
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
export const updateMap = (panel: GeomapPanel, options: GeomapPanelOptions) => {
|
||||
panel.initControls(options.controls);
|
||||
panel.forceUpdate(); // first render
|
||||
};
|
||||
|
||||
export const notifyPanelEditor = (geomapPanel: GeomapPanel, layers: MapLayerState[], selected: number) => {
|
||||
// Notify the panel editor
|
||||
if (geomapPanel.panelContext && geomapPanel.panelContext.onInstanceStateChange) {
|
||||
geomapPanel.panelContext.onInstanceStateChange({
|
||||
map: geomapPanel.map,
|
||||
layers: layers,
|
||||
selected: selected,
|
||||
actions: geomapPanel.actions,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getNextLayerName = (panel: GeomapPanel) => {
|
||||
let idx = panel.layers.length; // since basemap is 0, this looks right
|
||||
while (true && idx < 100) {
|
||||
const name = `Layer ${idx++}`;
|
||||
if (!panel.byName.has(name)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
return `Layer ${Date.now()}`;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { View, Collection } from 'ol';
|
||||
import { Coordinate } from 'ol/coordinate';
|
||||
import { isEmpty } from 'ol/extent';
|
||||
import BaseLayer from 'ol/layer/Base';
|
||||
import { fromLonLat } from 'ol/proj';
|
||||
|
||||
import { MapViewConfig } from '../types';
|
||||
import { centerPointRegistry, MapCenterID } from '../view';
|
||||
|
||||
import { getLayersExtent } from './getLayersExtent';
|
||||
|
||||
export const initViewExtent = (view: View, config: MapViewConfig, layers: Collection<BaseLayer>) => {
|
||||
const v = centerPointRegistry.getIfExists(config.id);
|
||||
if (v) {
|
||||
let coord: Coordinate | undefined = undefined;
|
||||
if (v.lat == null) {
|
||||
if (v.id === MapCenterID.Coordinates) {
|
||||
coord = [config.lon ?? 0, config.lat ?? 0];
|
||||
} else if (v.id === MapCenterID.Fit) {
|
||||
const extent = getLayersExtent(layers);
|
||||
if (!isEmpty(extent)) {
|
||||
view.fit(extent, {
|
||||
padding: [30, 30, 30, 30],
|
||||
maxZoom: config.zoom ?? config.maxZoom,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// TODO: view requires special handling
|
||||
}
|
||||
} else {
|
||||
coord = [v.lon ?? 0, v.lat ?? 0];
|
||||
}
|
||||
if (coord) {
|
||||
view.setCenter(fromLonLat(coord));
|
||||
}
|
||||
}
|
||||
|
||||
if (config.maxZoom) {
|
||||
view.setMaxZoom(config.maxZoom);
|
||||
}
|
||||
if (config.minZoom) {
|
||||
view.setMaxZoom(config.minZoom);
|
||||
}
|
||||
if (config.zoom && v?.id !== MapCenterID.Fit) {
|
||||
view.setZoom(config.zoom);
|
||||
}
|
||||
};
|
||||
|
||||
export const initMapView = (
|
||||
config: MapViewConfig,
|
||||
sharedView?: View | undefined,
|
||||
layers?: Collection<BaseLayer>
|
||||
): Array<View | undefined> => {
|
||||
let view = new View({
|
||||
center: [0, 0],
|
||||
zoom: 1,
|
||||
showFullExtent: true, // allows zooming so the full range is visible
|
||||
});
|
||||
|
||||
// With shared views, all panels use the same view instance
|
||||
if (config.shared) {
|
||||
if (!sharedView) {
|
||||
sharedView = view;
|
||||
} else {
|
||||
view = sharedView;
|
||||
}
|
||||
}
|
||||
if (layers) {
|
||||
initViewExtent(view, config, layers);
|
||||
}
|
||||
|
||||
return [sharedView, view];
|
||||
};
|
||||
Reference in New Issue
Block a user