Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,37 @@ Export to python
For advanced users, PathView allows you to export your graph as a Python script. This feature is useful for integrating your simulation into larger Python projects or for further analysis using Python libraries.

This is useful for instance for performing parametric studies or sensitivity analysis, where you can easily modify parameters in the Python script and rerun the simulation.

Sharing Graphs via URL
----------------------

PathView supports sharing complete graph configurations through URLs, making collaboration and graph distribution easy.

**How to share a graph:**

1. Create and configure your graph with all necessary nodes, connections, and parameters
2. Click the "🔗 Share URL" button in the floating action buttons (top-right area)
3. The complete graph URL is automatically copied to your clipboard
4. Share this URL with others - when they visit it, your exact graph configuration will load automatically

**What's included in shared URLs:**

- All node positions and configurations
- Edge connections and data flow
- Solver parameters and simulation settings
- Global variables and their values
- Event definitions
- Custom Python code

**Best practices:**

- URLs work best for moderately-sized graphs. For very complex graphs with many nodes, consider using the file save/load functionality instead
- URLs contain all graph data encoded in base64, so they can become quite long
- The shared graph state is completely self-contained - no server storage required

**Example use cases:**

- Sharing example configurations with students or colleagues
- Creating bookmarks for frequently-used graph templates
- Collaborating on model development
- Including interactive models in documentation or presentations
104 changes: 104 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// * Imports *
import { useState, useCallback, useEffect, useRef, version } from 'react';

Check failure on line 2 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'version' is defined but never used. Allowed unused vars must match /^[A-Z_]/u

Check failure on line 2 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'version' is defined but never used. Allowed unused vars must match /^[A-Z_]/u
import {
ReactFlowProvider,
useReactFlow,
Expand All @@ -9,6 +9,12 @@
import '@xyflow/react/dist/style.css';
import './styles/App.css';
import { getApiEndpoint } from './config.js';
import {
getGraphDataFromURL,
generateShareableURL,
updateURLWithGraphData,
clearGraphDataFromURL
} from './utils/urlSharing.js';
import Sidebar from './components/Sidebar';
import NodeSidebar from './components/NodeSidebar';
import { DnDProvider, useDnD } from './components/DnDContext.jsx';
Expand All @@ -22,6 +28,7 @@
import EdgeDetails from './components/EdgeDetails.jsx';
import SolverPanel from './components/SolverPanel.jsx';
import ResultsPanel from './components/ResultsPanel.jsx';
import ShareModal from './components/ShareModal.jsx';

// * Declaring variables *

Expand Down Expand Up @@ -83,7 +90,7 @@
}, []);

