This is an automated email from the ASF dual-hosted git repository.
xqhu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new d9ddcd4bf51 Add YAML Editor and Visualization Panel (#35947)
d9ddcd4bf51 is described below
commit d9ddcd4bf5199b28829fa69454bb5c34aad5e6fe
Author: Chenzo <[email protected]>
AuthorDate: Sat Sep 6 20:57:51 2025 +0800
Add YAML Editor and Visualization Panel (#35947)
* Yaml Panel
* Update CHANGES.md
* Update
* Update
sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/apache_beam_jupyterlab_sidepanel/yaml_parse_utils.py
Co-authored-by: gemini-code-assist[bot]
<176961590+gemini-code-assist[bot]@users.noreply.github.com>
* Update CHANGES.md
* Fix CI/CD fails
* Update CHANGES.md
* Update yaml_parse_utils.py
* Update CHANGES.md
* Update CHANGES.md
---------
Co-authored-by: gemini-code-assist[bot]
<176961590+gemini-code-assist[bot]@users.noreply.github.com>
---
CHANGES.md | 3 +-
.../yaml_parse_utils.py | 176 +++++++
.../apache-beam-jupyterlab-sidepanel/package.json | 17 +-
.../src/SidePanel.ts | 11 +-
.../apache-beam-jupyterlab-sidepanel/src/index.ts | 35 +-
.../src/yaml/CustomStyle.tsx | 179 +++++++
.../src/yaml/DataType.ts | 37 ++
.../src/yaml/EditablePanel.tsx | 408 ++++++++++++++++
.../src/yaml/EmojiMap.ts | 75 +++
.../src/yaml/Yaml.tsx | 322 +++++++++++++
.../src/yaml/YamlEditor.tsx | 338 +++++++++++++
.../src/yaml/YamlFlow.tsx | 227 +++++++++
.../src/yaml/YamlWidget.tsx | 34 ++
.../style/index.css | 3 +
.../style/mdc-theme.css | 4 +-
.../style/{index.css => yaml/Yaml.css} | 30 +-
.../style/{index.css => yaml/YamlEditor.css} | 26 +-
.../style/yaml/YamlFlow.css | 168 +++++++
.../apache-beam-jupyterlab-sidepanel/yarn.lock | 535 ++++++++++++++++++++-
19 files changed, 2602 insertions(+), 26 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index 0394882d8a7..af1ab8c6e3c 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -65,6 +65,7 @@
* New highly anticipated feature X added to Python SDK
([#X](https://github.com/apache/beam/issues/X)).
* New highly anticipated feature Y added to Java SDK
([#Y](https://github.com/apache/beam/issues/Y)).
+* (Python) Add YAML Editor and Visualization Panel
([#35772](https://github.com/apache/beam/issues/35772)).
## I/Os
@@ -96,7 +97,7 @@
* New highly anticipated feature X added to Python SDK
([#X](https://github.com/apache/beam/issues/X)).
* New highly anticipated feature Y added to Java SDK
([#Y](https://github.com/apache/beam/issues/Y)).
-* (Python) Prism runner now enabled by default for most Python pipelines using
the direct runner ([#34612](https://github.com/apache/beam/pull/34612)). This
may break some tests, see https://github.com/apache/beam/pull/34612 for details
on how to handle issues.
+* [Python] Prism runner now enabled by default for most Python pipelines using
the direct runner ([#34612](https://github.com/apache/beam/pull/34612)). This
may break some tests, see https://github.com/apache/beam/pull/34612 for details
on how to handle issues.
## I/Os
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/apache_beam_jupyterlab_sidepanel/yaml_parse_utils.py
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/apache_beam_jupyterlab_sidepanel/yaml_parse_utils.py
new file mode 100644
index 00000000000..aebca7b85d6
--- /dev/null
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/apache_beam_jupyterlab_sidepanel/yaml_parse_utils.py
@@ -0,0 +1,176 @@
+# Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+# use this file except in compliance with the License. You may obtain a copy
of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
under
+# the License.
+
+import dataclasses
+import json
+from dataclasses import dataclass
+from typing import Any
+from typing import Dict
+from typing import List
+from typing import TypedDict
+
+import yaml
+
+import apache_beam as beam
+from apache_beam.yaml.main import build_pipeline_components_from_yaml
+
+# ======================== Type Definitions ========================
+
+
+@dataclass
+class NodeData:
+ id: str
+ label: str
+ type: str = ""
+
+ def __post_init__(self):
+ # Ensure ID is not empty
+ if not self.id:
+ raise ValueError("Node ID cannot be empty")
+
+
+@dataclass
+class EdgeData:
+ source: str
+ target: str
+ label: str = ""
+
+ def __post_init__(self):
+ if not self.source or not self.target:
+ raise ValueError("Edge source and target cannot be empty")
+
+
+class FlowGraph(TypedDict):
+ nodes: List[Dict[str, Any]]
+ edges: List[Dict[str, Any]]
+
+
+# ======================== Main Function ========================
+
+
+def parse_beam_yaml(yaml_str: str, isDryRunMode: bool = False) -> str:
+ """
+ Parse Beam YAML and convert to flow graph data structure
+
+ Args:
+ yaml_str: Input YAML string
+
+ Returns:
+ Standardized response format:
+ - Success: {'status': 'success', 'data': {...}, 'error': None}
+ - Failure: {'status': 'error', 'data': None, 'error': 'message'}
+ """
+ # Phase 1: YAML Parsing
+ try:
+ parsed_yaml = yaml.safe_load(yaml_str)
+ if not parsed_yaml or 'pipeline' not in parsed_yaml:
+ return build_error_response(
+ "Invalid YAML structure: missing 'pipeline' section")
+ except yaml.YAMLError as e:
+ return build_error_response(f"YAML parsing error: {str(e)}")
+
+ # Phase 2: Pipeline Validation
+ try:
+ options, constructor = build_pipeline_components_from_yaml(
+ yaml_str,
+ [],
+ validate_schema='per_transform'
+ )
+ if isDryRunMode:
+ with beam.Pipeline(options=options) as p:
+ constructor(p)
+ except Exception as e:
+ return build_error_response(f"Pipeline validation failed: {str(e)}")
+
+ # Phase 3: Graph Construction
+ try:
+ pipeline = parsed_yaml['pipeline']
+ transforms = pipeline.get('transforms', [])
+
+ nodes: List[NodeData] = []
+ edges: List[EdgeData] = []
+
+ nodes.append(NodeData(id='0', label='Input', type='input'))
+ nodes.append(NodeData(id='1', label='Output', type='output'))
+
+ # Process transform nodes
+ for idx, transform in enumerate(transforms):
+ if not isinstance(transform, dict):
+ continue
+
+ payload = {k: v for k, v in transform.items() if k not in {"type"}}
+
+ node_id = f"t{idx}"
+ node_data = NodeData(
+ id=node_id,
+ label=transform.get('type', 'unnamed'),
+ type='default',
+ **payload)
+ nodes.append(node_data)
+
+ # Create connections between nodes
+ if idx > 0:
+ edges.append(
+ EdgeData(source=f"t{idx-1}", target=node_id, label='chain'))
+
+ if transforms:
+ edges.append(EdgeData(source='0', target='t0', label='start'))
+ edges.append(EdgeData(source=node_id, target='1', label='stop'))
+
+ def to_dict(node):
+ if hasattr(node, '__dataclass_fields__'):
+ return dataclasses.asdict(node)
+ return node
+
+ nodes_serializable = [to_dict(n) for n in nodes]
+
+ return build_success_response(
+ nodes=nodes_serializable, edges=[dataclasses.asdict(e) for e in edges])
+
+ except Exception as e:
+ return build_error_response(f"Graph construction failed: {str(e)}")
+
+
+# ======================== Utility Functions ========================
+
+
+def build_success_response(
+ nodes: List[Dict[str, Any]], edges: List[Dict[str, Any]]) -> str:
+ """Build success response"""
+ return json.dumps({'data': {'nodes': nodes, 'edges': edges}, 'error': None})
+
+
+def build_error_response(error_msg: str) -> str:
+ """Build error response"""
+ return json.dumps({'data': None, 'error': error_msg})
+
+
+if __name__ == "__main__":
+ # Example usage
+ example_yaml = """
+pipeline:
+ transforms:
+ - type: ReadFromCsv
+ name: A
+ config:
+ path: /path/to/input*.csv
+ - type: WriteToJson
+ name: B
+ config:
+ path: /path/to/output.json
+ input: ReadFromCsv
+ - type: Join
+ input: [A, B]
+ """
+
+ response = parse_beam_yaml(example_yaml, isDryRunMode=False)
+ print(response)
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/package.json
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/package.json
index 8b51461f6cd..eef3fcaa80f 100644
---
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/package.json
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/package.json
@@ -47,27 +47,37 @@
"@jupyterlab/launcher": "^4.3.6",
"@jupyterlab/mainmenu": "^4.3.6",
"@lumino/widgets": "^2.2.1",
+ "@monaco-editor/react": "^4.7.0",
"@rmwc/base": "^14.0.0",
"@rmwc/button": "^8.0.6",
+ "@rmwc/card": "^14.3.5",
"@rmwc/data-table": "^8.0.6",
"@rmwc/dialog": "^8.0.6",
"@rmwc/drawer": "^8.0.6",
"@rmwc/fab": "^8.0.6",
+ "@rmwc/grid": "^14.3.5",
"@rmwc/list": "^8.0.6",
"@rmwc/ripple": "^14.0.0",
"@rmwc/textfield": "^8.0.6",
"@rmwc/tooltip": "^8.0.6",
"@rmwc/top-app-bar": "^8.0.6",
+ "@rmwc/touch-target": "^14.3.5",
+ "@xyflow/react": "^12.8.2",
+ "dagre": "^0.8.5",
+ "lodash": "^4.17.21",
"material-design-icons": "^3.0.1",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "react-split": "^2.0.14"
},
"devDependencies": {
"@jupyterlab/builder": "^4.3.6",
"@testing-library/dom": "^9.3.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.0.0",
+ "@types/dagre": "^0.7.53",
"@types/jest": "^29.5.14",
+ "@types/lodash": "^4.17.20",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@typescript-eslint/eslint-plugin": "^7.3.1",
@@ -97,5 +107,6 @@
"test": "jest",
"resolutions": {
"@types/react": "^18.2.0"
- }
-}
+ },
+ "packageManager":
"[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
+}
\ No newline at end of file
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/SidePanel.ts
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/SidePanel.ts
index fb86b0a53fd..d8f19c27884 100644
---
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/SidePanel.ts
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/SidePanel.ts
@@ -58,14 +58,17 @@ export class SidePanel extends BoxPanel {
const sessionModelItr = manager.sessions.running();
const firstModel = sessionModelItr.next();
let onlyOneUniqueKernelExists = true;
- if (firstModel === undefined) {
- // There is zero unique running kernel.
+
+ if (firstModel.done) {
+ // No Running kernel
onlyOneUniqueKernelExists = false;
} else {
+ // firstModel.value is the first session
let sessionModel = sessionModelItr.next();
- while (sessionModel !== undefined) {
+
+ while (!sessionModel.done) {
+ // Check if there is more than one unique kernel
if (sessionModel.value.kernel.id !== firstModel.value.kernel.id) {
- // There is more than one unique running kernel.
onlyOneUniqueKernelExists = false;
break;
}
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/index.ts
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/index.ts
index 3f2b02d11b5..92a1ea3cdbb 100644
---
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/index.ts
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/index.ts
@@ -28,12 +28,15 @@ import { SidePanel } from './SidePanel';
import {
InteractiveInspectorWidget
} from './inspector/InteractiveInspectorWidget';
+import { YamlWidget } from './yaml/YamlWidget';
namespace CommandIDs {
export const open_inspector =
'apache-beam-jupyterlab-sidepanel:open_inspector';
export const open_clusters_panel =
'apache-beam-jupyterlab-sidepanel:open_clusters_panel';
+ export const open_yaml_editor =
+ 'apache-beam-jupyterlab-sidepanel:open_yaml_editor';
}
/**
@@ -67,6 +70,7 @@ function activate(
const category = 'Interactive Beam';
const inspectorCommandLabel = 'Open Inspector';
const clustersCommandLabel = 'Manage Clusters';
+ const yamlCommandLabel = 'Edit YAML Pipeline';
const { commands, shell, serviceManager } = app;
async function createInspectorPanel(): Promise<SidePanel> {
@@ -105,6 +109,24 @@ function activate(
return panel;
}
+ async function createYamlPanel(): Promise<SidePanel> {
+ const sessionContext = new SessionContext({
+ sessionManager: serviceManager.sessions,
+ specsManager: serviceManager.kernelspecs,
+ name: 'Interactive Beam YAML Session'
+ });
+ const yamlEditor = new YamlWidget(sessionContext);
+ const panel = new SidePanel(
+ serviceManager,
+ rendermime,
+ sessionContext,
+ 'Interactive Beam YAML Editor',
+ yamlEditor
+ );
+ activatePanel(panel);
+ return panel;
+ }
+
function activatePanel(panel: SidePanel): void {
shell.add(panel, 'main');
shell.activateById(panel.id);
@@ -122,6 +144,12 @@ function activate(
execute: createClustersPanel
});
+ // The open_yaml_editor command is also used by the below entry points.
+ commands.addCommand(CommandIDs.open_yaml_editor, {
+ label: yamlCommandLabel,
+ execute: createYamlPanel
+ });
+
// Entry point in launcher.
if (launcher) {
launcher.add({
@@ -132,6 +160,10 @@ function activate(
command: CommandIDs.open_clusters_panel,
category: category
});
+ launcher.add({
+ command: CommandIDs.open_yaml_editor,
+ category: category
+ });
}
// Entry point in top menu.
@@ -140,10 +172,11 @@ function activate(
mainMenu.addMenu(menu);
menu.addItem({ command: CommandIDs.open_inspector });
menu.addItem({ command: CommandIDs.open_clusters_panel });
+ menu.addItem({ command: CommandIDs.open_yaml_editor });
// Entry point in commands palette.
palette.addItem({ command: CommandIDs.open_inspector, category });
palette.addItem({ command: CommandIDs.open_clusters_panel, category });
+ palette.addItem({ command: CommandIDs.open_yaml_editor, category });
}
-
export default extension;
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/CustomStyle.tsx
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/CustomStyle.tsx
new file mode 100644
index 00000000000..87d93de0b60
--- /dev/null
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/CustomStyle.tsx
@@ -0,0 +1,179 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy
of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
under
+// the License.
+
+import React, { memo } from 'react';
+import { Handle, Position } from '@xyflow/react';
+import { EdgeProps, BaseEdge, getSmoothStepPath } from '@xyflow/react';
+import { INodeData } from './DataType';
+import { transformEmojiMap } from './EmojiMap';
+
+export function DefaultNode({ data }: { data: INodeData }) {
+ const emoji = data.label
+ ? transformEmojiMap[data.label] || 'π¦'
+ : data.emoji || 'π¦';
+ const typeClass = data.type ? `custom-node-${data.type}` : '';
+
+ return (
+ <div className={`custom-node ${typeClass}`}>
+ <div className="custom-node-header">
+ <div className="custom-node-icon">{emoji}</div>
+ <div className="custom-node-title">{data.label}</div>
+ </div>
+
+ <Handle type="target" position={Position.Top} className="custom-handle"
/>
+ <Handle
+ type="source"
+ position={Position.Bottom}
+ className="custom-handle"
+ />
+ </div>
+ );
+}
+
+// ===== Input Node =====
+export function InputNode({ data }: { data: INodeData }) {
+ return (
+ <div className="custom-node custom-node-input">
+ <div className="custom-node-header">
+ <div className="custom-node-icon">{data.emoji || 'π’'}</div>
+ <div className="custom-node-title">{data.label}</div>
+ </div>
+
+ <Handle
+ type="source"
+ position={Position.Bottom}
+ id="output"
+ className="custom-handle"
+ />
+ </div>
+ );
+}
+
+// ===== Output Node =====
+export function OutputNode({ data }: { data: INodeData }) {
+ return (
+ <div className="custom-node custom-node-output">
+ <div className="custom-node-header">
+ <div className="custom-node-icon">{data.emoji || 'π΄'}</div>
+ <div className="custom-node-title">{data.label}</div>
+ </div>
+
+ <Handle
+ type="target"
+ position={Position.Top}
+ id="input"
+ className="custom-handle"
+ />
+ </div>
+ );
+}
+
+export default memo(DefaultNode);
+
+export function AnimatedSVGEdge({
+ id,
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ sourcePosition,
+ targetPosition
+}: EdgeProps) {
+ const [initialEdgePath] = getSmoothStepPath({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ sourcePosition,
+ targetPosition
+ });
+
+ let edgePath = initialEdgePath;
+
+ // If the edge is almost vertical or horizontal, use a straight line
+ const dx = Math.abs(targetX - sourceX);
+ const dy = Math.abs(targetY - sourceY);
+ if (dx < 1) {
+ edgePath = `M${sourceX},${sourceY} L${sourceX + 1},${targetY}`;
+ } else if (dy < 1) {
+ edgePath = `M${sourceX},${sourceY} L${targetX},${sourceY + 1}`;
+ }
+
+ const dotCount = 4;
+ const dotDur = 3.5;
+
+ const dots = Array.from({ length: dotCount }, (_, i) => (
+ <circle key={i} r="5" fill="url(#dotGradient)" opacity="0.8">
+ <animateMotion
+ dur={`${dotDur}s`}
+ repeatCount="indefinite"
+ begin={`${(i * dotDur) / dotCount}s`}
+ path={edgePath}
+ />
+ <animate
+ attributeName="r"
+ values="5;7;5"
+ dur={`${dotDur}s`}
+ repeatCount="indefinite"
+ begin={`${(i * dotDur) / dotCount}s`}
+ />
+ </circle>
+ ));
+
+ return (
+ <>
+ {/* Gradient Base Edge */}
+ <BaseEdge
+ id={id}
+ path={edgePath}
+ style={{
+ stroke: 'url(#gradientEdge)',
+ strokeWidth: 12
+ }}
+ />
+
+ {/* Dots */}
+ {dots}
+
+ {/* Flow shader line */}
+ <path
+ d={edgePath}
+ fill="none"
+ stroke="rgba(255,255,255,0.2)"
+ strokeWidth={5}
+ strokeDasharray="10 10"
+ >
+ <animate
+ attributeName="stroke-dashoffset"
+ from="20"
+ to="0"
+ dur="0.5s"
+ repeatCount="indefinite"
+ />
+ </path>
+
+ {/* Gradient Color */}
+ <defs>
+ <linearGradient id="gradientEdge" gradientTransform="rotate(90)">
+ <stop offset="0%" stopColor="#4facfe" />
+ <stop offset="100%" stopColor="#00f2fe" />
+ </linearGradient>
+
+ <radialGradient id="dotGradient">
+ <stop offset="0%" stopColor="#fff" stopOpacity="1" />
+ <stop offset="50%" stopColor="#4facfe" stopOpacity="0.8" />
+ <stop offset="100%" stopColor="#00f2fe" stopOpacity="0.5" />
+ </radialGradient>
+ </defs>
+ </>
+ );
+}
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/DataType.ts
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/DataType.ts
new file mode 100644
index 00000000000..0ea535d5fc6
--- /dev/null
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/DataType.ts
@@ -0,0 +1,37 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy
of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
under
+// the License.
+
+export const nodeWidth = 320;
+export const nodeHeight = 100;
+
+export interface INodeData {
+ id: string;
+ label: string;
+ type?: string;
+ [key: string]: any;
+}
+
+export interface IEdgeData {
+ source: string;
+ target: string;
+ label?: string;
+}
+
+export interface IFlowGraph {
+ nodes: INodeData[];
+ edges: IEdgeData[];
+}
+
+export interface IApiResponse {
+ data: IFlowGraph | null;
+ error: string | null;
+}
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/EditablePanel.tsx
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/EditablePanel.tsx
new file mode 100644
index 00000000000..d2b19d4371f
--- /dev/null
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/EditablePanel.tsx
@@ -0,0 +1,408 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy
of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
under
+// the License.
+
+import React from 'react';
+import { Node } from '@xyflow/react';
+import '../../style/yaml/YamlEditor.css';
+import { transformEmojiMap } from './EmojiMap';
+
+type EditableKeyValuePanelProps = {
+ node: Node;
+ onChange: (newData: Record<string, any>) => void;
+ depth?: number;
+};
+
+type EditableKeyValuePanelState = {
+ localData: Record<string, any>;
+ collapsedKeys: Set<string>;
+};
+
+/**
+ * An editable key-value panel component for displaying
+ * and modifying node properties.
+ *
+ * Features:
+ * - Nested object support with collapsible sections
+ * - Real-time key-value editing with validation
+ * - Dynamic field addition and deletion
+ * - Support for multi-line text values
+ * - Object conversion for nested structures
+ * - Reference documentation integration
+ * - Visual hierarchy with depth-based indentation
+ * - Interactive UI with hover effects and transitions
+ *
+ * State Management:
+ * - localData: Local copy of the node data being edited
+ * - collapsedKeys: Set of keys that are currently collapsed
+ *
+ * Props:
+ * @param {Node} node - The node is data to be edited
+ * @param {(data: Record<string, any>) => void} onChange -
+ * Callback for data changes
+ * @param {number} [depth=0] - Current nesting depth for recursive rendering
+ *
+ * Methods:
+ * - toggleCollapse: Toggles collapse state of nested objects
+ * - handleKeyChange: Updates keys with validation
+ * - handleValueChange: Updates values in the local data
+ * - handleDelete: Removes key-value pairs
+ * - handleAddPair: Adds new key-value pairs
+ * - convertToObject: Converts primitive values to objects
+ * - renderValueEditor: Renders appropriate input based on value type
+ *
+ * UI Features:
+ * - Collapsible nested object sections
+ * - Multi-line text support for complex values
+ * - Add/Delete buttons for field management
+ * - Reference documentation links
+ * - Visual feedback for user interactions
+ * - Responsive design with proper spacing
+ */
+export class EditableKeyValuePanel extends React.Component<
+ EditableKeyValuePanelProps,
+ EditableKeyValuePanelState
+> {
+ static defaultProps = {
+ depth: 0
+ };
+
+ constructor(props: EditableKeyValuePanelProps) {
+ super(props);
+ this.state = {
+ localData: { ...(props.node ? props.node.data : {}) },
+ collapsedKeys: new Set()
+ };
+ }
+
+ componentDidUpdate(prevProps: EditableKeyValuePanelProps) {
+ if (prevProps.node !== this.props.node && this.props.node) {
+ this.setState({ localData: { ...(this.props.node.data ?? {}) } });
+ }
+ }
+
+ toggleCollapse = (key: string) => {
+ this.setState(({ collapsedKeys }) => {
+ const newSet = new Set(collapsedKeys);
+ newSet.has(key) ? newSet.delete(key) : newSet.add(key);
+ return { collapsedKeys: newSet };
+ });
+ };
+
+ handleKeyChange = (oldKey: string, newKey: string) => {
+ newKey = newKey.trim();
+ if (newKey === oldKey || newKey === '') {
+ return alert('Invalid Key!');
+ }
+ if (newKey in this.state.localData) {
+ return alert('Duplicated Key!');
+ }
+
+ const newData: Record<string, any> = {};
+ for (const [k, v] of Object.entries(this.state.localData)) {
+ newData[k === oldKey ? newKey : k] = v;
+ }
+
+ this.setState({ localData: newData }, () => this.props.onChange(newData));
+ };
+
+ handleValueChange = (key: string, newValue: any) => {
+ const newData = { ...this.state.localData, [key]: newValue };
+ this.setState({ localData: newData }, () => this.props.onChange(newData));
+ };
+
+ handleDelete = (key: string) => {
+ const { [key]: _, ...rest } = this.state.localData;
+ void _;
+ this.setState({ localData: rest }, () => this.props.onChange(rest));
+ };
+
+ handleAddPair = () => {
+ let i = 1;
+ const baseKey = 'newKey';
+ while (`${baseKey}${i}` in this.state.localData) {
+ i++;
+ }
+ const newKey = `${baseKey}${i}`;
+ const newData = { ...this.state.localData, [newKey]: '' };
+ this.setState({ localData: newData }, () => this.props.onChange(newData));
+ };
+
+ convertToObject = (key: string) => {
+ if (
+ typeof this.state.localData[key] === 'object' &&
+ this.state.localData[key] !== null
+ ) {
+ return;
+ }
+ const newData = { ...this.state.localData, [key]: {} };
+ this.setState({ localData: newData }, () => this.props.onChange(newData));
+ this.setState(({ collapsedKeys }) => {
+ const newSet = new Set(collapsedKeys);
+ newSet.delete(key);
+ return { collapsedKeys: newSet };
+ });
+ };
+
+ renderValueEditor = (key: string, value: any) => {
+ const isMultiline =
+ key === 'callable' || (typeof value === 'string' &&
value.includes('\n'));
+
+ return isMultiline ? (
+ <textarea
+ value={value}
+ onChange={e => this.handleValueChange(key, e.target.value)}
+ className="editor-input"
+ style={{ minHeight: 100 }}
+ />
+ ) : (
+ <input
+ type="text"
+ value={value}
+ onChange={e => this.handleValueChange(key, e.target.value)}
+ className="editor-input"
+ />
+ );
+ };
+
+ render() {
+ const { localData, collapsedKeys } = this.state;
+ const depth = this.props.depth ?? 0;
+
+ return (
+ <div style={{ fontFamily: 'monospace', fontSize: 14 }}>
+ {Object.entries(localData).map(([key, value]) => {
+ const isObject =
+ typeof value === 'object' &&
+ value !== null &&
+ !Array.isArray(value);
+ const isCollapsed = collapsedKeys.has(key);
+
+ return (
+ <div key={key} style={{ marginBottom: 4 }}>
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
+ {/* Toggle Button or Spacer */}
+ {isObject ? (
+ <button
+ onClick={() => this.toggleCollapse(key)}
+ style={{
+ width: 24,
+ height: 32,
+ cursor: 'pointer',
+ border: 'none',
+ background: 'none',
+ fontWeight: 'bold',
+ userSelect: 'none',
+ flexShrink: 0
+ }}
+ >
+ {isCollapsed ? 'βΆ' : 'βΌ'}
+ </button>
+ ) : (
+ <div style={{ width: 24, height: 32 }} />
+ )}
+
+ {/* Key input */}
+ <input
+ type="text"
+ value={key}
+ onChange={e => this.handleKeyChange(key, e.target.value)}
+ className="editor-input"
+ style={{
+ width: 120,
+ height: 32,
+ boxSizing: 'border-box',
+ padding: '4px 6px',
+ borderRadius: 4,
+ border: '1px solid #ccc',
+ fontFamily: 'inherit',
+ fontSize: 'inherit'
+ }}
+ />
+
+ {/* Value input or collapsed preview */}
+ <div style={{ flexGrow: 1 }}>
+ {isObject ? (
+ isCollapsed ? (
+ <span style={{ color: '#888' }}>{'{...}'}</span>
+ ) : (
+ <span style={{ color: '#444', fontStyle: 'italic' }}>
+ {'{...}'}
+ </span>
+ )
+ ) : (
+ this.renderValueEditor(key, value)
+ )}
+ </div>
+
+ {/* Action buttons */}
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
+ {!isObject && (
+ <button
+ onClick={() => this.convertToObject(key)}
+ style={{
+ width: 70,
+ height: 32,
+ padding: '4px 8px',
+ borderRadius: 4,
+ border: '1px solid #4caf50',
+ backgroundColor: '#e8f5e9',
+ color: '#2e7d32',
+ cursor: 'pointer'
+ }}
+ >
+ + Sub
+ </button>
+ )}
+ <button
+ onClick={() => this.handleDelete(key)}
+ style={{
+ height: 32,
+ padding: '4px 8px',
+ borderRadius: 4,
+ border: '1px solid #f44336',
+ backgroundColor: '#ffebee',
+ color: '#b71c1c',
+ cursor: 'pointer'
+ }}
+ >
+ Γ
+ </button>
+ </div>
+ </div>
+
+ {isObject && !isCollapsed && (
+ <div
+ style={{
+ marginLeft: 20,
+ marginTop: 4,
+ borderLeft: '1px solid #ccc',
+ paddingLeft: 8
+ }}
+ >
+ <EditableKeyValuePanel
+ node={{ id: key, data: value } as Node}
+ onChange={newVal => this.handleValueChange(key, newVal)}
+ depth={depth + 1}
+ />
+ </div>
+ )}
+ </div>
+ );
+ })}
+
+ <button
+ onClick={this.handleAddPair}
+ style={{
+ marginTop: 8,
+ padding: '6px 12px',
+ borderRadius: 4,
+ border: '1px solid #2196f3',
+ backgroundColor: '#e3f2fd',
+ color: '#0d47a1',
+ cursor: 'pointer'
+ }}
+ >
+ + Add {depth > 0 ? 'Nested ' : ''}Field
+ </button>
+
+ {/* Reference Doc */}
+ {depth === 0 && (
+ <div
+ style={{
+ marginTop: 14,
+ padding: '14px 20px',
+ borderRadius: 12,
+ background: 'linear-gradient(135deg, #f7f9fc, #e3f0ff)',
+ border: '1px solid #4facfe',
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ fontFamily: 'sans-serif',
+ fontSize: 14,
+ color: '#0d47a1',
+ transition: 'transform 0.15s ease, box-shadow 0.15s ease'
+ }}
+ onMouseEnter={e => {
+ e.currentTarget.style.transform = 'translateY(-2px)';
+ e.currentTarget.style.boxShadow =
+ '0 6px 16px rgba(0, 0, 0, 0.12)';
+ }}
+ onMouseLeave={e => {
+ e.currentTarget.style.transform = 'translateY(0)';
+ e.currentTarget.style.boxShadow =
+ '0 4px 12px rgba(0, 0, 0, 0.08)';
+ }}
+ >
+ {/* Emoji + label */}
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
+ <div
+ style={{
+ fontSize: 22,
+ width: 28,
+ height: 28,
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center'
+ }}
+ >
+ {transformEmojiMap[localData.label || ''] || 'π'}
+ </div>
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 2
}}>
+ <span
+ style={{ fontWeight: 600, fontSize: 14, color: '#0d47a1' }}
+ >
+ {localData.label}
+ </span>
+ <span style={{ fontSize: 12, color: '#555' }}>
+ Reference for Beam YAML transform
+ </span>
+ </div>
+ </div>
+
+ {/* Button */}
+ <a
+
href={`https://beam.apache.org/releases/yamldoc/current/#${encodeURIComponent(
+ localData.label?.toLowerCase() || ''
+ )}`}
+ target="_blank"
+ rel="noopener noreferrer"
+ style={{
+ padding: '6px 14px',
+ borderRadius: 6,
+ backgroundColor: '#2196f3',
+ color: 'white',
+ fontWeight: 500,
+ fontSize: 13,
+ textDecoration: 'none',
+ boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
+ transition: 'all 0.2s ease'
+ }}
+ onMouseEnter={e => {
+ e.currentTarget.style.backgroundColor = '#1976d2';
+ e.currentTarget.style.transform = 'translateY(-1px)';
+ e.currentTarget.style.boxShadow = '0 4px 8px rgba(0,0,0,0.15)';
+ }}
+ onMouseLeave={e => {
+ e.currentTarget.style.backgroundColor = '#2196f3';
+ e.currentTarget.style.transform = 'translateY(0)';
+ e.currentTarget.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
+ }}
+ >
+ Open Doc
+ </a>
+ </div>
+ )}
+ </div>
+ );
+ }
+}
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/EmojiMap.ts
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/EmojiMap.ts
new file mode 100644
index 00000000000..ed6a9f2285c
--- /dev/null
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/EmojiMap.ts
@@ -0,0 +1,75 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy
of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
under
+// the License.
+
+// Emoji mapping for various data processing and I/O operations
+// Generate with AI
+export const transformEmojiMap: Record<string, string> = {
+ // Data processing operations
+ AnomalyDetection: 'π',
+ AssertEqual: 'βοΈ',
+ AssignTimestamps: 'β°',
+ Combine: 'π',
+ Create: 'β¨',
+ Enrichment: 'π',
+ Explode: 'π₯',
+ ExtractWindowingInfo: 'πͺ',
+ Filter: 'π',
+ Flatten: 'π',
+ Join: 'π€',
+ LogForTesting: 'π',
+ MLTransform: 'π€',
+ MapToFields: 'πΊοΈ',
+ Partition: 'π',
+ PyTransform: 'π',
+ RunInference: 'π§ ',
+ Sql: 'ποΈ',
+ StripErrorMetadata: 'π§Ή',
+ ValidateWithSchema: 'β
',
+ WindowInto: 'πͺ',
+
+ // I/O operations
+ ReadFromAvro: 'β¬οΈπ',
+ WriteToAvro: 'β¬οΈπ',
+ ReadFromBigQuery: 'β¬οΈπ',
+ WriteToBigQuery: 'β¬οΈπ',
+ WriteToBigTable: 'β¬οΈπ',
+ ReadFromCsv: 'β¬οΈπ',
+ WriteToCsv: 'β¬οΈπ',
+ ReadFromIceberg: 'β¬οΈπ§',
+ WriteToIceberg: 'β¬οΈπ§',
+ ReadFromJdbc: 'β¬οΈπ',
+ WriteToJdbc: 'β¬οΈπ',
+ ReadFromJson: 'β¬οΈπ',
+ WriteToJson: 'β¬οΈπ',
+ ReadFromKafka: 'β¬οΈπ¬',
+ WriteToKafka: 'β¬οΈπ¬',
+ ReadFromMySql: 'β¬οΈπ¬',
+ WriteToMySql: 'β¬οΈπ¬',
+ ReadFromOracle: 'β¬οΈποΈ',
+ WriteToOracle: 'β¬οΈποΈ',
+ ReadFromParquet: 'β¬οΈπ¦',
+ WriteToParquet: 'β¬οΈπ¦',
+ ReadFromPostgres: 'β¬οΈπ',
+ WriteToPostgres: 'β¬οΈπ',
+ ReadFromPubSub: 'β¬οΈπ’',
+ WriteToPubSub: 'β¬οΈπ’',
+ ReadFromPubSubLite: 'β¬οΈπ£',
+ WriteToPubSubLite: 'β¬οΈπ£',
+ ReadFromSpanner: 'β¬οΈπ',
+ WriteToSpanner: 'β¬οΈπ',
+ ReadFromSqlServer: 'β¬οΈποΈ',
+ WriteToSqlServer: 'β¬οΈποΈ',
+ ReadFromTFRecord: 'β¬οΈπΌ',
+ WriteToTFRecord: 'β¬οΈπΌ',
+ ReadFromText: 'β¬οΈπ',
+ WriteToText: 'β¬οΈπ'
+};
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/Yaml.tsx
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/Yaml.tsx
new file mode 100644
index 00000000000..a004f86bdf5
--- /dev/null
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/Yaml.tsx
@@ -0,0 +1,322 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy
of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
under
+// the License.
+
+import React from 'react';
+import { ISessionContext } from '@jupyterlab/apputils';
+import { Card } from '@rmwc/card';
+import '@rmwc/textfield/styles';
+import '@rmwc/grid/styles';
+import '@rmwc/card/styles';
+import Split from 'react-split';
+import { debounce } from 'lodash';
+
+import { Node, Edge } from '@xyflow/react';
+import '@xyflow/react/dist/style.css';
+import '../../style/yaml/Yaml.css';
+
+import { YamlEditor } from './YamlEditor';
+import { EditableKeyValuePanel } from './EditablePanel';
+import { FlowEditor } from './YamlFlow';
+import { IApiResponse } from './DataType';
+import { nodeWidth, nodeHeight } from './DataType';
+
+interface IYamlProps {
+ sessionContext: ISessionContext;
+}
+
+interface IYamlState {
+ yamlContent: string;
+ elements: any;
+ selectedNode: Node | null;
+ errors: string[];
+ isDryRunMode: boolean;
+ Nodes: Node[];
+ Edges: Edge[];
+}
+
+const initialNodes: Node[] = [
+ {
+ id: '0',
+ width: nodeWidth,
+ position: { x: 0, y: 0 },
+ type: 'input',
+ data: { label: 'Input' }
+ },
+ {
+ id: '1',
+ width: nodeWidth,
+ position: { x: 0, y: 100 },
+ type: 'default',
+ data: { label: '1' }
+ },
+ {
+ id: '2',
+ width: nodeWidth,
+ position: { x: 0, y: 200 },
+ type: 'default',
+ data: { label: '2' }
+ },
+ {
+ id: '3',
+ width: nodeWidth,
+ position: { x: 0, y: 300 },
+ type: 'output',
+ data: { label: 'Output' }
+ }
+];
+
+const initialEdges: Edge[] = [
+ { id: 'e0-1', source: '0', target: '1' },
+ { id: 'e1-2', source: '1', target: '2' },
+ { id: 'e2-3', source: '2', target: '3' }
+];
+
+/**
+ * A YAML pipeline editor component with integrated flow visualization.
+ *
+ * Features:
+ * - Three-panel layout with YAML editor, flow diagram, and node properties
+ * - Real-time YAML validation and error display
+ * - Automatic flow diagram generation from YAML content
+ * - Interactive node selection and editing
+ * - Dry run mode support for pipeline testing
+ * - Kernel-based YAML parsing using Apache Beam utilities
+ * - Debounced updates to optimize performance
+ * - Split-pane resizable interface
+ *
+ * State Management:
+ * - yamlContent: Current YAML text content
+ * - elements: Combined nodes and edges for the flow diagram
+ * - selectedNode: Currently selected node in the flow diagram
+ * - errors: Array of validation errors
+ * - Nodes: Array of flow nodes
+ * - Edges: Array of flow edges
+ * - isDryRunMode: Flag for dry run mode state
+ *
+ * Props:
+ * @param {IYamlProps} props - Component props including
+ * sessionContext for kernel communication
+ *
+ * Methods:
+ * - handleNodeClick: Handles node selection in the flow diagram
+ * - handleYamlChange: Debounced handler for YAML content changes
+ * - validateAndRenderYaml: Validates YAML and updates the flow diagram
+ */
+export class Yaml extends React.Component<IYamlProps, IYamlState> {
+ constructor(props: IYamlProps) {
+ super(props);
+ this.state = {
+ yamlContent: '',
+ elements: [],
+ selectedNode: null,
+ errors: [],
+ Nodes: initialNodes,
+ Edges: initialEdges,
+ isDryRunMode: false
+ };
+ }
+
+ componentDidMount(): void {
+ this.props.sessionContext.ready.then(() => {
+ const kernel = this.props.sessionContext.session?.kernel;
+ if (!kernel) {
+ console.error('Kernel is not available even after ready');
+ return;
+ }
+
+ console.log('Kernel is ready:', kernel.name);
+ });
+ }
+
+ handleNodeClick = (node: Node) => {
+ this.setState({
+ selectedNode: node
+ });
+ };
+
+ //debounce methods to prevent excessive rendering
+ private handleYamlChange = debounce((value?: string) => {
+ const yamlText = value || '';
+ this.setState({ yamlContent: yamlText });
+ this.validateAndRenderYaml(yamlText);
+ }, 2000);
+
+ validateAndRenderYaml(yamlText: string) {
+ const escapedYaml = yamlText.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
+ const code = `
+from apache_beam_jupyterlab_sidepanel.yaml_parse_utils import parse_beam_yaml
+print(parse_beam_yaml("""${escapedYaml}""",
+ isDryRunMode=${this.state.isDryRunMode ? 'True' : 'False'}))
+`.trim();
+ const session = this.props.sessionContext.session;
+ if (!session?.kernel) {
+ console.error('No kernel available');
+ return;
+ }
+
+ // Clear previous state immediately
+ this.setState({
+ Nodes: [],
+ Edges: [],
+ selectedNode: null,
+ errors: []
+ });
+
+ const future = session.kernel.requestExecute({ code });
+
+ // Handle kernel execution results
+ future.onIOPub = msg => {
+ if (msg.header.msg_type === 'stream') {
+ const content = msg.content as { name: string; text: string };
+ const output = content.text.trim();
+
+ try {
+ const result: IApiResponse = JSON.parse(output);
+
+ if (result.error) {
+ this.setState({
+ elements: [],
+ selectedNode: null,
+ errors: [result.error]
+ });
+ } else {
+ const flowNodes: Node[] = result.data.nodes.map(node => ({
+ id: node.id,
+ type: node.type,
+ width: nodeWidth,
+ height: nodeHeight,
+ position: { x: 0, y: 0 }, // Will be auto-layouted
+ data: {
+ label: node.label,
+ ...node // include all original properties
+ }
+ }));
+
+ // Transform edges for React Flow
+ const flowEdges: Edge[] = result.data.edges.map(edge => ({
+ id: `${edge.source}-${edge.target}`,
+ source: edge.source,
+ target: edge.target,
+ animated: edge.label === 'pipeline_entry',
+ label: edge.label,
+ type: 'default' // or your custom edge type
+ }));
+
+ this.setState({
+ Nodes: flowNodes,
+ Edges: flowEdges,
+ errors: []
+ });
+ }
+ } catch (err) {
+ this.setState({
+ elements: [],
+ selectedNode: null,
+ errors: [output]
+ });
+ }
+ }
+ };
+ }
+
+ render(): React.ReactNode {
+ return (
+ <div
+ style={{
+ height: '100vh',
+ width: '100vw'
+ }}
+ >
+ <Split
+ direction="horizontal"
+ sizes={[25, 35, 20]} // L/C/R
+ minSize={100}
+ gutterSize={6}
+ className="split-pane"
+ >
+ {/* Left */}
+ <div
+ style={{
+ height: '100%',
+ overflow: 'auto',
+ minWidth: '100px'
+ }}
+ >
+ <YamlEditor
+ value={this.state.yamlContent}
+ onChange={value => {
+ // Clear old errors & Handle new errors
+ this.setState({ errors: [] });
+ this.handleYamlChange(value || '');
+ }}
+ errors={this.state.errors}
+ showConsole={true}
+ onDryRunModeChange={newValue =>
+ this.setState({ isDryRunMode: newValue })
+ }
+ />
+ </div>
+
+ <div
+ style={{
+ padding: '1rem',
+ height: '100%'
+ }}
+ >
+ <Card
+ style={{
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column'
+ }}
+ >
+ <div
+ className="w-full h-full
+ bg-gray-50 dark:bg-zinc-900
+ text-sm font-sans"
+ style={{ flex: 1, minHeight: 0 }}
+ >
+ <FlowEditor
+ Nodes={this.state.Nodes}
+ Edges={this.state.Edges}
+ onNodesUpdate={(nodes: Node[]) =>
+ this.setState({ Nodes: nodes })
+ }
+ onEdgesUpdate={(edges: Edge[]) =>
+ this.setState({ Edges: edges })
+ }
+ onNodeClick={this.handleNodeClick}
+ />
+ </div>
+ </Card>
+ </div>
+
+ {/* Right */}
+ <div
+ style={{
+ height: '100%',
+ overflow: 'auto',
+ padding: '12px',
+ boxSizing: 'border-box'
+ }}
+ >
+ <EditableKeyValuePanel
+ node={this.state.selectedNode}
+ // TODO: implement onChange to update node data from Panel
+ onChange={() => {}}
+ />
+ </div>
+ </Split>
+ </div>
+ );
+ }
+}
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlEditor.tsx
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlEditor.tsx
new file mode 100644
index 00000000000..8bbedc4b547
--- /dev/null
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlEditor.tsx
@@ -0,0 +1,338 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy
of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
under
+// the License.
+
+import React, { useState, useRef } from 'react';
+import Editor from '@monaco-editor/react';
+interface IYamlEditorProps {
+ value: string;
+ onChange: (value: string | undefined) => void;
+ onDryRunModeChange: (value: boolean) => void;
+ errors?: string[];
+ showConsole?: boolean;
+ consoleHeight?: number;
+}
+
+/**
+ * A YAML editor component built with React and Monaco Editor.
+ *
+ * Features:
+ * - Full YAML syntax highlighting and validation
+ * - Adjustable font size (10px-24px) with zoom controls
+ * - Word wrap toggle
+ * - File download and upload capabilities
+ * - Dry run mode toggle for testing
+ * - Error console with detailed validation messages
+ *
+ * Props:
+ * @param {string} value - Current YAML content
+ * @param {(isDryRun: boolean) => void} onDryRunModeChange -
+ * Callback when dry run mode changes
+ * @param {(value: string) => void} onChange - Callback when content changes
+ * @param {string[]} [errors=[]] - Array of error messages to display
+ * @param {boolean} [showConsole=true] - Whether to show the error console
+ * @param {number} [consoleHeight=200] - Height of the error console in pixels
+ */
+export const YamlEditor: React.FC<IYamlEditorProps> = ({
+ value,
+ onDryRunModeChange,
+ onChange,
+ errors = [],
+ showConsole = true,
+ consoleHeight = 200
+}) => {
+ const [fontSize, setFontSize] = useState(14);
+ const [wordWrap, setWordWrap] = useState(true);
+ const [isDryRunMode, setDryRunMode] = useState(false);
+ const [isFontMenuOpen, setFontMenuOpen] = useState<boolean>(false);
+ const fileInputRef = useRef<HTMLInputElement>(null);
+
+ const zoomIn = () => setFontSize(prev => Math.min(prev + 1, 24));
+ const zoomOut = () => setFontSize(prev => Math.max(prev - 1, 10));
+ const handleFontSizeChange = (e: React.ChangeEvent<HTMLInputElement>) =>
+ setFontSize(Number(e.target.value));
+
+ // Download YAML File
+ const handleDownload = () => {
+ const blob = new Blob([value], { type: 'text/yaml;charset=utf-8' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = 'pipeline.yaml';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ };
+
+ // Open YAML File
+ const handleOpenFile = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const file = e.target.files?.[0];
+ if (!file) {
+ return;
+ }
+ const reader = new FileReader();
+ reader.onload = ev => {
+ const content = ev.target?.result;
+ if (typeof content === 'string') {
+ onChange(content);
+ }
+ };
+ reader.readAsText(file);
+ e.target.value = '';
+ };
+
+ return (
+ <div
+ style={{
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ background: '#1e1e1e'
+ }}
+ >
+ {/* ================ Toolbar ================ */}
+ <div
+ style={{
+ padding: '8px',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ borderBottom: '1px solid #333',
+ gap: 8
+ }}
+ >
+ {/* Fonts */}
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+ <button
+ onClick={() => setFontMenuOpen(!isFontMenuOpen)}
+ style={{
+ background: 'transparent',
+ border: '1px solid #555',
+ color: '#ddd',
+ padding: '4px 8px',
+ borderRadius: '4px',
+ cursor: 'pointer'
+ }}
+ >
+ Font {fontSize}px
+ </button>
+ {isFontMenuOpen && (
+ <div
+ style={{
+ position: 'absolute',
+ top: '40px',
+ left: '8px',
+ background: '#252526',
+ padding: '8px',
+ borderRadius: '4px',
+ boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
+ zIndex: 100
+ }}
+ >
+ <div
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ marginBottom: 8
+ }}
+ >
+ <button
+ onClick={zoomOut}
+ style={{
+ background: '#333',
+ border: 'none',
+ color: '#fff',
+ width: 24,
+ height: 24,
+ borderRadius: 4,
+ cursor: 'pointer'
+ }}
+ >
+ -
+ </button>
+ <input
+ type="range"
+ min="10"
+ max="24"
+ value={fontSize}
+ onChange={handleFontSizeChange}
+ style={{ margin: '0 8px', width: 100 }}
+ />
+ <button
+ onClick={zoomIn}
+ style={{
+ background: '#333',
+ border: 'none',
+ color: '#fff',
+ width: 24,
+ height: 24,
+ borderRadius: 4,
+ cursor: 'pointer'
+ }}
+ >
+ +
+ </button>
+ </div>
+ <div style={{ color: '#999', fontSize: 12 }}>
+ Current: {fontSize}px
+ </div>
+ </div>
+ )}
+ </div>
+
+ {/* Autowrap & DryRun */}
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
+ <label
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ color: '#ddd',
+ cursor: 'pointer'
+ }}
+ >
+ <input
+ type="checkbox"
+ checked={wordWrap}
+ onChange={() => setWordWrap(!wordWrap)}
+ style={{ marginRight: 6 }}
+ />{' '}
+ Autowrap
+ </label>
+ <label
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ color: '#ddd',
+ cursor: 'pointer'
+ }}
+ >
+ <input
+ type="checkbox"
+ checked={isDryRunMode}
+ onChange={() => {
+ onDryRunModeChange(!isDryRunMode);
+ setDryRunMode(!isDryRunMode);
+ }}
+ style={{ marginRight: 6 }}
+ />{' '}
+ Dry Run Mode
+ </label>
+ </div>
+
+ {/* Download / Open */}
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
+ <button
+ onClick={handleDownload}
+ style={{
+ padding: '4px 10px',
+ borderRadius: 4,
+ border: '1px solid #2196f3',
+ background: '#2196f3',
+ color: '#fff',
+ cursor: 'pointer'
+ }}
+ >
+ Download
+ </button>
+ <button
+ onClick={() => fileInputRef.current?.click()}
+ style={{
+ padding: '4px 10px',
+ borderRadius: 4,
+ border: '1px solid #4caf50',
+ background: '#4caf50',
+ color: '#fff',
+ cursor: 'pointer'
+ }}
+ >
+ Open
+ </button>
+ <input
+ ref={fileInputRef}
+ type="file"
+ accept=".yaml,.yml"
+ style={{ display: 'none' }}
+ onChange={handleOpenFile}
+ />
+ </div>
+ </div>
+ {/* ================ ~Toolbar ================ */}
+
+ {/* ================ Code Editor ================ */}
+ <div style={{ flex: 1, minHeight: 0 }}>
+ <Editor
+ height="98%"
+ language="yaml"
+ value={value}
+ theme="vs-dark"
+ onChange={onChange}
+ options={{
+ fontSize,
+ fontFamily: 'monospace',
+ wordWrap: wordWrap ? 'on' : 'off',
+ minimap: { enabled: false },
+ scrollBeyondLastLine: false,
+ automaticLayout: true,
+ lineNumbers: 'on',
+ tabSize: 2
+ }}
+ />
+ </div>
+ {/* ================ ~Code Editor ================ */}
+
+ {/* ================ Console ================ */}
+ {showConsole && (
+ <div
+ style={{
+ height: consoleHeight,
+ background: '#1e1e1e',
+ borderTop: '1px solid #ff6b6b',
+ overflow: 'auto',
+ padding: 12,
+ fontFamily: 'monospace',
+ fontSize: 13,
+ whiteSpace: 'pre'
+ }}
+ >
+ <div
+ style={{
+ color: errors.length ? '#f48771' : '#888',
+ marginBottom: errors.length ? 12 : 0,
+ fontWeight: 500
+ }}
+ >
+ {errors.length ? 'β' : 'β
YAML Format Correct'}
+ </div>
+ {errors.map((error, i) => (
+ <pre
+ key={i}
+ style={{
+ color: '#f48771',
+ margin: '8px 0',
+ padding: 0,
+ backgroundColor: 'transparent',
+ border: 'none',
+ overflow: 'visible',
+ whiteSpace: 'pre-wrap',
+ wordBreak: 'break-all',
+ fontFamily: 'inherit'
+ }}
+ >
+ {error}
+ </pre>
+ ))}
+ </div>
+ )}
+ {/* ================ ~Console ================ */}
+ </div>
+ );
+};
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlFlow.tsx
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlFlow.tsx
new file mode 100644
index 00000000000..0db9dbad14f
--- /dev/null
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlFlow.tsx
@@ -0,0 +1,227 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy
of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
under
+// the License.
+
+import React, { useEffect, useCallback, useMemo } from 'react';
+import {
+ ReactFlow,
+ useNodesState,
+ useEdgesState,
+ addEdge,
+ MiniMap,
+ Controls,
+ Background,
+ Panel,
+ Node,
+ Edge,
+ applyNodeChanges,
+ NodeChange,
+ Connection
+} from '@xyflow/react';
+import { debounce } from 'lodash';
+import dagre from 'dagre';
+import {
+ DefaultNode,
+ InputNode,
+ OutputNode,
+ AnimatedSVGEdge
+} from './CustomStyle';
+import { nodeWidth, nodeHeight } from './DataType';
+
+import '@xyflow/react/dist/style.css';
+
+interface IFlowEditorProps {
+ Nodes: Node[];
+ Edges: Edge[];
+ onNodesUpdate?: (nodes: Node[]) => void;
+ onEdgesUpdate?: (edges: Edge[]) => void;
+ onNodeClick?: (node: Node) => void;
+ debounceWait?: number;
+}
+
+/**
+ * A flow diagram editor component built with React Flow.
+ *
+ * Features:
+ * - Interactive node-based flow diagram editor
+ * - Auto-layout functionality using Dagre graph layout
+ * - Support for different node types (default, input, output)
+ * - Animated edge connections
+ * - Mini-map and controls for navigation
+ * - Debounced updates to optimize performance
+ * - Real-time node and edge manipulation
+ *
+ * Props:
+ * @param {Node[]} Nodes - Initial array of nodes
+ * @param {Edge[]} Edges - Initial array of edges
+ * @param {(nodes: Node[]) => void} onNodesUpdate -
+ * Callback when nodes are updated
+ * @param {(edges: Edge[]) => void} onEdgesUpdate -
+ * Callback when edges are updated
+ * @param {(event: React.MouseEvent, node: Node) => void} onNodeClick -
+ * Callback when a node is clicked
+ * @param {number} [debounceWait=500] -
+ * Debounce wait time in milliseconds for updates
+ */
+export const FlowEditor: React.FC<IFlowEditorProps> = ({
+ Nodes,
+ Edges,
+ onNodesUpdate,
+ onEdgesUpdate,
+ onNodeClick,
+ debounceWait = 500 //Default debounce wait time
+}: IFlowEditorProps) => {
+ const [nodes, setNodes, _] = useNodesState(Nodes);
+ const [edges, setEdges, onEdgesChange] = useEdgesState(Edges);
+
+ void _;
+ // Debounce callback
+ const debouncedNodesUpdate = useMemo(
+ () =>
+ debounce((nodes: Node[]) => {
+ onNodesUpdate?.(nodes);
+ }, debounceWait),
+ [onNodesUpdate, debounceWait]
+ );
+
+ const debouncedEdgesUpdate = useMemo(
+ () =>
+ debounce((edges: Edge[]) => {
+ onEdgesUpdate?.(edges);
+ }, debounceWait),
+ [onEdgesUpdate, debounceWait]
+ );
+
+ const onConnect = useCallback(
+ (params: Connection) => setEdges(eds => addEdge(params, eds)),
+ [setEdges]
+ );
+
+ // Listen initialNodes/initialEdges changes and update state accordingly
+ useEffect(() => {
+ setNodes(Nodes);
+ }, [Nodes]);
+
+ useEffect(() => {
+ setEdges(Edges);
+ }, [Edges]);
+
+ const handleNodeClick = useCallback(
+ (event: React.MouseEvent, node: Node) => {
+ onNodeClick?.(node);
+ },
+ [onNodeClick]
+ );
+
+ const applyAutoLayout = useCallback(
+ (nodes: Node[], edges: Edge[]): Node[] => {
+ const g = new dagre.graphlib.Graph();
+ g.setDefaultEdgeLabel(() => ({}));
+ g.setGraph({
+ rankdir: 'TB',
+ ranksep: 100
+ }); // TB = Top to Bottom. Use LR for Left to Right
+
+ nodes.forEach(node => {
+ g.setNode(node.id, { width: nodeWidth, height: nodeHeight });
+ });
+
+ edges.forEach(edge => {
+ g.setEdge(edge.source, edge.target);
+ });
+
+ dagre.layout(g);
+
+ return nodes.map(node => {
+ const pos = g.node(node.id);
+ return {
+ ...node,
+ position: {
+ x: pos.x - nodeWidth / 2,
+ y: pos.y - nodeHeight / 2
+ }
+ };
+ });
+ },
+ []
+ );
+
+ const handleNodesChange = useCallback(
+ (changes: NodeChange[]) => {
+ const updatedNodes = applyNodeChanges(changes, nodes);
+
+ // Judge whether the node data is changed or not
+ // except position / dragging / selectedοΌ
+ const structuralChanges = changes.filter(
+ c => !['position', 'dragging', 'selected', 'select'].includes(c.type)
+ );
+
+ if (structuralChanges.length > 0) {
+ // Only when there are structural changes, we apply auto-layout
+ const autoLayouted = applyAutoLayout(updatedNodes, edges);
+ setNodes(autoLayouted);
+ onNodesUpdate?.(autoLayouted);
+ } else {
+ // Dragging or selection changes, just update normally
+ setNodes(updatedNodes);
+ }
+ },
+ [nodes, edges, onNodesUpdate, applyAutoLayout]
+ );
+
+ const NodeTypes = {
+ default: DefaultNode,
+ input: InputNode,
+ output: OutputNode
+ };
+
+ // notify parent whenever nodes/edges change
+ useEffect(() => {
+ debouncedNodesUpdate(nodes);
+ return () => debouncedNodesUpdate.cancel();
+ }, [nodes, debouncedNodesUpdate]);
+
+ useEffect(() => {
+ debouncedEdgesUpdate(edges);
+ return () => debouncedEdgesUpdate.cancel();
+ }, [edges, debouncedEdgesUpdate]);
+
+ return (
+ <ReactFlow
+ nodes={nodes}
+ edges={edges}
+ nodeTypes={NodeTypes}
+ edgeTypes={{ default: AnimatedSVGEdge }}
+ onNodesChange={handleNodesChange}
+ onEdgesChange={onEdgesChange}
+ onConnect={onConnect}
+ onNodeClick={handleNodeClick}
+ fitView
+ >
+ <Panel position="top-right">
+ <button
+ onClick={() => {
+ const newNodes = applyAutoLayout(nodes, edges);
+ setNodes(newNodes);
+ }}
+ className="px-3 py-1
+ bg-blue-600 text-white rounded-md shadow
+ hover:bg-blue-700 transition-all duration-200"
+ >
+ Auto Layout
+ </button>
+ </Panel>
+ <MiniMap />
+ <Controls />
+ <Background />
+ </ReactFlow>
+ );
+};
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlWidget.tsx
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlWidget.tsx
new file mode 100644
index 00000000000..191e4b38016
--- /dev/null
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlWidget.tsx
@@ -0,0 +1,34 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy
of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
under
+// the License.
+
+import * as React from 'react';
+
+import { ISessionContext, ReactWidget } from '@jupyterlab/apputils';
+
+import { Yaml } from './Yaml';
+
+/**
+ * Converts the React component Clusters into a lumino widget used
+ * in Jupyter labextensions.
+ */
+export class YamlWidget extends ReactWidget {
+ constructor(sessionContext: ISessionContext) {
+ super();
+ this._sessionContext = sessionContext;
+ }
+
+ protected render(): React.ReactElement<any> {
+ return <Yaml sessionContext={this._sessionContext} />;
+ }
+
+ private _sessionContext: ISessionContext;
+}
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
index 1b2227845b6..1a158f9bfe4 100644
---
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
@@ -18,3 +18,6 @@
@import './inspector/Inspectables.css';
@import './inspector/InspectableView.css';
@import './inspector/InteractiveInspector.css';
+@import './yaml/Yaml.css';
+@import './yaml/YamlEditor.css';
+@import './yaml/YamlFlow.css';
\ No newline at end of file
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/mdc-theme.css
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/mdc-theme.css
index b6383f96512..be39963782a 100644
---
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/mdc-theme.css
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/mdc-theme.css
@@ -49,11 +49,11 @@
color: var(--jp-ui-font-color1);
}
-.mdc-form-field > label {
+.mdc-form-field>label {
margin-bottom: 0;
}
.mdc-text-field {
margin-left: 4px;
margin-right: 4px;
-}
+}
\ No newline at end of file
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/Yaml.css
similarity index 58%
copy from
sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
copy to
sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/Yaml.css
index 1b2227845b6..3947022fc7f 100644
---
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/Yaml.css
@@ -11,10 +11,30 @@
* License for the specific language governing permissions and limitations
under
* the License.
*/
-@import '~material-design-icons/iconfont/material-icons.css';
-@import './mdc-theme.css';
+.split-pane {
+ height: 90%;
+ display: flex;
+}
-@import './inspector/Inspectables.css';
-@import './inspector/InspectableView.css';
-@import './inspector/InteractiveInspector.css';
+.gutter {
+ background-color: #ccc;
+ background-clip: padding-box;
+ box-sizing: border-box;
+ z-index: 1;
+ transition: background-color 0.2s;
+}
+
+.gutter:hover {
+ background-color: #aaa;
+}
+
+.gutter.gutter-horizontal {
+ cursor: col-resize;
+ width: 6px;
+}
+
+.gutter.gutter-vertical {
+ cursor: row-resize;
+ height: 6px;
+}
\ No newline at end of file
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/YamlEditor.css
similarity index 61%
copy from
sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
copy to
sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/YamlEditor.css
index 1b2227845b6..b1b45a7b641 100644
---
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/YamlEditor.css
@@ -11,10 +11,26 @@
* License for the specific language governing permissions and limitations
under
* the License.
*/
-@import '~material-design-icons/iconfont/material-icons.css';
-@import './mdc-theme.css';
+input {
+ border: 1px solid #ccc;
+ padding: 4px 6px;
+ border-radius: 4px;
+}
-@import './inspector/Inspectables.css';
-@import './inspector/InspectableView.css';
-@import './inspector/InteractiveInspector.css';
+button {
+ cursor: pointer;
+ background: none;
+ border: none;
+ font-size: 1.1rem;
+}
+
+.editor-input {
+ width: 100%;
+ height: 20px;
+ padding: 4px 6px;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+ font-family: inherit;
+ font-size: inherit;
+}
\ No newline at end of file
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/YamlFlow.css
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/YamlFlow.css
new file mode 100644
index 00000000000..091e3e211d7
--- /dev/null
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/YamlFlow.css
@@ -0,0 +1,168 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+ * use this file except in compliance with the License. You may obtain a copy
of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+
+.custom-node {
+ background: linear-gradient(145deg, #ffffff, #e5e5e5);
+ border: 2px solid #ccc;
+ border-radius: 12px;
+ padding: 12px 16px;
+ width: 250px;
+ display: inline-block;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
+ transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s
ease;
+ cursor: pointer;
+ user-select: none;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+}
+
+.custom-node:hover {
+ transform: translateY(-3px) scale(1.02);
+ box-shadow: 0 6px 14px rgba(0, 0, 0, 0.18);
+ border-color: #4cafef;
+}
+
+.custom-node:active {
+ transform: scale(0.96);
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
+ border-color: #2196f3;
+}
+
+/* Header */
+.custom-node-header {
+ display: flex;
+ align-items: center;
+}
+
+.custom-node-icon {
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 22px;
+ margin-right: 10px;
+ box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.1);
+}
+
+.custom-node-title {
+ font-size: 16px;
+ font-weight: bold;
+ color: #333;
+ letter-spacing: 0.3px;
+ line-height: 1.3;
+ flex-grow: 1;
+ white-space: normal;
+}
+
+/* Handle */
+.custom-handle {
+ width: 40px !important;
+ height: 8px !important;
+ border-radius: 4px;
+ background: #00bcd4 !important;
+ border: none !important;
+ transition: background 0.2s ease;
+}
+
+.custom-handle:hover {
+ background: #008ba3 !important;
+}
+
+/* ====== Colors ====== */
+.custom-node-input {
+ border-color: #4caf50;
+}
+
+.custom-node-input .custom-node-icon {
+ background: linear-gradient(135deg, #e8f5e9, #a5d6a7);
+}
+
+.custom-node-default {
+ border-color: #2196f3;
+}
+
+.custom-node-default .custom-node-icon {
+ background: linear-gradient(135deg, #e3f2fd, #90caf9);
+}
+
+.custom-node-output {
+ border-color: #ff9800;
+}
+
+.custom-node-output .custom-node-icon {
+ background: linear-gradient(135deg, #fff3e0, #ffcc80);
+}
+
+/* ====== Input ====== */
+.custom-node-input {
+ border-color: #4caf50;
+ background: linear-gradient(145deg, #f1fbf3, #dcedc8);
+}
+
+.custom-node-input .custom-node-icon {
+ background: linear-gradient(135deg, #c8e6c9, #81c784);
+ color: #ffffff;
+ box-shadow: 0 0 8px rgba(76, 175, 80, 0.5) inset;
+ transition: box-shadow 0.3s ease, transform 0.2s ease;
+}
+
+.custom-node-input:hover .custom-node-icon {
+ box-shadow: 0 0 12px rgba(76, 175, 80, 0.7) inset;
+ transform: scale(1.05);
+}
+
+.custom-node-input:active .custom-node-icon {
+ transform: scale(0.95);
+}
+
+.custom-node-input .custom-handle {
+ background: #43a047 !important;
+}
+
+.custom-node-input .custom-handle:hover {
+ background: #2e7d32 !important;
+}
+
+/* ====== Output ====== */
+.custom-node-output {
+ border-color: #ff9800;
+ background: linear-gradient(145deg, #fff8f0, #ffe0b2);
+}
+
+.custom-node-output .custom-node-icon {
+ background: linear-gradient(135deg, #ffcc80, #ffb74d);
+ color: #ffffff;
+ box-shadow: 0 0 8px rgba(255, 152, 0, 0.5) inset;
+ transition: box-shadow 0.3s ease, transform 0.2s ease;
+}
+
+.custom-node-output:hover .custom-node-icon {
+ box-shadow: 0 0 12px rgba(255, 152, 0, 0.7) inset;
+ transform: scale(1.05);
+}
+
+.custom-node-output:active .custom-node-icon {
+ transform: scale(0.95);
+}
+
+.custom-node-output .custom-handle {
+ background: #fb8c00 !important;
+}
+
+.custom-node-output .custom-handle:hover {
+ background: #ef6c00 !important;
+}
\ No newline at end of file
diff --git
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/yarn.lock
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/yarn.lock
index 3a1e6924b85..58bedbf7192 100644
---
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/yarn.lock
+++
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/yarn.lock
@@ -1400,6 +1400,27 @@ __metadata:
languageName: node
linkType: hard
+"@material/button@npm:^14.0.0":
+ version: 14.0.0
+ resolution: "@material/button@npm:14.0.0"
+ dependencies:
+ "@material/density": ^14.0.0
+ "@material/dom": ^14.0.0
+ "@material/elevation": ^14.0.0
+ "@material/feature-targeting": ^14.0.0
+ "@material/focus-ring": ^14.0.0
+ "@material/ripple": ^14.0.0
+ "@material/rtl": ^14.0.0
+ "@material/shape": ^14.0.0
+ "@material/theme": ^14.0.0
+ "@material/tokens": ^14.0.0
+ "@material/touch-target": ^14.0.0
+ "@material/typography": ^14.0.0
+ tslib: ^2.1.0
+ checksum:
83b55dd1038b3fa98e685d31f4a20c3530bd152f4c2f2319ae553c9496955f3ab9a86bfd50f523018a28bbeaf49410cd689c2b25339eb8b718b1677f6fe7103b
+ languageName: node
+ linkType: hard
+
"@material/button@npm:^8.0.0":
version: 8.0.0
resolution: "@material/button@npm:8.0.0"
@@ -1417,6 +1438,22 @@ __metadata:
languageName: node
linkType: hard
+"@material/card@npm:^14.0.0":
+ version: 14.0.0
+ resolution: "@material/card@npm:14.0.0"
+ dependencies:
+ "@material/dom": ^14.0.0
+ "@material/elevation": ^14.0.0
+ "@material/feature-targeting": ^14.0.0
+ "@material/ripple": ^14.0.0
+ "@material/rtl": ^14.0.0
+ "@material/shape": ^14.0.0
+ "@material/theme": ^14.0.0
+ tslib: ^2.1.0
+ checksum:
e058ff89fadd4ef4cc28d7d8c9f6ac8152ba94430707a272d5cd2ebe4ebd4e481d00b42f97addf25cd49439b892d5979f0133bfb7bb090a322f99036cdb4d87f
+ languageName: node
+ linkType: hard
+
"@material/checkbox@npm:^8.0.0":
version: 8.0.0
resolution: "@material/checkbox@npm:8.0.0"
@@ -1460,6 +1497,15 @@ __metadata:
languageName: node
linkType: hard
+"@material/density@npm:^14.0.0":
+ version: 14.0.0
+ resolution: "@material/density@npm:14.0.0"
+ dependencies:
+ tslib: ^2.1.0
+ checksum:
94279ca2d7b75bbb998026da5fb7fbd68c830f986d25c4dcd5988a9f180bb600d8c1135c7828f844f1d74fa8919d817f44d10be03a18a4ccf81afe0f0d3810a9
+ languageName: node
+ linkType: hard
+
"@material/density@npm:^8.0.0":
version: 8.0.0
resolution: "@material/density@npm:8.0.0"
@@ -1528,6 +1574,20 @@ __metadata:
languageName: node
linkType: hard
+"@material/elevation@npm:^14.0.0":
+ version: 14.0.0
+ resolution: "@material/elevation@npm:14.0.0"
+ dependencies:
+ "@material/animation": ^14.0.0
+ "@material/base": ^14.0.0
+ "@material/feature-targeting": ^14.0.0
+ "@material/rtl": ^14.0.0
+ "@material/theme": ^14.0.0
+ tslib: ^2.1.0
+ checksum:
1cdc33f86b47d40dbc3f425f36dae20ebdfc5b8ce1b0307d4bbd30bc9680d17aff1af13399c733aeba7425cd97ff12197e14d21fa9e7fa6e3582f6f63ed0cb1a
+ languageName: node
+ linkType: hard
+
"@material/elevation@npm:^8.0.0":
version: 8.0.0
resolution: "@material/elevation@npm:8.0.0"
@@ -1590,6 +1650,17 @@ __metadata:
languageName: node
linkType: hard
+"@material/focus-ring@npm:^14.0.0":
+ version: 14.0.0
+ resolution: "@material/focus-ring@npm:14.0.0"
+ dependencies:
+ "@material/dom": ^14.0.0
+ "@material/feature-targeting": ^14.0.0
+ "@material/rtl": ^14.0.0
+ checksum:
61cbd9d2c449b7e743198c7dcae3181ee7488fdcefcd7666aff016f03b9a128b918f74cdf97862c189f6629e4cc654ea1325ca51f56751a20924fc7cb86914cb
+ languageName: node
+ linkType: hard
+
"@material/form-field@npm:^8.0.0":
version: 8.0.0
resolution: "@material/form-field@npm:8.0.0"
@@ -1605,6 +1676,25 @@ __metadata:
languageName: node
linkType: hard
+"@material/icon-button@npm:^14.0.0":
+ version: 14.0.0
+ resolution: "@material/icon-button@npm:14.0.0"
+ dependencies:
+ "@material/base": ^14.0.0
+ "@material/density": ^14.0.0
+ "@material/dom": ^14.0.0
+ "@material/elevation": ^14.0.0
+ "@material/feature-targeting": ^14.0.0
+ "@material/focus-ring": ^14.0.0
+ "@material/ripple": ^14.0.0
+ "@material/rtl": ^14.0.0
+ "@material/theme": ^14.0.0
+ "@material/touch-target": ^14.0.0
+ tslib: ^2.1.0
+ checksum:
99a7b5fd1882e45eb904fd6076975c6c6faeb50cabd4d17771232cd4b4f8696d1593accb102791265f602943a75eba99d8a89299afe0c0bc56b9abacd7dd4fb7
+ languageName: node
+ linkType: hard
+
"@material/icon-button@npm:^8.0.0":
version: 8.0.0
resolution: "@material/icon-button@npm:8.0.0"
@@ -1620,6 +1710,15 @@ __metadata:
languageName: node
linkType: hard
+"@material/layout-grid@npm:^14.0.0":
+ version: 14.0.0
+ resolution: "@material/layout-grid@npm:14.0.0"
+ dependencies:
+ tslib: ^2.1.0
+ checksum:
96a0748754fb034ab5a87cc7ed3642c9cb9b7812db034811599e12053ac39f06406f45d1f676bf3dbad597217b171313c0a314d20cad2c34fc93f08affc7307f
+ languageName: node
+ linkType: hard
+
"@material/line-ripple@npm:^8.0.0":
version: 8.0.0
resolution: "@material/line-ripple@npm:8.0.0"
@@ -1796,6 +1895,18 @@ __metadata:
languageName: node
linkType: hard
+"@material/shape@npm:^14.0.0":
+ version: 14.0.0
+ resolution: "@material/shape@npm:14.0.0"
+ dependencies:
+ "@material/feature-targeting": ^14.0.0
+ "@material/rtl": ^14.0.0
+ "@material/theme": ^14.0.0
+ tslib: ^2.1.0
+ checksum:
bcab9e26a10ff26880c99f86b31485f0b416330cd83e81bceeb9c4bc13fddc3a108eb3bbb011998da6ae1bf1f42707bb6095a804915728b10c746724e0b21607
+ languageName: node
+ linkType: hard
+
"@material/shape@npm:^8.0.0":
version: 8.0.0
resolution: "@material/shape@npm:8.0.0"
@@ -1866,6 +1977,15 @@ __metadata:
languageName: node
linkType: hard
+"@material/tokens@npm:^14.0.0":
+ version: 14.0.0
+ resolution: "@material/tokens@npm:14.0.0"
+ dependencies:
+ "@material/elevation": ^14.0.0
+ checksum:
666320dd0bde170e337ab8172153fa975381144cf46765f1fa898bfe7dad216a7437ee0b7f9c32ab4d8317a4096064596fe89fe5d9a90f047b14081392beb9d6
+ languageName: node
+ linkType: hard
+
"@material/top-app-bar@npm:^8.0.0":
version: 8.0.0
resolution: "@material/top-app-bar@npm:8.0.0"
@@ -1883,6 +2003,18 @@ __metadata:
languageName: node
linkType: hard
+"@material/touch-target@npm:^14.0.0":
+ version: 14.0.0
+ resolution: "@material/touch-target@npm:14.0.0"
+ dependencies:
+ "@material/base": ^14.0.0
+ "@material/feature-targeting": ^14.0.0
+ "@material/rtl": ^14.0.0
+ tslib: ^2.1.0
+ checksum:
2ad21fccf992d15b049ca5cf5ee954685cdcd64157f3bd36b44488af1f5363075c24a962274532f87377f3b5e0cc3ca929797bb213126467c1db415f7896bf86
+ languageName: node
+ linkType: hard
+
"@material/touch-target@npm:^8.0.0":
version: 8.0.0
resolution: "@material/touch-target@npm:8.0.0"
@@ -1893,6 +2025,17 @@ __metadata:
languageName: node
linkType: hard
+"@material/typography@npm:^14.0.0":
+ version: 14.0.0
+ resolution: "@material/typography@npm:14.0.0"
+ dependencies:
+ "@material/feature-targeting": ^14.0.0
+ "@material/theme": ^14.0.0
+ tslib: ^2.1.0
+ checksum:
24e52daaf1f94a32689b585e8e9cb9f09894cb0d9b3cbe4f8c19112fa59a1586b8b250f7dfaa7aa27b00158b0cd1184c89bcace40906557fbe59a94c65a45417
+ languageName: node
+ linkType: hard
+
"@material/typography@npm:^8.0.0":
version: 8.0.0
resolution: "@material/typography@npm:8.0.0"
@@ -1938,6 +2081,28 @@ __metadata:
languageName: node
linkType: hard
+"@monaco-editor/loader@npm:^1.5.0":
+ version: 1.5.0
+ resolution: "@monaco-editor/loader@npm:1.5.0"
+ dependencies:
+ state-local: ^1.0.6
+ checksum:
45e5f56ea9b1e5c16e3d40b05f8c365af830627d2aa8215c86cfac57384419c1b896927408c1261a12dc182a08419d4f20a0d0949d3e76ca42ccc68f4ffec508
+ languageName: node
+ linkType: hard
+
+"@monaco-editor/react@npm:^4.7.0":
+ version: 4.7.0
+ resolution: "@monaco-editor/react@npm:4.7.0"
+ dependencies:
+ "@monaco-editor/loader": ^1.5.0
+ peerDependencies:
+ monaco-editor: ">= 0.25.0 < 1"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ checksum:
8b3bd8adfcd6af70dc5f965e986932269e1e2c2a0f6beb5a3c632c8c7942c1341f6086d9664f9a949983bdf4a04a706e529a93bfec3b5884642915dfcc0354c3
+ languageName: node
+ linkType: hard
+
"@nodelib/fs.scandir@npm:2.1.5":
version: 2.1.5
resolution: "@nodelib/fs.scandir@npm:2.1.5"
@@ -2067,6 +2232,23 @@ __metadata:
languageName: node
linkType: hard
+"@rmwc/button@npm:14.3.5":
+ version: 14.3.5
+ resolution: "@rmwc/button@npm:14.3.5"
+ dependencies:
+ "@material/button": ^14.0.0
+ "@rmwc/base": 14.3.5
+ "@rmwc/icon": 14.3.5
+ "@rmwc/provider": 14.3.5
+ "@rmwc/ripple": 14.3.5
+ "@rmwc/types": 14.3.5
+ peerDependencies:
+ react: ">=16.8.x"
+ react-dom: ">=16.8.x"
+ checksum:
0bbd2e78e3c89ca660dfd2b114e987d5afa8ac242999ad3e0d5a19943efa6f02a728f9922285a8e6bc13ce640c706bdf182bf00a57f79e2bd395831c56546209
+ languageName: node
+ linkType: hard
+
"@rmwc/button@npm:^8.0.6, @rmwc/button@npm:^8.0.8":
version: 8.0.8
resolution: "@rmwc/button@npm:8.0.8"
@@ -2084,6 +2266,23 @@ __metadata:
languageName: node
linkType: hard
+"@rmwc/card@npm:^14.3.5":
+ version: 14.3.5
+ resolution: "@rmwc/card@npm:14.3.5"
+ dependencies:
+ "@material/card": ^14.0.0
+ "@rmwc/base": 14.3.5
+ "@rmwc/button": 14.3.5
+ "@rmwc/icon-button": 14.3.5
+ "@rmwc/ripple": 14.3.5
+ "@rmwc/types": 14.3.5
+ peerDependencies:
+ react: ">=16.8.x"
+ react-dom: ">=16.8.x"
+ checksum:
2177e30d444a367045deba77f4dc1e287950ea6bc407ec69964ba362baad1069d1138dd0ad3ef996ae982125449b52ee865746895ab3ae2b1cc536c383928497
+ languageName: node
+ linkType: hard
+
"@rmwc/checkbox@npm:^8.0.8":
version: 8.0.8
resolution: "@rmwc/checkbox@npm:8.0.8"
@@ -2195,6 +2394,36 @@ __metadata:
languageName: node
linkType: hard
+"@rmwc/grid@npm:^14.3.5":
+ version: 14.3.5
+ resolution: "@rmwc/grid@npm:14.3.5"
+ dependencies:
+ "@material/layout-grid": ^14.0.0
+ "@rmwc/base": 14.3.5
+ "@rmwc/types": 14.3.5
+ peerDependencies:
+ react: ">=16.8.x"
+ react-dom: ">=16.8.x"
+ checksum:
6466a9bb4d8b533ae09df4b320f7bc6be7c6e9274a11a47a08174b9e7cdbe6e9669b513d292b55391fe387609b9839a5bae36d9659a660f9be989461c677e65a
+ languageName: node
+ linkType: hard
+
+"@rmwc/icon-button@npm:14.3.5":
+ version: 14.3.5
+ resolution: "@rmwc/icon-button@npm:14.3.5"
+ dependencies:
+ "@material/icon-button": ^14.0.0
+ "@rmwc/base": 14.3.5
+ "@rmwc/icon": 14.3.5
+ "@rmwc/ripple": 14.3.5
+ "@rmwc/types": 14.3.5
+ peerDependencies:
+ react: ">=16.8.x"
+ react-dom: ">=16.8.x"
+ checksum:
ae8e32bf5fb9fb6ba2abec6b34deea6a3d09868766e5ff092d0ba44fa199492c2353f1e8a6a715c4a8a9c3abfa76b7854e2003d41767a5822cd00ccf6492bc01
+ languageName: node
+ linkType: hard
+
"@rmwc/icon-button@npm:^8.0.8":
version: 8.0.8
resolution: "@rmwc/icon-button@npm:8.0.8"
@@ -2211,6 +2440,20 @@ __metadata:
languageName: node
linkType: hard
+"@rmwc/icon@npm:14.3.5":
+ version: 14.3.5
+ resolution: "@rmwc/icon@npm:14.3.5"
+ dependencies:
+ "@rmwc/base": 14.3.5
+ "@rmwc/provider": 14.3.5
+ "@rmwc/types": 14.3.5
+ peerDependencies:
+ react: ">=16.8.x"
+ react-dom: ">=16.8.x"
+ checksum:
335afea39b5421863bd4bc460005c4d0f3d83a5febc4bfac256dcef5e2c255e5594853bec876adc81e94c3693ad5396cd63d1e14af86e11ba1d2c4c3e7d65fc8
+ languageName: node
+ linkType: hard
+
"@rmwc/icon@npm:^8.0.8":
version: 8.0.8
resolution: "@rmwc/icon@npm:8.0.8"
@@ -2312,7 +2555,7 @@ __metadata:
languageName: node
linkType: hard
-"@rmwc/ripple@npm:^14.0.0":
+"@rmwc/ripple@npm:14.3.5, @rmwc/ripple@npm:^14.0.0":
version: 14.3.5
resolution: "@rmwc/ripple@npm:14.3.5"
dependencies:
@@ -2443,6 +2686,19 @@ __metadata:
languageName: node
linkType: hard
+"@rmwc/touch-target@npm:^14.3.5":
+ version: 14.3.5
+ resolution: "@rmwc/touch-target@npm:14.3.5"
+ dependencies:
+ "@material/touch-target": ^14.0.0
+ "@rmwc/base": 14.3.5
+ peerDependencies:
+ react: ">=16.8.x"
+ react-dom: ">=16.8.x"
+ checksum:
c9f82f3c685240be0df5346aeed0abdb7bdd68e2640e6bee930579a359309424965d38c55324cac94235a890ac73a408450dd129a9eb369727e96a5710679003
+ languageName: node
+ linkType: hard
+
"@rmwc/types@npm:14.3.5":
version: 14.3.5
resolution: "@rmwc/types@npm:14.3.5"
@@ -2591,6 +2847,64 @@ __metadata:
languageName: node
linkType: hard
+"@types/d3-color@npm:*":
+ version: 3.1.3
+ resolution: "@types/d3-color@npm:3.1.3"
+ checksum:
8a0e79a709929502ec4effcee2c786465b9aec51b653ba0b5d05dbfec3e84f418270dd603002d94021885061ff592f614979193bd7a02ad76317f5608560e357
+ languageName: node
+ linkType: hard
+
+"@types/d3-drag@npm:^3.0.7":
+ version: 3.0.7
+ resolution: "@types/d3-drag@npm:3.0.7"
+ dependencies:
+ "@types/d3-selection": "*"
+ checksum:
1107cb1667ead79073741c06ea4a9e8e4551698f6c9c60821e327a6aa30ca2ba0b31a6fe767af85a2e38a22d2305f6c45b714df15c2bba68adf58978223a5fc5
+ languageName: node
+ linkType: hard
+
+"@types/d3-interpolate@npm:*, @types/d3-interpolate@npm:^3.0.4":
+ version: 3.0.4
+ resolution: "@types/d3-interpolate@npm:3.0.4"
+ dependencies:
+ "@types/d3-color": "*"
+ checksum:
efd2770e174e84fc7316fdafe03cf3688451f767dde1fa6211610137f495be7f3923db7e1723a6961a0e0e9ae0ed969f4f47c038189fa0beb1d556b447922622
+ languageName: node
+ linkType: hard
+
+"@types/d3-selection@npm:*, @types/d3-selection@npm:^3.0.10":
+ version: 3.0.11
+ resolution: "@types/d3-selection@npm:3.0.11"
+ checksum:
4b76630f76dffdafc73cdc786d73e7b4c96f40546483074b3da0e7fe83fd7f5ed9bc6c50f79bcef83595f943dcc9ed6986953350f39371047af644cc39c41b43
+ languageName: node
+ linkType: hard
+
+"@types/d3-transition@npm:^3.0.8":
+ version: 3.0.9
+ resolution: "@types/d3-transition@npm:3.0.9"
+ dependencies:
+ "@types/d3-selection": "*"
+ checksum:
c8608b1ac7cf09acfe387f3d41074631adcdfd7f2c8ca2efb378309adf0e9fc8469dbcf0d7a8c40fd1f03f2d2bf05fcda0cde7aa356ae8533a141dcab4dff221
+ languageName: node
+ linkType: hard
+
+"@types/d3-zoom@npm:^3.0.8":
+ version: 3.0.8
+ resolution: "@types/d3-zoom@npm:3.0.8"
+ dependencies:
+ "@types/d3-interpolate": "*"
+ "@types/d3-selection": "*"
+ checksum:
a1685728949ed39faf8ce162cc13338639c57bc2fd4d55fc7902b2632cad2bc2a808941263e57ce6685647e8a6a0a556e173386a52d6bb74c9ed6195b68be3de
+ languageName: node
+ linkType: hard
+
+"@types/dagre@npm:^0.7.53":
+ version: 0.7.53
+ resolution: "@types/dagre@npm:0.7.53"
+ checksum:
e5b8e52cabd62849479aa11842112ea10e2b0911a8e0c23f7832e639d08dbd0a30d83a4e21223dfec20493e4ba5406bcdfc4c51084bc9c1b14edf16d6d215f24
+ languageName: node
+ linkType: hard
+
"@types/eslint-scope@npm:^3.7.7":
version: 3.7.7
resolution: "@types/eslint-scope@npm:3.7.7"
@@ -2680,6 +2994,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/lodash@npm:^4.17.20":
+ version: 4.17.20
+ resolution: "@types/lodash@npm:4.17.20"
+ checksum:
dc7bb4653514dd91117a4c4cec2c37e2b5a163d7643445e4757d76a360fabe064422ec7a42dde7450c5e7e0e7e678d5e6eae6d2a919abcddf581d81e63e63839
+ languageName: node
+ linkType: hard
+
"@types/node@npm:*":
version: 24.0.3
resolution: "@types/node@npm:24.0.3"
@@ -3086,6 +3407,37 @@ __metadata:
languageName: node
linkType: hard
+"@xyflow/react@npm:^12.8.2":
+ version: 12.8.2
+ resolution: "@xyflow/react@npm:12.8.2"
+ dependencies:
+ "@xyflow/system": 0.0.66
+ classcat: ^5.0.3
+ zustand: ^4.4.0
+ peerDependencies:
+ react: ">=17"
+ react-dom: ">=17"
+ checksum:
53af765d263a01541f8815bef5c17cb3d62238446ef8e956413d2b73268cfe45b9815eeb3899129a00f5295dfbcdcba82029b9b9566268cec59d775a7757e676
+ languageName: node
+ linkType: hard
+
+"@xyflow/system@npm:0.0.66":
+ version: 0.0.66
+ resolution: "@xyflow/system@npm:0.0.66"
+ dependencies:
+ "@types/d3-drag": ^3.0.7
+ "@types/d3-interpolate": ^3.0.4
+ "@types/d3-selection": ^3.0.10
+ "@types/d3-transition": ^3.0.8
+ "@types/d3-zoom": ^3.0.8
+ d3-drag: ^3.0.0
+ d3-interpolate: ^3.0.1
+ d3-selection: ^3.0.0
+ d3-zoom: ^3.0.0
+ checksum:
224e94f43495980cfcf437ad9632474926cd06cf7b6c57a315605cc5e2449cb9cff2b2b43d602a163d43ead9d7458f3f83631cd020f89ce53eccfde62b55c3a5
+ languageName: node
+ linkType: hard
+
"abab@npm:^2.0.3, abab@npm:^2.0.6":
version: 2.0.6
resolution: "abab@npm:2.0.6"
@@ -3294,25 +3646,33 @@ __metadata:
"@jupyterlab/launcher": ^4.3.6
"@jupyterlab/mainmenu": ^4.3.6
"@lumino/widgets": ^2.2.1
+ "@monaco-editor/react": ^4.7.0
"@rmwc/base": ^14.0.0
"@rmwc/button": ^8.0.6
+ "@rmwc/card": ^14.3.5
"@rmwc/data-table": ^8.0.6
"@rmwc/dialog": ^8.0.6
"@rmwc/drawer": ^8.0.6
"@rmwc/fab": ^8.0.6
+ "@rmwc/grid": ^14.3.5
"@rmwc/list": ^8.0.6
"@rmwc/ripple": ^14.0.0
"@rmwc/textfield": ^8.0.6
"@rmwc/tooltip": ^8.0.6
"@rmwc/top-app-bar": ^8.0.6
+ "@rmwc/touch-target": ^14.3.5
"@testing-library/dom": ^9.3.0
"@testing-library/jest-dom": ^6.1.4
"@testing-library/react": ^14.0.0
+ "@types/dagre": ^0.7.53
"@types/jest": ^29.5.14
+ "@types/lodash": ^4.17.20
"@types/react": ^18.2.0
"@types/react-dom": ^18.2.0
"@typescript-eslint/eslint-plugin": ^7.3.1
"@typescript-eslint/parser": ^7.3.1
+ "@xyflow/react": ^12.8.2
+ dagre: ^0.8.5
eslint: ^8.56.0
eslint-config-prettier: ^9.1.0
eslint-plugin-prettier: ^5.1.3
@@ -3321,11 +3681,13 @@ __metadata:
jest: ^29.7.0
jest-environment-jsdom: ^29.0.0
jest-util: ^29.7.0
+ lodash: ^4.17.21
material-design-icons: ^3.0.1
npm-run-all: ^4.1.5
prettier: ^3.2.4
react: ^18.2.0
react-dom: ^18.2.0
+ react-split: ^2.0.14
rimraf: ^5.0.5
ts-jest: ^29.1.2
typescript: ~5.3.3
@@ -3809,6 +4171,13 @@ __metadata:
languageName: node
linkType: hard
+"classcat@npm:^5.0.3":
+ version: 5.0.5
+ resolution: "classcat@npm:5.0.5"
+ checksum:
19bdeb99b8923b47f9df978b6ef2c5a4cc3bcaa8fb6be16244e31fad619b291b366429747331903ac2ea27560ffd6066d14089a99c95535ce0f1e897525fa63d
+ languageName: node
+ linkType: hard
+
"classnames@npm:*, classnames@npm:^2.2.6, classnames@npm:^2.3.1":
version: 2.5.1
resolution: "classnames@npm:2.5.1"
@@ -4109,6 +4478,98 @@ __metadata:
languageName: node
linkType: hard
+"d3-color@npm:1 - 3":
+ version: 3.1.0
+ resolution: "d3-color@npm:3.1.0"
+ checksum:
4931fbfda5d7c4b5cfa283a13c91a954f86e3b69d75ce588d06cde6c3628cebfc3af2069ccf225e982e8987c612aa7948b3932163ce15eb3c11cd7c003f3ee3b
+ languageName: node
+ linkType: hard
+
+"d3-dispatch@npm:1 - 3":
+ version: 3.0.1
+ resolution: "d3-dispatch@npm:3.0.1"
+ checksum:
fdfd4a230f46463e28e5b22a45dd76d03be9345b605e1b5dc7d18bd7ebf504e6c00ae123fd6d03e23d9e2711e01f0e14ea89cd0632545b9f0c00b924ba4be223
+ languageName: node
+ linkType: hard
+
+"d3-drag@npm:2 - 3, d3-drag@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "d3-drag@npm:3.0.0"
+ dependencies:
+ d3-dispatch: 1 - 3
+ d3-selection: 3
+ checksum:
d297231e60ecd633b0d076a63b4052b436ddeb48b5a3a11ff68c7e41a6774565473a6b064c5e9256e88eca6439a917ab9cea76032c52d944ddbf4fd289e31111
+ languageName: node
+ linkType: hard
+
+"d3-ease@npm:1 - 3":
+ version: 3.0.1
+ resolution: "d3-ease@npm:3.0.1"
+ checksum:
06e2ee5326d1e3545eab4e2c0f84046a123dcd3b612e68858219aa034da1160333d9ce3da20a1d3486d98cb5c2a06f7d233eee1bc19ce42d1533458bd85dedcd
+ languageName: node
+ linkType: hard
+
+"d3-interpolate@npm:1 - 3, d3-interpolate@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "d3-interpolate@npm:3.0.1"
+ dependencies:
+ d3-color: 1 - 3
+ checksum:
a42ba314e295e95e5365eff0f604834e67e4a3b3c7102458781c477bd67e9b24b6bb9d8e41ff5521050a3f2c7c0c4bbbb6e187fd586daa3980943095b267e78b
+ languageName: node
+ linkType: hard
+
+"d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "d3-selection@npm:3.0.0"
+ checksum:
f4e60e133309115b99f5b36a79ae0a19d71ee6e2d5e3c7216ef3e75ebd2cb1e778c2ed2fa4c01bef35e0dcbd96c5428f5bd6ca2184fe2957ed582fde6841cbc5
+ languageName: node
+ linkType: hard
+
+"d3-timer@npm:1 - 3":
+ version: 3.0.1
+ resolution: "d3-timer@npm:3.0.1"
+ checksum:
1cfddf86d7bca22f73f2c427f52dfa35c49f50d64e187eb788dcad6e927625c636aa18ae4edd44d084eb9d1f81d8ca4ec305dae7f733c15846a824575b789d73
+ languageName: node
+ linkType: hard
+
+"d3-transition@npm:2 - 3":
+ version: 3.0.1
+ resolution: "d3-transition@npm:3.0.1"
+ dependencies:
+ d3-color: 1 - 3
+ d3-dispatch: 1 - 3
+ d3-ease: 1 - 3
+ d3-interpolate: 1 - 3
+ d3-timer: 1 - 3
+ peerDependencies:
+ d3-selection: 2 - 3
+ checksum:
cb1e6e018c3abf0502fe9ff7b631ad058efb197b5e14b973a410d3935aead6e3c07c67d726cfab258e4936ef2667c2c3d1cd2037feb0765f0b4e1d3b8788c0ea
+ languageName: node
+ linkType: hard
+
+"d3-zoom@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "d3-zoom@npm:3.0.0"
+ dependencies:
+ d3-dispatch: 1 - 3
+ d3-drag: 2 - 3
+ d3-interpolate: 1 - 3
+ d3-selection: 2 - 3
+ d3-transition: 2 - 3
+ checksum:
8056e3527281cfd1ccbcbc458408f86973b0583e9dac00e51204026d1d36803ca437f970b5736f02fafed9f2b78f145f72a5dbc66397e02d4d95d4c594b8ff54
+ languageName: node
+ linkType: hard
+
+"dagre@npm:^0.8.5":
+ version: 0.8.5
+ resolution: "dagre@npm:0.8.5"
+ dependencies:
+ graphlib: ^2.1.8
+ lodash: ^4.17.15
+ checksum:
b9fabd425466d7b662381c2e457b1adda996bc4169aa60121d4de50250d83a6bb4b77d559e2f887c9c564caea781c2a377fd4de2a76c15f8f04ec3d086ca95f9
+ languageName: node
+ linkType: hard
+
"data-urls@npm:^2.0.0":
version: 2.0.0
resolution: "data-urls@npm:2.0.0"
@@ -5453,6 +5914,15 @@ __metadata:
languageName: node
linkType: hard
+"graphlib@npm:^2.1.8":
+ version: 2.1.8
+ resolution: "graphlib@npm:2.1.8"
+ dependencies:
+ lodash: ^4.17.15
+ checksum:
1e0db4dea1c8187d59103d5582ecf32008845ebe2103959a51d22cb6dae495e81fb9263e22c922bca3aaecb56064a45cd53424e15a4626cfb5a0c52d0aff61a8
+ languageName: node
+ linkType: hard
+
"harmony-reflect@npm:^1.4.6":
version: 1.6.2
resolution: "harmony-reflect@npm:1.6.2"
@@ -6985,7 +7455,7 @@ __metadata:
languageName: node
linkType: hard
-"lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4,
lodash@npm:^4.7.0":
+"lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21,
lodash@npm:^4.17.4, lodash@npm:^4.7.0":
version: 4.17.21
resolution: "lodash@npm:4.17.21"
checksum:
eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7
@@ -7964,7 +8434,7 @@ __metadata:
languageName: node
linkType: hard
-"prop-types@npm:15.x, prop-types@npm:^15.5.10, prop-types@npm:^15.5.8,
prop-types@npm:^15.8.1":
+"prop-types@npm:15.x, prop-types@npm:^15.5.10, prop-types@npm:^15.5.7,
prop-types@npm:^15.5.8, prop-types@npm:^15.8.1":
version: 15.8.1
resolution: "prop-types@npm:15.8.1"
dependencies:
@@ -8136,6 +8606,18 @@ __metadata:
languageName: node
linkType: hard
+"react-split@npm:^2.0.14":
+ version: 2.0.14
+ resolution: "react-split@npm:2.0.14"
+ dependencies:
+ prop-types: ^15.5.7
+ split.js: ^1.6.0
+ peerDependencies:
+ react: "*"
+ checksum:
8e9e22b8ef48063ab0b55a96bbb3ff66012ddbe899661deac30b10b72aa08c1b669eb644e599ec1ac0ce880017fc2d4b2e6cb48b113789646d6186d61d8f55f2
+ languageName: node
+ linkType: hard
+
"react@npm:>=17.0.0 <19.0.0, react@npm:^18.2.0":
version: 18.3.1
resolution: "react@npm:18.3.1"
@@ -8796,6 +9278,13 @@ __metadata:
languageName: node
linkType: hard
+"split.js@npm:^1.6.0":
+ version: 1.6.5
+ resolution: "split.js@npm:1.6.5"
+ checksum:
a3e77d8e0628de06c58e2d8e6ab41872132586c417b4f40ac3d3dbf76c2b31f40745dd86c133609dacd5599ada5d4bee157f6290403223b86bd79fe700a77983
+ languageName: node
+ linkType: hard
+
"sprintf-js@npm:^1.1.3":
version: 1.1.3
resolution: "sprintf-js@npm:1.1.3"
@@ -8828,6 +9317,13 @@ __metadata:
languageName: node
linkType: hard
+"state-local@npm:^1.0.6":
+ version: 1.0.7
+ resolution: "state-local@npm:1.0.7"
+ checksum:
d1afcf1429e7e6eb08685b3a94be8797db847369316d4776fd51f3962b15b984dacc7f8e401ad20968e5798c9565b4b377afedf4e4c4d60fe7495e1cbe14a251
+ languageName: node
+ linkType: hard
+
"stop-iteration-iterator@npm:^1.0.0, stop-iteration-iterator@npm:^1.1.0":
version: 1.1.0
resolution: "stop-iteration-iterator@npm:1.1.0"
@@ -9368,11 +9864,11 @@ __metadata:
"typescript@patch:typescript@~5.3.3#~builtin<compat/typescript>":
version: 5.3.3
- resolution:
"typescript@patch:typescript@npm%3A5.3.3#~builtin<compat/typescript>::version=5.3.3&hash=e012d7"
+ resolution:
"typescript@patch:typescript@npm%3A5.3.3#~builtin<compat/typescript>::version=5.3.3&hash=85af82"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
- checksum:
4e604a9e107ce0c23b16a2f8d79d0531d4d8fe9ebbb7a8c395c66998c39892f0e0a071ef0b0d4e66420a8ec2b8d6cfd9cdb29ba24f25b37cba072e9282376df9
+ checksum:
f61375590b3162599f0f0d5b8737877ac0a7bc52761dbb585d67e7b8753a3a4c42d9a554c4cc929f591ffcf3a2b0602f65ae3ce74714fd5652623a816862b610
languageName: node
linkType: hard
@@ -9470,6 +9966,15 @@ __metadata:
languageName: node
linkType: hard
+"use-sync-external-store@npm:^1.2.2":
+ version: 1.5.0
+ resolution: "use-sync-external-store@npm:1.5.0"
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ checksum:
5e639c9273200adb6985b512c96a3a02c458bc8ca1a72e91da9cdc6426144fc6538dca434b0f99b28fb1baabc82e1c383ba7900b25ccdcb43758fb058dc66c34
+ languageName: node
+ linkType: hard
+
"util-deprecate@npm:^1.0.2":
version: 1.0.2
resolution: "util-deprecate@npm:1.0.2"
@@ -9985,3 +10490,23 @@ __metadata:
checksum:
f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700
languageName: node
linkType: hard
+
+"zustand@npm:^4.4.0":
+ version: 4.5.7
+ resolution: "zustand@npm:4.5.7"
+ dependencies:
+ use-sync-external-store: ^1.2.2
+ peerDependencies:
+ "@types/react": ">=16.8"
+ immer: ">=9.0.6"
+ react: ">=16.8"
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ checksum:
103ab43456bbc3be6afe79b18a93c7fa46ffaa1aa35c45b213f13f4cd0868fee78b43c6805c6d80a822297df2e455fd021c28be94b80529ec4806b2724f20219
+ languageName: node
+ linkType: hard