feat(canvas): full service cockpit on left-click + real grouping

The inspector is now a proper service panel: address with copy, group
assignment (Ungrouped / any group / "+ New group" wrapping the node),
masked env vars with a deep link into the service's Variables tab, a
config grid deep-linking to Config, inferred connections, and the
lifecycle actions. ServiceDetailPage honours ?section= for the deep
links.

Grouping is first-class: assign via the inspector or drag nodes onto a
frame; members slot beneath existing ones and the frame grows only as
needed instead of clamping or ballooning. Fixed a drag bug where moving
a member inside its own group ejected it.

Context menus everywhere: right-click a node (existing actions), a
group frame (Rename via inline edit, Ungroup keeps children in place),
or empty pane (Add service, Add group here, Tidy layout, Fit view).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-23 18:50:38 +02:00
1 parent d9bfd8a2fd
commit 7fc6ed1215
5 files changed
+847 -217

No files matched your search

@@ -15,6 +15,7 @@ export type ServiceNodeData = {
export type GroupNodeData = {
title: string;
onRename?: (groupId: string, title: string) => void;
renameNonce?: number;
};
export type ServiceNodeType = Node<ServiceNodeData, 'serviceNode'>;
@@ -170,6 +171,16 @@ export function GroupNode({ id, data }: NodeProps<GroupNodeType>) {
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(data.title);
// Context-menu rename requests bump the nonce; enter edit mode when it
// changes. Render-phase state adjustment — the React-sanctioned pattern.
const [seenRenameNonce, setSeenRenameNonce] = useState(data.renameNonce ?? 0);
const incomingNonce = data.renameNonce ?? 0;
if (incomingNonce > seenRenameNonce) {
setSeenRenameNonce(incomingNonce);
setDraft(data.title);
setEditing(true);
}
const commit = () => {
const next = draft.trim() || data.title;
setEditing(false);