const onDragStart = (event, nodeType) => {
setType(nodeType);

Check failure on line 93 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'setType' is not defined

Check failure on line 93 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'setType' is not defined
event.dataTransfer.setData('text/plain', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
Expand All @@ -99,6 +106,62 @@
// Python code editor state
const [pythonCode, setPythonCode] = useState("# Define your Python variables and functions here\n# Example:\n# my_variable = 42\n# def my_function(x):\n# return x * 2\n");

// State for URL sharing feedback
const [shareUrlFeedback, setShareUrlFeedback] = useState('');

Check failure on line 110 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'shareUrlFeedback' is assigned a value but never used. Allowed unused vars must match /^[A-Z_]/u

Check failure on line 110 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'shareUrlFeedback' is assigned a value but never used. Allowed unused vars must match /^[A-Z_]/u
const [showShareModal, setShowShareModal] = useState(false);
const [shareableURL, setShareableURL] = useState('');

// Load graph data from URL on component mount
useEffect(() => {
const loadGraphFromURL = () => {
const urlGraphData = getGraphDataFromURL();
if (urlGraphData) {
try {
// Validate that it's a valid graph file
if (!urlGraphData.nodes || !Array.isArray(urlGraphData.nodes)) {
console.warn("Invalid graph data in URL");
return;
}

// Load the graph data and ensure nodeColor exists on all nodes
const {
nodes: loadedNodes,
edges: loadedEdges,
nodeCounter: loadedNodeCounter,
solverParams: loadedSolverParams,
globalVariables: loadedGlobalVariables,
events: loadedEvents,
pythonCode: loadedPythonCode
} = urlGraphData;

// Ensure all loaded nodes have a nodeColor property
const nodesWithColors = (loadedNodes || []).map(node => ({
...node,
data: {
...node.data,
nodeColor: node.data.nodeColor || '#DDE6ED'
}
}));

setNodes(nodesWithColors);
setEdges(loadedEdges || []);
setSelectedNode(null);
setNodeCounter(loadedNodeCounter ?? loadedNodes.length);
setSolverParams(loadedSolverParams ?? DEFAULT_SOLVER_PARAMS);
setGlobalVariables(loadedGlobalVariables ?? []);
setEvents(loadedEvents ?? []);
setPythonCode(loadedPythonCode ?? "# Define your Python variables and functions here\n# Example:\n# my_variable = 42\n# def my_function(x):\n# return x * 2\n");

console.log('Graph loaded from URL successfully');
} catch (error) {
console.error('Error loading graph from URL:', error);
}
}
};

loadGraphFromURL();
}, []); // Empty dependency array means this runs once on mount

Check warning on line 163 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

React Hook useEffect has missing dependencies: 'setEdges' and 'setNodes'. Either include them or remove the dependency array

Check warning on line 163 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

React Hook useEffect has missing dependencies: 'setEdges' and 'setNodes'. Either include them or remove the dependency array

const [defaultValues, setDefaultValues] = useState({});
const [isEditingLabel, setIsEditingLabel] = useState(false);
const [tempLabel, setTempLabel] = useState('');
Expand Down Expand Up @@ -302,7 +365,7 @@
setNodes((nds) => [...nds, newNode]);
setNodeCounter((count) => count + 1);
},
[screenToFlowPosition, type, nodeCounter, fetchDefaultValues, setDefaultValues, setNodes, setNodeCounter],

Check warning on line 368 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

React Hook useCallback has an unnecessary dependency: 'setDefaultValues'. Either exclude it or remove the dependency array

Check warning on line 368 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

React Hook useCallback has an unnecessary dependency: 'setDefaultValues'. Either exclude it or remove the dependency array
);

// Function to save a graph to computer with "Save As" dialog
Expand Down Expand Up @@ -500,7 +563,41 @@
setNodeCounter(0);
setSolverParams(DEFAULT_SOLVER_PARAMS);
setGlobalVariables([]);
// Clear URL when resetting graph
clearGraphDataFromURL();
};

// Share current graph via URL
const shareGraphURL = async () => {
const graphData = {
version: versionInfo ? Object.fromEntries(Object.entries(versionInfo).filter(([key]) => key !== 'status')) : 'unknown',
nodes,
edges,
nodeCounter,
solverParams,
globalVariables,
events,
pythonCode
};

try {
const url = generateShareableURL(graphData);
if (url) {
setShareableURL(url);
setShowShareModal(true);
// Update browser URL as well
updateURLWithGraphData(graphData, true);
} else {
setShareUrlFeedback('Error generating share URL');
setTimeout(() => setShareUrlFeedback(''), 3000);
}
} catch (error) {
console.error('Error sharing graph URL:', error);
setShareUrlFeedback('Error generating share URL');
setTimeout(() => setShareUrlFeedback(''), 3000);
}
};

const downloadCsv = async () => {
if (!csvData) return;

Expand Down Expand Up @@ -947,7 +1044,7 @@
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [selectedEdge, selectedNode, copiedNode, duplicateNode, setCopyFeedback]);

Check warning on line 1047 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

React Hook useEffect has missing dependencies: 'deleteSelectedEdge' and 'deleteSelectedNode'. Either include them or remove the dependency array

Check warning on line 1047 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

React Hook useEffect has missing dependencies: 'deleteSelectedEdge' and 'deleteSelectedNode'. Either include them or remove the dependency array

return (
<div style={{ width: '100vw', height: '100vh', display: 'flex', flexDirection: 'column' }}>
Expand Down Expand Up @@ -1050,6 +1147,7 @@
selectedNode, selectedEdge,
deleteSelectedNode, deleteSelectedEdge,
saveGraph, loadGraph, resetGraph, saveToPython, runPathsim,
shareGraphURL,
dockOpen, setDockOpen, onToggleLogs,
showKeyboardShortcuts, setShowKeyboardShortcuts,
}}
Expand Down Expand Up @@ -1116,6 +1214,12 @@
/>
)}

