Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VT-23. Finalize POC of extension #6

Merged
merged 14 commits into from
Sep 11, 2024
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,5 @@ dmypy.json

# Yarn cache
.yarn/
workspace_export.py
test_output
372 changes: 372 additions & 0 deletions notebooks/api-test.ipynb

Large diffs are not rendered by default.

24 changes: 20 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import TreeViewComponent from './components/TreeView';
const App: React.FC = () => {
const [selectedNode, setSelectedNode] = useState<ISelectedNodeType | null>(null);

const [workspace, setWorkspace] = useState<IWorkspaceState>({
const initialWorkspaceState: IWorkspaceState = {
model: null,
connectivity: null,
parcellation: null,
tractogram: null,
coupling: null,
noise: null,
integrationMethod: null
});
};
const [workspace, setWorkspace] = useState<IWorkspaceState>(initialWorkspaceState);

const addToWorkspace = (node: ISelectedNodeType) => {
setWorkspace(prevWorkspace => {
Expand All @@ -35,10 +37,24 @@ const App: React.FC = () => {
});
};

const updateConnectivityOptions = (optionType: 'parcellation' | 'tractogram', value: string) => {
setWorkspace(prev => {
return {
...prev,
parcellation: optionType === 'parcellation' ? value : prev.parcellation,
tractogram: optionType === 'tractogram' ? value : prev.tractogram,
};
});
};

const resetWorkspace = () => {
setWorkspace(initialWorkspaceState);
};

return (
<div className="layout">
<InfoBoxComponent selectedNode={selectedNode} addToWorkspace={addToWorkspace} />
<WorkspaceComponent workspace={workspace} />
<WorkspaceComponent workspace={workspace} updateConnectivityOptions={updateConnectivityOptions} resetWorkspace={resetWorkspace}/>
<TreeViewComponent selectedNode={selectedNode} />
<GraphViewComponent setSelectedNode={setSelectedNode} />
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/components/GaphView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const GraphViewComponent: React.FC<IGraphViewProps> = ({
type: node.type,
definition: node.definition,
iri: node.iri,
requires: node.requires,
childNodes: node.childNodes,
childLinks: node.childLinks,
collapsed: false
Expand Down Expand Up @@ -111,7 +112,7 @@ export const GraphViewComponent: React.FC<IGraphViewProps> = ({
}
};

// for graph centering when it is first rendered
// center graph when it is first rendered
useEffect(() => {
if (fgRef.current && data.nodes.length > 0 && isInitialRender) {
fgRef.current.centerAt(75, 75);
Expand Down
36 changes: 30 additions & 6 deletions src/components/InfoBox.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { ISelectedNodeType } from './interfaces/InfoBoxInterfaces';
import { fetchNodeByLabel } from '../handler';

interface IInfoBoxProps {
selectedNode: {
Expand All @@ -8,23 +9,46 @@ interface IInfoBoxProps {
type: string;
definition: string;
iri: string;
requires: string[];
} | null;
addToWorkspace: (node: ISelectedNodeType) => void;
}

const InfoBoxComponent: React.FC<IInfoBoxProps> = ({
selectedNode,
addToWorkspace
}) => {
const InfoBoxComponent: React.FC<IInfoBoxProps> = ({ selectedNode, addToWorkspace }) => {
// Valid types for adding objects to workspace
// TODO: add valid type for connectivity
const validTypes = ['Neural Mass Model', 'Noise', 'Coupling', 'Integrator'];

const isAddable = selectedNode && validTypes.includes(selectedNode.type);

const handleAddToWorkspace = async () => {
if (!selectedNode) return;

if (isAddable) {
addToWorkspace(selectedNode);

// check requirements of selected node
if (selectedNode.requires && selectedNode.requires.length > 0) {
const requiredNodePromises = selectedNode.requires.map(label => fetchNodeByLabel(label));
const requiredNodesResponses = await Promise.all(requiredNodePromises);

requiredNodesResponses.forEach(response => {
const { nodes } = response; // Extract nodes from the response
nodes.forEach(node => {
if (validTypes.includes(node.type) && (node.type !== selectedNode.type)) { // check for same type as selected node to not overwrite it
addToWorkspace(node);
}
});
});
}
} else {
console.warn(`Node type "${selectedNode.type}" is not allowed in the workspace.`);
}
};

return (
<div className="info-box">
<h3>Concept Details</h3>
<h3>Node Details</h3>
{selectedNode ? (
<div>
<div className="node-info-container">
Expand All @@ -48,7 +72,7 @@ const InfoBoxComponent: React.FC<IInfoBoxProps> = ({
</div>
<button
className="add-button"
onClick={() => addToWorkspace(selectedNode)}
onClick={handleAddToWorkspace}
disabled={!isAddable}
>
Add to Workspace
Expand Down
25 changes: 23 additions & 2 deletions src/components/TreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface INodeRelation {
const TreeViewComponent: React.FC<ITreeViewProps> = ({ selectedNode }) => {
const [parents, setParents] = useState<INodeRelation[]>([]);
const [children, setChildren] = useState<INodeRelation[]>([]);
const [requirements, setRequirements] = useState<string[]>([]);

useEffect(() => {
if (!selectedNode) {
Expand Down Expand Up @@ -45,6 +46,12 @@ const TreeViewComponent: React.FC<ITreeViewProps> = ({ selectedNode }) => {

fetchAndSetParents();
fetchAndSetChildren();

if (selectedNode.requires && selectedNode.requires.length > 0) {
setRequirements(selectedNode.requires);
} else {
setRequirements([]);
}
}, [selectedNode]);

const processNodeRelations = (
Expand All @@ -68,10 +75,12 @@ const TreeViewComponent: React.FC<ITreeViewProps> = ({ selectedNode }) => {

return (
<div className="tree-view">
<h3>Node Connections</h3>
{selectedNode ? (
<>
<h2 style={{ textAlign: 'center' }}>{selectedNode.label}</h2>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
{/* Parents Column */}
<div>
<h3>Parents</h3>
{parents.length > 0 ? (
Expand All @@ -87,7 +96,8 @@ const TreeViewComponent: React.FC<ITreeViewProps> = ({ selectedNode }) => {
)}
</div>

<div>
{/* Children Column */}
<div style={{ marginLeft: '20px' }}>
<h3>Children</h3>
{children.length > 0 ? (
<ul>
Expand All @@ -101,10 +111,21 @@ const TreeViewComponent: React.FC<ITreeViewProps> = ({ selectedNode }) => {
<p>No children found.</p>
)}
</div>
{/* Requires Column */}
{requirements.length > 0 && (
<div style={{ marginLeft: '20px' }}>
<h3>Requires</h3>
<ul>
{requirements.map((requirement, index) => (
<li key={index}>{requirement}</li>
))}
</ul>
</div>
)}
</div>
</>
) : (
<p>Please select a node first</p>
<p>Please select a node to see its connections</p>
)}
</div>
);
Expand Down
136 changes: 110 additions & 26 deletions src/components/Workspace.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { IWorkspaceProps } from './interfaces/WorkspaceInterfaces';
import { exportWorkspace } from '../handler';
import { exportWorkspace, runSimulation } from '../handler';

const WorkspaceComponent: React.FC<IWorkspaceProps> = ({ workspace }) => {
const [exportType, setExportType] = useState('python file');
const WorkspaceComponent: React.FC<IWorkspaceProps> = ({ workspace, updateConnectivityOptions, resetWorkspace }) => {
const [exportType, setExportType] = useState('py');
const [message, setMessage] = useState<string | null>(null);
const [filename, setFilename] = useState('workspace_export'); // Default filename
const [directory, setDirectory] = useState('');

const handleExport = async () => {
const parcellationOptions = ['DesikanKilliany', 'Destrieux', 'Schaefer1000', 'hcpmmp1', 'virtualdbs'];
const tractogramOptions = ['MghUscHcp32', 'PPMI85', 'dTOR'];

const constructNodeData = () => {
const nodeData = {
model: workspace.model ? workspace.model.label : 'None',
connectivity: workspace.connectivity ? workspace.connectivity.label : 'None',
parcellation: workspace.parcellation ? workspace.parcellation : 'None',
tractogram: workspace.tractogram ? workspace.tractogram : 'None',
coupling: workspace.coupling ? workspace.coupling.label : 'None',
noise: workspace.noise ? workspace.noise.label : 'None',
integrationMethod: workspace.integrationMethod ? workspace.integrationMethod.label : 'None',
};

return nodeData;
};

const handleExport = async () => {
const nodeData = constructNodeData();

try {
const response = await exportWorkspace(exportType, nodeData, filename);
const response = await exportWorkspace(exportType, nodeData, directory);

if (response.status === 'success') {
setMessage(`File saved successfully: ${response.message}`);
setMessage(`${response.message}`);
} else {
setMessage(`Failed to save file: ${response.error}`);
}
Expand All @@ -33,16 +43,83 @@ const WorkspaceComponent: React.FC<IWorkspaceProps> = ({ workspace }) => {
}
};

const handleRunSimulation = async () => {
const nodeData = constructNodeData();

try {
const response = await runSimulation(exportType, nodeData, directory);

if (response.status === 'success') {
setMessage(`${response.message}`);
} else {
setMessage(`${response.error}`);
}
} catch (error) {
if (error instanceof Error) {
setMessage(`Error during simulation run: ${error.message}`);
} else {
setMessage('An unknown error occurred during simulation run.');
}
}
};

// set timer for success/error message
useEffect(() => {
if (message) {
const timer = setTimeout(() => {
setMessage(null);
}, 30000);

return () => clearTimeout(timer);
}
}, [message]);

return (
<div className="workspace">
<h3>Workspace</h3>
<div className="workspace-header">
<h3>Workspace</h3>
<button className="reset-button" onClick={resetWorkspace}>Reset Workspace</button>
</div>
<div>
<h4>Model</h4>
<p>{workspace.model ? workspace.model.label : 'None'}</p>
</div>
<div>
<h4>Connectivity</h4>
<p>{workspace.connectivity ? workspace.connectivity.label : 'None'}</p>
<div className="dropdown-container">
{/* Parcellation Dropdown */}
<div className="dropdown-section">
<label htmlFor="parcellation">Parcellation:</label>
<select
id="parcellation"
value={workspace.parcellation || ''}
onChange={(e) => updateConnectivityOptions('parcellation', e.target.value)} // Update state on change
>
<option value="" disabled>Select a parcellation</option>
{parcellationOptions.map((option, index) => (
<option key={index} value={option}>
{option}
</option>
))}
</select>
</div>

{/* Tractogram Dropdown */}
<div className="dropdown-section">
<label htmlFor="tractogram">Tractogram:</label>
<select
id="tractogram"
value={workspace.tractogram || ''}
onChange={(e) => updateConnectivityOptions('tractogram', e.target.value)} // Update state on change
>
<option value="" disabled>Select a tractogram</option>
{tractogramOptions.map((option, index) => (
<option key={index} value={option}>
{option}
</option>
))}
</select>
</div>
</div>
</div>
<div>
<h4>Coupling</h4>
Expand All @@ -60,28 +137,35 @@ const WorkspaceComponent: React.FC<IWorkspaceProps> = ({ workspace }) => {
{/* Export controls */}
<div className="export-controls">
<div className="export-control">
<label htmlFor="export-type">Select export type: </label>
<select
id="export-type"
value={exportType}
onChange={(e) => setExportType(e.target.value)}
>
<option value="python file">Python File</option>
<option value="xml file">XML File</option>
</select>
<div className="dropdown-section">
<label htmlFor="export-type">Select export type: </label>
<select
id="export-type"
value={exportType}
onChange={(e) => setExportType(e.target.value)}
>
<option value="py">Simulation code (.py)</option>
<option value="xml">Model specification (.xml)</option>
<option value="yaml">Metadata (.yaml)</option>
</select>
</div>
</div>

<div className="export-control">
<label htmlFor="filename">Filename: </label>
<label htmlFor="directory">Save Directory Path: </label>
<input
id="filename"
id="directory"
type="text"
value={filename}
onChange={(e) => setFilename(e.target.value)}
value={directory}
onChange={(e) => setDirectory(e.target.value)}
placeholder="/home/user/downloads"
/>
</div>

<button className="export-button" onClick={handleExport}>Export</button>
<div className="button-container">
<button className="export-button" onClick={handleExport}>Export</button>
<button className="export-button" onClick={handleRunSimulation}>Run Simulation</button>
</div>
{message && <p>{message}</p>} {/* Display success/error message */}
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/components/interfaces/GraphViewInterfaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface INodeType {
type: string;
definition: string;
iri: string;
requires: string[];
x?: number;
y?: number;
collapsed?: boolean;
Expand Down
Loading
Loading