From 25c13c55c0bb9e07ec6481e944bb114c0e1a88c5 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Thu, 21 Aug 2025 12:49:42 -0500 Subject: [PATCH] Canvas: Dynamic connection direction (#108423) --- .../canvas-connection-examples.json | 387 ++++++++++++++++-- .../visualizations/canvas/index.md | 9 +- .../grafana-schema/src/common/common.gen.ts | 29 +- .../grafana-schema/src/common/dimensions.cue | 13 +- .../panelcfg/x/CanvasPanelCfg_types.gen.ts | 1 + public/app/features/canvas/element.ts | 4 +- public/app/features/canvas/runtime/scene.tsx | 3 + public/app/features/dimensions/context.ts | 9 + public/app/features/dimensions/direction.ts | 58 +++ .../editors/DirectionDimensionEditor.tsx | 122 ++++++ public/app/features/dimensions/types.ts | 2 + public/app/features/dimensions/utils.ts | 18 + .../components/connections/ConnectionSVG.tsx | 14 +- .../components/connections/ConnectionSVG2.tsx | 14 +- .../plugins/panel/canvas/editor/options.ts | 21 +- .../plugins/panel/canvas/migrations.test.ts | 53 +++ public/app/plugins/panel/canvas/migrations.ts | 23 ++ public/app/plugins/panel/canvas/module.tsx | 5 +- public/app/plugins/panel/canvas/panelcfg.cue | 1 + .../app/plugins/panel/canvas/panelcfg.gen.ts | 1 + public/app/plugins/panel/canvas/utils.ts | 13 +- public/locales/en-US/grafana.json | 24 +- 22 files changed, 747 insertions(+), 77 deletions(-) create mode 100644 public/app/features/dimensions/direction.ts create mode 100644 public/app/features/dimensions/editors/DirectionDimensionEditor.tsx diff --git a/devenv/dev-dashboards/panel-canvas/canvas-connection-examples.json b/devenv/dev-dashboards/panel-canvas/canvas-connection-examples.json index 943ba719303..1689471193e 100644 --- a/devenv/dev-dashboards/panel-canvas/canvas-connection-examples.json +++ b/devenv/dev-dashboards/panel-canvas/canvas-connection-examples.json @@ -25,7 +25,6 @@ "fiscalYearStartMonth": 0, "graphTooltip": 0, "links": [], - "liveNow": false, "panels": [ { "datasource": { @@ -42,7 +41,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -156,11 +155,19 @@ "x": -0.8229007633587786, "y": 0.27741935483870966 }, + "sourceOriginal": { + "x": 1223.8335877862596, + "y": 300 + }, "target": { "x": 1.0112359550561798, "y": -0.012987012987012988 }, "targetName": "Element 20", + "targetOriginal": { + "x": 990, + "y": 474 + }, "vertices": [ { "x": -0.18181818181818182, @@ -191,11 +198,19 @@ "x": -0.3068702290076336, "y": -0.47419354838709676 }, + "sourceOriginal": { + "x": 1401.090076335878, + "y": 533 + }, "target": { "x": 1.0112359550561798, "y": -0.4805194805194805 }, "targetName": "Element 20", + "targetOriginal": { + "x": 990, + "y": 492 + }, "vertices": [ { "x": 0.415, @@ -227,11 +242,19 @@ "x": 0.7903930131004366, "y": -0.36935483870967745 }, + "sourceOriginal": { + "x": 1778, + "y": 500.5 + }, "target": { "x": 1.0120481927710843, "y": 0.024691358024691357 }, "targetName": "Element 35", + "targetOriginal": { + "x": 1523, + "y": 177 + }, "vertices": [ { "x": -0.1607843137254902, @@ -518,11 +541,19 @@ "x": 0.8876404494382022, "y": 0.2597402597402597 }, + "sourceOriginal": { + "x": 741, + "y": 356.5 + }, "target": { "x": -0.9775280898876404, "y": -0.012987012987012988 }, - "targetName": "Element 19" + "targetName": "Element 19", + "targetOriginal": { + "x": 813, + "y": 358 + } } ], "constraint": { @@ -614,11 +645,19 @@ "x": 0.8876404494382022, "y": 0.2597402597402597 }, + "sourceOriginal": { + "x": 741, + "y": 239.5 + }, "target": { "x": -0.9887640449438202, "y": -0.06493506493506493 }, - "targetName": "Element 18" + "targetName": "Element 18", + "targetOriginal": { + "x": 812, + "y": 242 + } } ], "constraint": { @@ -673,11 +712,19 @@ "x": -0.2247191011235955, "y": -0.4805194805194805 }, + "sourceOriginal": { + "x": 342, + "y": 395 + }, "target": { "x": 0.011235955056179775, "y": 0.922077922077922 }, - "targetName": "Element 12" + "targetName": "Element 12", + "targetOriginal": { + "x": 343, + "y": 458 + } }, { "color": { @@ -693,11 +740,19 @@ "x": 0.7752808988764045, "y": 0.5194805194805194 }, + "sourceOriginal": { + "x": 431, + "y": 356.5 + }, "target": { "x": -1.0833333333333333, "y": 0.020618556701030927 }, - "targetName": "Element 15" + "targetName": "Element 15", + "targetOriginal": { + "x": 526, + "y": 356 + } } ], "constraint": { @@ -888,11 +943,19 @@ "x": 1, "y": 0.5 }, + "sourceOriginal": { + "x": 431, + "y": 337.25 + }, "target": { "x": -1.0112359550561798, "y": -0.012987012987012988 }, "targetName": "Element 16", + "targetOriginal": { + "x": 562, + "y": 240 + }, "vertices": [ { "x": 0.42748091603053434, @@ -963,11 +1026,19 @@ "x": 0, "y": -1 }, + "sourceOriginal": { + "x": 150, + "y": 223 + }, "target": { "x": -0.5168539325842697, "y": 1 }, "targetName": "Element 6", + "targetOriginal": { + "x": 296, + "y": 318 + }, "vertices": [ { "x": 0, @@ -996,11 +1067,19 @@ "x": 0, "y": 1 }, + "sourceOriginal": { + "x": 150, + "y": 148 + }, "target": { "x": -1.0112359550561798, "y": -0.012987012987012988 }, "targetName": "Element 30", + "targetOriginal": { + "x": 563, + "y": 82 + }, "vertices": [ { "x": 0, @@ -1062,11 +1141,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 216, + "y": 356.5 + }, "target": { "x": -1.0112359550561798, "y": -0.012987012987012988 }, - "targetName": "Element 6" + "targetName": "Element 6", + "targetOriginal": { + "x": 252, + "y": 357 + } } ], "constraint": { @@ -1121,11 +1208,19 @@ "x": 0, "y": -1 }, + "sourceOriginal": { + "x": 342, + "y": 223 + }, "target": { "x": 0.011235955056179775, "y": 1.025974025974026 }, - "targetName": "Element 6" + "targetName": "Element 6", + "targetOriginal": { + "x": 343, + "y": 317 + } } ], "constraint": { @@ -1218,11 +1313,19 @@ "x": -1, "y": 0 }, + "sourceOriginal": { + "x": 38, + "y": 493.5 + }, "target": { "x": -1.0224719101123596, "y": 0.013333333333333334 }, "targetName": "Element 9", + "targetOriginal": { + "x": 59, + "y": 185 + }, "vertices": [ { "x": -0.9523809523809523, @@ -1399,11 +1502,19 @@ "x": -1, "y": 0 }, + "sourceOriginal": { + "x": 563, + "y": 473.5 + }, "target": { "x": 1.0224719101123596, "y": -0.45454545454545453 }, "targetName": "Element 6", + "targetOriginal": { + "x": 433, + "y": 374 + }, "vertices": [ { "x": 0.5692307692307692, @@ -1468,11 +1579,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 989, + "y": 239.5 + }, "target": { "x": -0.96, "y": 0.4583333333333333 }, "targetName": "Element 2", + "targetOriginal": { + "x": 1392, + "y": 487 + }, "vertices": [ { "x": 0.3349875930521092, @@ -1506,11 +1625,19 @@ "x": 1, "y": -0.5 }, + "sourceOriginal": { + "x": 989, + "y": 258.75 + }, "target": { "x": -0.9761904761904762, "y": 0.5263157894736842 }, "targetName": "Element 34", + "targetOriginal": { + "x": 1138, + "y": 290 + }, "vertices": [ { "x": 0.5033557046979866, @@ -1612,11 +1739,19 @@ "x": -1, "y": 0 }, + "sourceOriginal": { + "x": 811, + "y": 473.5 + }, "target": { "x": 1.0112359550561798, "y": 0.012987012987012988 }, - "targetName": "Element 17" + "targetName": "Element 17", + "targetOriginal": { + "x": 742, + "y": 473 + } } ], "constraint": { @@ -1671,11 +1806,19 @@ "x": 1, "y": -0.5 }, + "sourceOriginal": { + "x": 1025.5, + "y": 382.25 + }, "target": { "x": -1.0238095238095237, "y": 0.10526315789473684 }, - "targetName": "Element 33" + "targetName": "Element 33", + "targetOriginal": { + "x": 1136, + "y": 384 + } }, { "color": { @@ -1691,11 +1834,19 @@ "x": 1, "y": 0.5 }, + "sourceOriginal": { + "x": 1025.5, + "y": 333.75 + }, "target": { "x": -1, "y": 0.05263157894736842 }, "targetName": "Element 34", + "targetOriginal": { + "x": 1137, + "y": 299 + }, "vertices": [ { "x": 0.5246636771300448, @@ -1721,11 +1872,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 1025.5, + "y": 358 + }, "target": { "x": -0.98, "y": -0.10416666666666667 }, "targetName": "Element 2", + "targetOriginal": { + "x": 1391, + "y": 514 + }, "vertices": [ { "x": 0.7564979480164159, @@ -1792,11 +1951,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 742, + "y": 81.5 + }, "target": { "x": -1.0112359550561798, "y": -0.012987012987012988 }, - "targetName": "Element 31" + "targetName": "Element 31", + "targetOriginal": { + "x": 811, + "y": 82 + } }, { "color": { @@ -1818,11 +1985,19 @@ "x": 0, "y": 1 }, + "sourceOriginal": { + "x": 653, + "y": 43 + }, "target": { "x": -0.029411764705882353, "y": 1 }, "targetName": "Element 38", + "targetOriginal": { + "x": 1673, + "y": 249 + }, "vertices": [ { "x": 0, @@ -1892,11 +2067,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 990, + "y": 81.5 + }, "target": { "x": -0.9939759036144579, "y": 0.5061728395061729 }, "targetName": "Element 35", + "targetOriginal": { + "x": 1190, + "y": 138 + }, "vertices": [ { "x": 0.48, @@ -1961,11 +2144,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 1221, + "y": 386 + }, "target": { "x": 1.0337078651685394, "y": 0.45454545454545453 }, "targetName": "Element 20", + "targetOriginal": { + "x": 992, + "y": 456 + }, "vertices": [ { "x": -0.05240174672489083, @@ -2067,11 +2258,19 @@ "x": -1, "y": 0 }, + "sourceOriginal": { + "x": 1189, + "y": 179 + }, "target": { "x": 0.19444444444444445, "y": 0.9896907216494846 }, "targetName": "Element 29", + "targetOriginal": { + "x": 1011, + "y": 310 + }, "vertices": [ { "x": 1, @@ -2169,11 +2368,19 @@ "x": -1, "y": 0 }, + "sourceOriginal": { + "x": 1574, + "y": 308 + }, "target": { "x": -0.4977168949771689, "y": 1.017467248908297 }, "targetName": "Element 36", + "targetOriginal": { + "x": 1450, + "y": 384 + }, "vertices": [ { "x": 1, @@ -2208,7 +2415,7 @@ }, "showAdvancedTypes": true }, - "pluginVersion": "11.1.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -2236,7 +2443,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "#6ED0E0", @@ -2260,7 +2468,7 @@ "h": 11, "w": 15, "x": 5, - "y": 22 + "y": 36 }, "id": 4, "options": { @@ -2320,11 +2528,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 216, + "y": 192 + }, "target": { "x": -1.02020202020202, "y": 0.041666666666666664 }, - "targetName": "Element 2" + "targetName": "Element 2", + "targetOriginal": { + "x": 326, + "y": 191 + } } ], "constraint": { @@ -2377,11 +2593,19 @@ "x": 1, "y": 1 }, + "sourceOriginal": { + "x": 426, + "y": 168 + }, "target": { "x": -0.9797979797979798, "y": 0 }, - "targetName": "Element 3" + "targetName": "Element 3", + "targetOriginal": { + "x": 549, + "y": 93 + } }, { "color": { @@ -2398,11 +2622,19 @@ "x": 1, "y": -1 }, + "sourceOriginal": { + "x": 426, + "y": 216 + }, "target": { "x": -1, "y": 0.041666666666666664 }, - "targetName": "Element 4" + "targetName": "Element 4", + "targetOriginal": { + "x": 549, + "y": 297 + } } ], "constraint": { @@ -2455,11 +2687,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 647, + "y": 93 + }, "target": { "x": -0.9595959595959596, "y": -0.041666666666666664 }, - "targetName": "Element 11" + "targetName": "Element 11", + "targetOriginal": { + "x": 799, + "y": 94 + } } ], "constraint": { @@ -2512,11 +2752,19 @@ "x": 1, "y": 0 }, + "sourceOriginal": { + "x": 648, + "y": 298 + }, "target": { "x": -0.9797979797979798, "y": 0.041666666666666664 }, - "targetName": "Element 5" + "targetName": "Element 5", + "targetOriginal": { + "x": 798, + "y": 299 + } } ], "constraint": { @@ -2838,7 +3086,7 @@ }, "showAdvancedTypes": false }, - "pluginVersion": "11.1.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "csvContent": "gateway, product-details, product-reviews, reviews-ratings, details-checkout\n100, 56, 44, 22, 28", @@ -2873,7 +3121,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "blue", @@ -2897,7 +3146,7 @@ "h": 15, "w": 15, "x": 5, - "y": 33 + "y": 47 }, "id": 2, "options": { @@ -2949,6 +3198,10 @@ "field": "server_database2", "fixed": "white" }, + "direction": { + "field": "server_database2", + "mode": "field" + }, "path": "straight", "size": { "fixed": 2, @@ -2959,17 +3212,29 @@ "x": -1, "y": -1 }, + "sourceOriginal": { + "x": 468, + "y": 125 + }, "target": { "x": 0.030303030303030304, "y": 0 }, - "targetName": "Element 11" + "targetName": "Element 11", + "targetOriginal": { + "x": 285, + "y": 170 + } }, { "color": { "field": "server_database", "fixed": "white" }, + "direction": { + "field": "server_database", + "mode": "field" + }, "path": "straight", "size": { "fixed": 2, @@ -2980,17 +3245,29 @@ "x": -1, "y": -1 }, + "sourceOriginal": { + "x": 468, + "y": 125 + }, "target": { "x": 0.09090909090909091, "y": 0.16666666666666666 }, - "targetName": "Element 12" + "targetName": "Element 12", + "targetOriginal": { + "x": 390, + "y": 264 + } }, { "color": { "field": "server_region", "fixed": "white" }, + "direction": { + "field": "server_region", + "mode": "field" + }, "path": "straight", "size": { "fixed": 2, @@ -3001,11 +3278,19 @@ "x": 1, "y": -1 }, + "sourceOriginal": { + "x": 588.9921875, + "y": 125 + }, "target": { "x": -0.2727272727272727, "y": 0.4166666666666667 }, - "targetName": "Element 13" + "targetName": "Element 13", + "targetOriginal": { + "x": 658, + "y": 259 + } } ], "constraint": { @@ -3050,6 +3335,10 @@ "field": "database_server", "fixed": "white" }, + "direction": { + "field": "database_server", + "mode": "field" + }, "path": "straight", "size": { "fixed": 2, @@ -3060,11 +3349,19 @@ "x": 1, "y": 1 }, + "sourceOriginal": { + "x": 328, + "y": 367 + }, "target": { "x": -0.15151515151515152, "y": -0.16666666666666666 }, - "targetName": "Element 12" + "targetName": "Element 12", + "targetOriginal": { + "x": 386, + "y": 268 + } } ], "constraint": { @@ -3109,6 +3406,10 @@ "field": "region_server", "fixed": "white" }, + "direction": { + "field": "region_server", + "mode": "field" + }, "path": "straight", "size": { "fixed": 2, @@ -3119,11 +3420,19 @@ "x": -1, "y": 1 }, + "sourceOriginal": { + "x": 699, + "y": 365 + }, "target": { "x": -0.21212121212121213, "y": 0.08333333333333333 }, - "targetName": "Element 13" + "targetName": "Element 13", + "targetOriginal": { + "x": 659, + "y": 263 + } } ], "constraint": { @@ -3212,6 +3521,10 @@ "field": "database2_server", "fixed": "white" }, + "direction": { + "field": "database2_server", + "mode": "field" + }, "path": "straight", "size": { "fixed": 2, @@ -3222,11 +3535,19 @@ "x": 1, "y": 1 }, + "sourceOriginal": { + "x": 172, + "y": 191 + }, "target": { "x": -0.3333333333333333, "y": 0 }, - "targetName": "Element 11" + "targetName": "Element 11", + "targetOriginal": { + "x": 279, + "y": 170 + } } ], "constraint": { @@ -3605,7 +3926,7 @@ }, "showAdvancedTypes": false }, - "pluginVersion": "11.1.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "csvContent": "database_server, server_database, server_region, region_server, database2_server, server_database2\n10, 53, 35, 12, 22, 81", @@ -3620,9 +3941,9 @@ "type": "canvas" } ], + "preload": false, "refresh": "", - "revision": 1, - "schemaVersion": 39, + "schemaVersion": 41, "tags": ["gdev", "panel-tests", "canvas"], "templating": { "list": [] @@ -3631,11 +3952,9 @@ "from": "now-6h", "to": "now" }, - "timeRangeUpdatedDuringEditOrView": false, "timepicker": {}, "timezone": "", "title": "Panel Tests - Canvas Connection Examples", "uid": "Pu8lwQAVz", - "version": 6, - "weekStart": "" + "version": 15 } diff --git a/docs/sources/panels-visualizations/visualizations/canvas/index.md b/docs/sources/panels-visualizations/visualizations/canvas/index.md index 63daf9b63e8..0c0821cc923 100644 --- a/docs/sources/panels-visualizations/visualizations/canvas/index.md +++ b/docs/sources/panels-visualizations/visualizations/canvas/index.md @@ -481,12 +481,19 @@ You can style the selected connection using the following options: - **Color** - Set the connection color. - **Size** - Control the size of the connection by entering a number in the **Value** field. - **Radius** - Add curve to the connection by entering a value to represent the degree. -- **Arrow Direction** - Control the appearance of the arrow head. Choose from: +- **Direction** - Control the appearance of the arrow head. Choose your source from **Fixed** or **Field**. The default value is **Forward** regardless of the source type. + + If the direction source is **Fixed**, choose from: - **Forward** - The arrow head points in the direction in which the connection was drawn. - **Reverse** - The arrow head points in the opposite direction of which the connection was drawn. - **Both** - Adds arrow heads to both ends of the connection. - **None** - Removes the arrow head. + If the direction source is **Field**, select a field that contains numeric values: + - **Positive values** - Display forward arrows. + - **Negative values** - Display reverse arrows. + - **Zero** - Display no arrow heads. + - **Line style** - Choose from the following line styles: **Solid**, **Dashed**, and **Dotted**. ### Standard options diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts index 5be227ba4b9..1931a6ad967 100644 --- a/packages/grafana-schema/src/common/common.gen.ts +++ b/packages/grafana-schema/src/common/common.gen.ts @@ -111,6 +111,26 @@ export enum ResourceDimensionMode { Mapping = 'mapping', } +/** + * Links to a resource (image/svg path) + */ +export interface ResourceDimensionConfig extends BaseDimensionConfig { + fixed?: string; + mode: ResourceDimensionMode; +} + +export enum ConnectionDirection { + Both = 'both', + Forward = 'forward', + None = 'none', + Reverse = 'reverse', +} + +export enum DirectionDimensionMode { + Field = 'field', + Fixed = 'fixed', +} + export interface MapLayerOptions { /** * Custom options depending on the type @@ -899,12 +919,9 @@ export interface DataSourceRef { uid?: string; } -/** - * Links to a resource (image/svg path) - */ -export interface ResourceDimensionConfig extends BaseDimensionConfig { - fixed?: string; - mode: ResourceDimensionMode; +export interface DirectionDimensionConfig extends BaseDimensionConfig { + fixed?: ConnectionDirection; + mode: DirectionDimensionMode; } export interface FrameGeometrySource { diff --git a/packages/grafana-schema/src/common/dimensions.cue b/packages/grafana-schema/src/common/dimensions.cue index 08c0b8ac452..0ffd7d83b48 100644 --- a/packages/grafana-schema/src/common/dimensions.cue +++ b/packages/grafana-schema/src/common/dimensions.cue @@ -35,7 +35,7 @@ TextDimensionMode: "fixed" | "field" | "template" @cuetsy(kind="enum") TextDimensionConfig: { BaseDimensionConfig mode: TextDimensionMode - fixed?: string + fixed?: string }@cuetsy(kind="interface") ResourceDimensionMode: "fixed" | "field" | "mapping" @cuetsy(kind="enum") @@ -44,6 +44,15 @@ ResourceDimensionMode: "fixed" | "field" | "mapping" @cuetsy(kind="enum") ResourceDimensionConfig: { BaseDimensionConfig mode: ResourceDimensionMode - fixed?: string + fixed?: string }@cuetsy(kind="interface") +ConnectionDirection: "forward" | "reverse" | "both" | "none" @cuetsy(kind="enum", memberNames="Forward|Reverse|Both|None") + +DirectionDimensionMode: "fixed" | "field" @cuetsy(kind="enum") + +DirectionDimensionConfig: { + BaseDimensionConfig + mode: DirectionDimensionMode + fixed?: ConnectionDirection +}@cuetsy(kind="interface") diff --git a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts index 997afed4975..60cb7a2adcd 100644 --- a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts @@ -80,6 +80,7 @@ export enum ConnectionPath { export interface CanvasConnection { color?: ui.ColorDimensionConfig; + direction?: ui.DirectionDimensionConfig; path: ConnectionPath; size?: ui.ScaleDimensionConfig; source: ConnectionCoordinates; diff --git a/public/app/features/canvas/element.ts b/public/app/features/canvas/element.ts index 1821670a621..7e78ad784e7 100644 --- a/public/app/features/canvas/element.ts +++ b/public/app/features/canvas/element.ts @@ -2,7 +2,7 @@ import { ComponentType } from 'react'; import { DataLink, RegistryItem, Action } from '@grafana/data'; import { PanelOptionsSupplier } from '@grafana/data/internal'; -import { ColorDimensionConfig, ScaleDimensionConfig } from '@grafana/schema'; +import { ColorDimensionConfig, ScaleDimensionConfig, DirectionDimensionConfig } from '@grafana/schema'; import { config } from 'app/core/config'; import { BackgroundConfig, Constraint, LineConfig, Placement } from 'app/plugins/panel/canvas/panelcfg.gen'; @@ -64,7 +64,7 @@ export interface CanvasConnection { lineStyle?: LineStyleConfig; vertices?: ConnectionCoordinates[]; radius?: ScaleDimensionConfig; - direction?: ConnectionDirection; + direction?: DirectionDimensionConfig; sourceOriginal?: ConnectionCoordinates; targetOriginal?: ConnectionCoordinates; // See https://github.com/anseki/leader-line#options for more examples of more properties diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index 7495d74ff88..768ebec9f57 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -14,6 +14,7 @@ import { ScaleDimensionConfig, TextDimensionConfig, TooltipDisplayMode, + DirectionDimensionConfig, } from '@grafana/schema'; import { Portal } from '@grafana/ui'; import { config } from 'app/core/config'; @@ -24,6 +25,7 @@ import { getScalarDimensionFromData, getScaleDimensionFromData, getTextDimensionFromData, + getDirectionDimensionFromData, } from 'app/features/dimensions/utils'; import { CanvasContextMenu } from 'app/plugins/panel/canvas/components/CanvasContextMenu'; import { CanvasTooltip } from 'app/plugins/panel/canvas/components/CanvasTooltip'; @@ -207,6 +209,7 @@ export class Scene { getScalar: (scalar: ScalarDimensionConfig) => getScalarDimensionFromData(this.data, scalar), getText: (text: TextDimensionConfig) => getTextDimensionFromData(this.data, text), getResource: (res: ResourceDimensionConfig) => getResourceDimensionFromData(this.data, res), + getDirection: (direction: DirectionDimensionConfig) => getDirectionDimensionFromData(this.data, direction), getPanelData: () => this.data, }; diff --git a/public/app/features/dimensions/context.ts b/public/app/features/dimensions/context.ts index bd0755de986..ca08090808f 100644 --- a/public/app/features/dimensions/context.ts +++ b/public/app/features/dimensions/context.ts @@ -5,15 +5,24 @@ import { ScalarDimensionConfig, ScaleDimensionConfig, TextDimensionConfig, + DirectionDimensionConfig, + ConnectionDirection, } from '@grafana/schema'; import { DimensionSupplier } from './types'; export interface DimensionContext { getColor(color: ColorDimensionConfig): DimensionSupplier; + getScale(scale: ScaleDimensionConfig): DimensionSupplier; + getScalar(scalar: ScalarDimensionConfig): DimensionSupplier; + getText(text: TextDimensionConfig): DimensionSupplier; + getResource(resource: ResourceDimensionConfig): DimensionSupplier; + + getDirection(direction: DirectionDimensionConfig): DimensionSupplier; + getPanelData(): PanelData | undefined; } diff --git a/public/app/features/dimensions/direction.ts b/public/app/features/dimensions/direction.ts new file mode 100644 index 00000000000..d19131a59d1 --- /dev/null +++ b/public/app/features/dimensions/direction.ts @@ -0,0 +1,58 @@ +import { DataFrame, Field } from '@grafana/data'; +import { ConnectionDirection, DirectionDimensionConfig, DirectionDimensionMode } from '@grafana/schema'; + +import { DimensionSupplier } from './types'; +import { findField, getLastNotNullFieldValue } from './utils'; + +//--------------------------------------------------------- +// Direction dimension +//--------------------------------------------------------- + +export function getDirectionDimension( + frame: DataFrame | undefined, + config: DirectionDimensionConfig +): DimensionSupplier { + return getDirectionDimensionForField(findField(frame, config.field), config); +} + +export function getDirectionDimensionForField( + field: Field | undefined, + config: DirectionDimensionConfig +): DimensionSupplier { + const mode = config.mode ?? DirectionDimensionMode.Fixed; + + if (mode === DirectionDimensionMode.Fixed || !field) { + const v = config.fixed ?? ConnectionDirection.Forward; + return { + isAssumed: Boolean(config.field?.length) || !config.fixed, + fixed: v, + value: () => v, + get: () => v, + }; + } + + const getDirectionFromValue = (value: unknown): ConnectionDirection => { + if (value == null) { + return ConnectionDirection.Forward; + } + + const numValue = Number(value); + if (isNaN(numValue)) { + return ConnectionDirection.Forward; + } + + if (numValue > 0) { + return ConnectionDirection.Forward; + } else if (numValue < 0) { + return ConnectionDirection.Reverse; + } else { + return ConnectionDirection.None; + } + }; + + return { + field, + get: (index: number): ConnectionDirection => getDirectionFromValue(field.values[index]), + value: () => getDirectionFromValue(getLastNotNullFieldValue(field)), + }; +} diff --git a/public/app/features/dimensions/editors/DirectionDimensionEditor.tsx b/public/app/features/dimensions/editors/DirectionDimensionEditor.tsx new file mode 100644 index 00000000000..7f376d06b17 --- /dev/null +++ b/public/app/features/dimensions/editors/DirectionDimensionEditor.tsx @@ -0,0 +1,122 @@ +import { useCallback } from 'react'; + +import { + FieldNamePickerConfigSettings, + SelectableValue, + StandardEditorProps, + StandardEditorsRegistryItem, +} from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { DirectionDimensionMode, DirectionDimensionConfig, ConnectionDirection } from '@grafana/schema'; +import { InlineField, InlineFieldRow, RadioButtonGroup, Select } from '@grafana/ui'; +import { FieldNamePicker } from '@grafana/ui/internal'; + +import { DirectionDimensionOptions } from '../types'; + +// eslint-disable-next-line @typescript-eslint/consistent-type-assertions +const dummyFieldSettings = { + settings: {}, +} as StandardEditorsRegistryItem; + +type Props = StandardEditorProps; + +export const DirectionDimensionEditor = ({ value, context, onChange }: Props) => { + const directionOptions = [ + { + label: t('dimensions.direction-dimension-editor.label-fixed', 'Fixed'), + value: DirectionDimensionMode.Fixed, + description: t('dimensions.direction-dimension-editor.description-fixed', 'Fixed direction value'), + }, + { + label: t('dimensions.direction-dimension-editor.label-field', 'Field'), + value: DirectionDimensionMode.Field, + description: t('dimensions.direction-dimension-editor.description-field', 'Direction based on field value'), + }, + ]; + + const fixedDirectionOptions: Array> = [ + { value: ConnectionDirection.Forward, label: t('canvas.connection.direction-options.label-forward', 'Forward') }, + { value: ConnectionDirection.Reverse, label: t('canvas.connection.direction-options.label-reverse', 'Reverse') }, + { value: ConnectionDirection.Both, label: t('canvas.connection.direction-options.label-both', 'Both') }, + { value: ConnectionDirection.None, label: t('canvas.connection.direction-options.label-none', 'None') }, + ]; + + const labelWidth = 9; + + const onModeChange = useCallback( + (mode: DirectionDimensionMode) => { + onChange({ + ...value, + mode, + }); + }, + [onChange, value] + ); + + const onFieldChange = useCallback( + (field?: string) => { + onChange({ + ...value, + field, + }); + }, + [onChange, value] + ); + + const onFixedChange = useCallback( + (selection: SelectableValue) => { + onChange({ + ...value, + field: undefined, + fixed: selection.value ?? ConnectionDirection.Forward, + }); + }, + [onChange, value] + ); + + const mode = value?.mode ?? DirectionDimensionMode.Fixed; + const selectedDirection = fixedDirectionOptions.find((opt) => opt.value === value?.fixed) || fixedDirectionOptions[0]; + + return ( + <> + + + + + + + {mode === DirectionDimensionMode.Field && ( + + + + + + )} + + {mode === DirectionDimensionMode.Fixed && ( + + +