{/* Share URL Modal */}
<ShareModal
isOpen={showShareModal}
onClose={() => setShowShareModal(false)}
shareableURL={shareableURL}
/>

</div>
);
Expand Down
25 changes: 23 additions & 2 deletions src/components/GraphView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function FloatingButtons({
selectedNode, selectedEdge,
deleteSelectedNode, deleteSelectedEdge,
saveGraph, loadGraph, resetGraph, saveToPython, runPathsim,
shareGraphURL,
dockOpen, onToggleLogs
}) {
return (
Expand Down Expand Up @@ -162,7 +163,27 @@ function FloatingButtons({
style={{
position: 'absolute',
right: 20,
top: 150,
top: 143,
zIndex: 10,
padding: '8px 8px',
backgroundColor: '#78A083',
color: 'white',
border: 'none',
borderRadius: 5,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}
onClick={shareGraphURL}
>
🔗
</button>
<button
style={{
position: 'absolute',
right: 20,
top: 185,
zIndex: 10,
padding: '8px 12px',
backgroundColor: '#78A083',
Expand All @@ -183,7 +204,7 @@ function FloatingButtons({
style={{
position: 'absolute',
right: 20,
top: 200,
top: 230,
zIndex: 10,
padding: '8px 12px',
backgroundColor: '#444',
Expand Down
141 changes: 141 additions & 0 deletions src/components/ShareModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React, { useState, useEffect } from 'react';

const ShareModal = ({ isOpen, onClose, shareableURL }) => {
const [copyFeedback, setCopyFeedback] = useState('');

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(shareableURL);
setCopyFeedback('Copied!');
} catch (error) {

Check failure on line 10 in src/components/ShareModal.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'error' is defined but never used

Check failure on line 10 in src/components/ShareModal.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'error' is defined but never used
// Fallback for older browsers
try {
const textArea = document.createElement('textarea');
textArea.value = shareableURL;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
setCopyFeedback('Copied!');
} catch (fallbackError) {

Check failure on line 20 in src/components/ShareModal.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'fallbackError' is defined but never used

Check failure on line 20 in src/components/ShareModal.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'fallbackError' is defined but never used
setCopyFeedback('Failed to copy');
}
}

// Clear feedback after 2 seconds
setTimeout(() => setCopyFeedback(''), 2000);
};

// Reset feedback when modal opens
useEffect(() => {
if (isOpen) {
setCopyFeedback('');
}
}, [isOpen]);

if (!isOpen) return null;

return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
}}
onClick={onClose}
>
<div
style={{
backgroundColor: 'white',
borderRadius: 8,
padding: 24,
maxWidth: 500,
width: '90%',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
position: 'relative',
}}
onClick={(e) => e.stopPropagation()}
>
{/* Close button */}
<button
onClick={onClose}
style={{
position: 'absolute',
top: 12,
right: 12,
background: 'none',
border: 'none',
fontSize: 18,
cursor: 'pointer',
color: '#666',
padding: 4,
}}
>
×
</button>

{/* Header */}
<div style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>

</div>
<p style={{ margin: 0, color: '#666', fontSize: 14 }}>
Copy this URL to share your workflow with others.
</p>
</div>

{/* URL input and copy button */}
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<input
type="text"
value={shareableURL}
readOnly
style={{
flex: 1,
padding: '8px 12px',
border: '1px solid #ddd',
borderRadius: 4,
fontSize: 14,
backgroundColor: '#f9f9f9',
color: '#333',
}}
onClick={(e) => e.target.select()}
/>
<button
onClick={handleCopy}
style={{
padding: '8px 16px',
backgroundColor: copyFeedback === 'Copied!' ? '#27ae60' : '#4A90E2',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
fontSize: 14,
fontWeight: 500,
minWidth: 60,
transition: 'background-color 0.2s',
}}
>
{copyFeedback || 'Copy'}
</button>
</div>

{/* Additional info */}
<div style={{ marginTop: 16, fontSize: 12, color: '#888' }}>
<p style={{ margin: 0 }}>
This URL contains your complete graph configuration including nodes, connections, parameters, and code.
</p>
</div>
</div>
</div>
);
};

export default ShareModal;
Loading
Loading