Skip to content

[FEATURE] Add canvas panel plugin - #729

Merged
ibakshay merged 17 commits into
perses:mainfrom
adrianSepiol:feature/canvas-panel-plugin
Sep 9, 2026
Merged

ibakshay merged 17 commits into
perses:mainfrom
adrianSepiol:feature/canvas-panel-plugin

Conversation

@adrianSepiol

@adrianSepiol adrianSepiol commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Description

Related to: perses/perses#3845

Introduces the canvas panel plugin — a free-form network diagram editor for building weathermap-style dashboards. Users can place nodes, connect them with edges, and overlay background shapes or images. Node and edge colors can be driven by query-bound thresholds, and edge thickness can scale with metric values.

Screenshots

Here's a walkthrough of the main interactions:

Example weather map created in canvas:

Screenshot 2026-07-20 at 15 24 28

Adding a node:

Screen.Recording.2026-07-20.at.15.26.09.mov

After creating a node, you can specify its label and position, add a link, bind a query to it, and use the query value in the label or derive the color from thresholds.

Adding an edge:

Screen.Recording.2026-07-20.at.15.26.45.mov

Edges are created by dragging from one node to another. They can be bidirectional and, like nodes, can have a query bound to them.

Zoom, pan, and resize:

Screen.Recording.2026-07-20.at.15.32.35.mov

Adding backgrounds:

adding.background.mov

Multiple backgrounds can be added with an image or a solid color at varying opacity levels. If "Global" is selected, the background always fills the entire view regardless of pan/zoom; otherwise it is scoped to a specified area.

Checklist

  • Pull request has a descriptive title and context useful to a reviewer.
  • Pull request title follows the [<catalog_entry>] <commit message> naming convention using one of the
    following catalog_entry values: FEATURE, ENHANCEMENT, BUGFIX, BREAKINGCHANGE, DOC,IGNORE.
  • All commits have DCO signoffs.

UI Changes

  • Changes that impact the UI include screenshots and/or screencasts of the relevant changes.
  • Code follows the UI guidelines.

@adrianSepiol
adrianSepiol force-pushed the feature/canvas-panel-plugin branch 8 times, most recently from 39b9803 to d73e5e2 Compare July 17, 2026 07:54
@adrianSepiol
adrianSepiol marked this pull request as ready for review July 20, 2026 13:42
@adrianSepiol
adrianSepiol requested review from a team, AntoineThebaud and Nexucis as code owners July 20, 2026 13:42
@adrianSepiol
adrianSepiol requested review from shahrokni and removed request for a team July 20, 2026 13:42
@@ -0,0 +1,20 @@
{
"kind": "Canvas",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears to me that this is a nodes panel rather than a canvas. Unless we are thinking about adding other things different from nodes in the future.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, We would like to develop it further with new things. I think adding background is one of the things that is already here and fits more into "canvas" than "nodes" plugin.

@jgbernalp

Copy link
Copy Markdown
Contributor

First of all, awesome job @adrianSepiol!

Would be there an option to create a graph (nodes and connections) based on a query? for example network topology charts have queries that return a list of nodes and their connections. In that case could this panel create a graph from that information in addition to the manual placement of nodes?

@adrianSepiol

Copy link
Copy Markdown
Contributor Author

First of all, awesome job @adrianSepiol!

Would be there an option to create a graph (nodes and connections) based on a query? for example network topology charts have queries that return a list of nodes and their connections. In that case could this panel create a graph from that information in addition to the manual placement of nodes?

That is something we also discussed and would like to work on in the future, but the idea is to make this work with this spec for the first iteration and then add new functionalities gradually.

@ibakshay ibakshay left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job Adrian! My agent found some nits.

Comment thread canvas/src/components/editor/EditorCanvas.tsx Outdated
Comment thread canvas/src/components/editor/EditorCanvas.tsx Outdated
Comment thread canvas/src/components/panel/CanvasPanel.tsx Outdated
export function PanelEdgeLayer({ spec, seriesByQueryIndex, k, paletteColors }: PanelEdgeLayerProps): ReactElement {
const nodes = spec.nodes ?? [];
const edges = spec.edges ?? [];
const nodeById = new Map(nodes.map((n) => [n.id, n]));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NOTE] Rule: js-index-maps — Build Maps for Repeated Lookups

const nodeById = new Map(nodes.map((n) => [n.id, n])) is created on every render without useMemo. Since PanelEdgeLayer re-renders on zoom changes, this map is rebuilt every time even though spec.nodes hasn't changed.

Fix: Wrap in useMemo(() => new Map(nodes.map(...)), [nodes]).

Comment thread canvas/src/hooks/useCanvasTheme.ts Outdated
@ibakshay

Copy link
Copy Markdown
Contributor

[SUGGESTION] please add a "info" message in the panel for the end users when the canvas is empty. Otherwise, it looks blank. Otherwise, the panel looks blank.
image

Comment thread canvas/sdk/go/canvas.go
Comment thread canvas/sdk/go/canvas.go Outdated
Comment thread canvas/sdk/go/canvas.go Outdated
Comment thread canvas/sdk/go/canvas.go Outdated
@ibakshay

Copy link
Copy Markdown
Contributor

In the edge settings, please add full names for north, south, ... instead of short abbreviations.
image

Comment thread canvas/src/components/editor/BackgroundPropertiesPanel.tsx Outdated
Comment thread canvas/src/components/editor/BackgroundPropertiesPanel.tsx Outdated
@shahrokni

shahrokni commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

@adrianSepiol
Thank you for the great contribution. ❤️
Do we need a data source to connect to?

network observability or service-mesh health overviews
How could we validate the plugin against a real network?

Comment thread canvas/src/components/editor/BackgroundPropertiesPanel.tsx Outdated
Comment thread canvas/src/components/editor/EdgePropertiesPanel.tsx Outdated
Comment thread canvas/src/components/editor/EdgePropertiesPanel.tsx Outdated
Comment thread canvas/src/components/editor/EdgePropertiesPanel.tsx Outdated
Comment on lines +92 to +94
const v = e.target.value;
onChange({ ...edge, sourceQueryIndex: v === '' ? undefined : Number(v) });
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we validate sourceQueryIndex here?
Currently any none-empty value is converted with Number(V), which could potentially result in NaN, a negative value, or non-integer index.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've extracted parseNumberInput and use it in such cases.

Comment thread canvas/src/components/editor/EdgePropertiesPanel.tsx Outdated
Comment thread canvas/src/components/editor/EdgePropertiesPanel.tsx Outdated
Comment on lines +147 to +160
<TextField
select
label="Source anchor"
size="small"
value={edge.sourceAnchor ?? 'n'}
onChange={onSourceAnchorChange}
slotProps={{ select: { MenuProps: { PaperProps: { style: { maxHeight: 240 } } } } }}
>
{ANCHOR_OPTIONS.map((a) => (
<MenuItem key={a} value={a}>
{a}
</MenuItem>
))}
</TextField>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shahrokni Test 👀

Comment thread canvas/src/components/editor/EditorCanvas.tsx Outdated
Comment thread canvas/src/components/editor/EditorCanvas.tsx Outdated
Comment thread canvas/src/components/editor/EditorCanvas.tsx Outdated
Comment on lines +156 to +161
case 'selecting': {
const ids = applySelection();
selectItems(ids);
endInteraction();
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shahrokni 👀 Test: Could it draw a selection box over an empty space of the canvas? Then what would happen?

Comment on lines +165 to +177
const onSvgDoubleClick = useCallback(
(event: MouseEvent<SVGSVGElement>): void => {
if (event.ctrlKey || event.metaKey) {
const boundingBox = nodeBoundingBox(displayNodes);
if (boundingBox) {
fitView(boundingBox, width, height);
}
} else {
resetPan();
}
},
[displayNodes, fitView, resetPan, width, height]
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shahrokni Test 👀

Comment on lines +179 to +189
const onKeyDown = useCallback(
(event: KeyboardEvent<SVGSVGElement>): void => {
if (event.key !== 'Delete' && event.key !== 'Backspace') {
return;
}
if (selectedIds.size > 0) {
deleteSelected();
}
},
[selectedIds, deleteSelected]
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not with this PR, but as a follow up,
Probably we should consider a simple Undo, Redo support,
Imagine that you have selected many nodes in a huge canvas, and just unintentionally, you press Delete.
Would be painful to reorganize everything again.

<EditorNode
key={node.id}
node={node}
isHovered={hoveredId === node.id}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shahrokni Test 👀 Overlapping nodes

Comment thread canvas/src/components/editor/EditorCanvas.tsx Outdated
Comment on lines +1 to +2
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shahrokni Test 👀 Node self connection?

Comment on lines +63 to +66
const canvasWidth = containerRef.current?.clientWidth ?? 0;
const cx = transform.invertX(canvasWidth / 2);
const cy = transform.invertY(CANVAS_HEIGHT / 2);
addNode(cx, cy);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we avoid adding a node when the container is unavailable or has zero width?
Falling back to 0 here could result in calculating an unintended canvas position.
Simply, if no canvas or no width, why should we add a node?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added guard to check if canvas is mounted.

Comment thread canvas/src/components/editor/EditorItemsPanel.tsx Outdated
Comment thread canvas/src/components/editor/EditorItemsPanel.tsx Outdated
Comment on lines +54 to +87
<TextField
label="X"
size="small"
type="number"
value={Math.round(node.x)}
onChange={onIntFieldChange('x')}
sx={{ width: 80 }}
/>
<TextField
label="Y"
size="small"
type="number"
value={Math.round(node.y)}
onChange={onIntFieldChange('y')}
sx={{ width: 80 }}
/>
<TextField
label="Width"
size="small"
type="number"
value={Math.round(node.width)}
slotProps={{ htmlInput: { min: 8 } }}
onChange={onIntFieldChange('width', 8)}
sx={{ width: 80 }}
/>
<TextField
label="Height"
size="small"
type="number"
value={Math.round(node.height)}
slotProps={{ htmlInput: { min: 8 } }}
onChange={onIntFieldChange('height', 8)}
sx={{ width: 80 }}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have some duplication here. The Text Fields could be dynamically generated by a loop over and object-keys or array of X ,Y, Width, Height

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are too many differences to have gain from loop especially after introducing point type but as I mentioned before I created component and use it here as well.

sx={{ width: 80 }}
/>
</Stack>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shahrokni Test 👀

Comment thread canvas/src/components/panel/PanelEdgeLayer.tsx Outdated
Comment on lines +1 to +2
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shahrokni Test 👀 Pre-ordered SVG layers. Could it be problamatic?

Comment on lines +5 to +7
//
// http://www.apache.org/licenses/LICENSE-2.0
//

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shahrokni Test 👀 Importance of Ctrl+ Click for select for larger canvases.

Comment thread canvas/src/components/panel/ThresholdLegend.tsx Outdated
Comment thread canvas/src/components/panel/ThresholdLegend.tsx Outdated
Comment thread canvas/src/components/settings/EdgeThicknessSettings.tsx
@@ -0,0 +1,63 @@
// Copyright The Perses Authors

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shahrokni Test 👀 Collision if it is a long list of legends?

Comment thread canvas/src/components/shared/EdgeLines.tsx Outdated
Comment thread canvas/src/contexts/SpecContext.tsx Outdated
Comment thread canvas/src/contexts/SpecContext.tsx Outdated
Comment thread canvas/src/hooks/useEdgeConnect.ts Outdated
Comment thread canvas/src/hooks/useEdgeConnect.ts Outdated
Comment thread canvas/src/hooks/useEdgeConnect.ts Outdated
Comment thread canvas/src/hooks/useEdgeConnect.ts Outdated
Comment thread canvas/src/hooks/useEdgeConnect.ts Outdated
Comment thread canvas/src/hooks/useEdgeConnect.ts Outdated
Comment thread canvas/src/hooks/useNodeMove.ts Outdated
Comment thread canvas/src/hooks/useRectSelect.ts Outdated
@adrianSepiol
adrianSepiol force-pushed the feature/canvas-panel-plugin branch from 4b84d3b to 7c250fb Compare August 21, 2026 11:29
@adrianSepiol
adrianSepiol force-pushed the feature/canvas-panel-plugin branch 4 times, most recently from 784c15b to 2742a8a Compare September 4, 2026 11:49
@adrianSepiol

Copy link
Copy Markdown
Contributor Author

@adrianSepiol Thank you for the great contribution. ❤️ Do we need a data source to connect to?

network observability or service-mesh health overviews
How could we validate the plugin against a real network?

@shahrokni This plugin functions as a canvas where you manually create nodes and edges, then connect queries to the edges/nodes of your choice. It supports time series queries. To test it, simply create a query, add nodes/edges, and connect them. Query results can be displayed as labels on nodes/edges, and you can define thresholds that dynamically change the color of nodes/edges based on the returned values.

I've addressed all your comments, either through code changes or replies in the comments. Whenever you have time, feel free to take another look.

Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
[ENHANCEMENT] update panel schema re-exports to use @perses-dev/plugin-system

panelEditorSchema and buildPanelEditorSchema have moved from
@perses-dev/spec to @perses-dev/plugin-system.

Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
…mponent

Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
…eeEndpoint properties

Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
…points

Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
… component for numeric inputs

Signed-off-by: Adrian Sepiół <a.sepiol@sap.com>
@adrianSepiol
adrianSepiol force-pushed the feature/canvas-panel-plugin branch from cb0a0ba to efe4e10 Compare September 8, 2026 07:56

@shahrokni shahrokni left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my perspective, and for the comments I left, the issues are resolved.

@ibakshay ibakshay left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have texted the UI manually as well. Spectacular work!! 🚀 @adrianSepiol

@ibakshay
ibakshay added this pull request to the merge queue Sep 9, 2026
Merged via the queue into perses:main with commit 6df72c9 Sep 9, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants