Skip to content

Commit

Permalink
component-ize things further
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonmadigan committed Aug 21, 2024
1 parent beb5672 commit c22b20f
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 128 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-policy-topology",
"version": "0.1.1",
"version": "0.1.2",
"type": "module",
"dependencies": {
"@patternfly/patternfly": "^4.224.5",
Expand Down
15 changes: 15 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,18 @@
transform: rotate(360deg);
}
}

.dot-string-editor {
margin-top: 20px;
max-width: 1000px; /* Increase the max-width to make the textarea wider */
width: 100%; /* Ensure the textarea takes the full width available */
}

.dot-string-editor h3 {
margin-bottom: 10px;
}

.large-textarea {
width: 100%; /* Ensure the textarea takes the full width of its container */
font-size: 14px; /* Optional: Adjust the font size */
}
77 changes: 74 additions & 3 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import React from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import PolicyTopology from './PolicyTopology.js';
import PickResource from './PickResource.js';
import ResetPolicyTopology from './ResetPolicyTopology.js';
import DotStringEditor from './DotStringEditor.js'; // Import the new component
import graphlib from 'graphlib';
import * as dot from 'graphlib-dot';
import './App.css';

function App() {
const dotString = `
const initialDotString = `
strict digraph "" {
graph [bb="0,0,440.51,352"];
node [fillcolor=lightgrey,
Expand Down Expand Up @@ -112,11 +117,77 @@ function App() {
}
`;

const [dotString, setDotString] = useState(initialDotString);
const [filteredDot, setFilteredDot] = useState(dotString);
const [graph, setGraph] = useState(null);

useEffect(() => {
const g = dot.read(dotString);
setGraph(g);
setFilteredDot(dotString);
}, [dotString]);

const handleNodeSelection = useCallback((nodeId) => {
if (!graph) return;

if (nodeId === null) {
setFilteredDot(dotString);
return;
}

const filteredGraph = new graphlib.Graph();
const nodesToInclude = new Set();

const addPredecessors = (node) => {
if (!nodesToInclude.has(node)) {
nodesToInclude.add(node);
const predecessors = graph.predecessors(node) || [];
predecessors.forEach(addPredecessors);
}
};

const addSuccessors = (node) => {
const successors = graph.successors(node) || [];
successors.forEach(successor => {
nodesToInclude.add(successor);
});
};

addPredecessors(nodeId);
addSuccessors(nodeId);

nodesToInclude.forEach(node => {
filteredGraph.setNode(node, graph.node(node));
});

graph.edges().forEach(edge => {
if (nodesToInclude.has(edge.v) && nodesToInclude.has(edge.w)) {
filteredGraph.setEdge(edge.v, edge.w, graph.edge(edge.v, edge.w));
}
});

const filteredDotString = dot.write(filteredGraph);
setFilteredDot(filteredDotString);
}, [graph, dotString]);

const resetGraph = () => {
setFilteredDot(dotString);
};

const handleDotStringChange = (newDotString) => {
setDotString(newDotString);
};

return (
<div className="App">
<header className="App-header">
<h1>Policy Topology Example</h1>
<PolicyTopology dotString={dotString} />
<div className="controls-container">
<PickResource graph={graph} onResourceSelect={handleNodeSelection} />
<ResetPolicyTopology onReset={resetGraph} />
</div>
<PolicyTopology filteredDot={filteredDot} onNodeClick={handleNodeSelection} />
<DotStringEditor dotString={dotString} onDotStringChange={handleDotStringChange} />
</header>
</div>
);
Expand Down
49 changes: 49 additions & 0 deletions src/DotStringEditor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { useState, useEffect, useRef } from 'react';
import { TextArea } from '@patternfly/react-core';

const DotStringEditor = ({ dotString, onDotStringChange }) => {
const [localDotString, setLocalDotString] = useState(dotString);
const timeoutRef = useRef(null);

useEffect(() => {
setLocalDotString(dotString);
}, [dotString]);

const handleChange = (value) => {
setLocalDotString(value);

// Clear the existing timeout if there is one
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

// Set a new timeout to trigger the update after 1 second of inactivity
timeoutRef.current = setTimeout(() => {
onDotStringChange(value);
}, 1000); // 1 second delay
};

// Clear the timeout on component unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);

return (
<div className="dot-string-editor">
<h3>Edit DOT String</h3>
<TextArea
value={localDotString}
onChange={handleChange}
aria-label="DOT string editor"
rows={15}
resizeOrientation="vertical"
/>
</div>
);
};

export default DotStringEditor;
45 changes: 45 additions & 0 deletions src/PickResource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, { useState, useEffect } from 'react';
import { Dropdown, DropdownToggle, DropdownItem } from '@patternfly/react-core';

const PickResource = ({ graph, onResourceSelect }) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [dropdownItems, setDropdownItems] = useState([]);
const [selectedLabel, setSelectedLabel] = useState('Select a resource');

useEffect(() => {
if (graph) {
const items = [
<DropdownItem key="reset" component="button" onClick={() => handleSelection(null)}>
-
</DropdownItem>,
...graph.nodes().map(node => (
<DropdownItem key={node} component="button" onClick={() => handleSelection(node)}>
{graph.node(node).label}
</DropdownItem>
)),
];
setDropdownItems(items);
}
}, [graph]);

const handleSelection = (nodeId) => {
setSelectedLabel(nodeId ? graph.node(nodeId).label : 'Select a resource');
onResourceSelect(nodeId);
setIsDropdownOpen(false);
};

const onToggle = (isOpen) => {
setIsDropdownOpen(isOpen);
};

return (
<Dropdown
onSelect={() => setIsDropdownOpen(false)}
toggle={<DropdownToggle onToggle={onToggle}>{selectedLabel}</DropdownToggle>}
isOpen={isDropdownOpen}
dropdownItems={dropdownItems}
/>
);
};

export default PickResource;
133 changes: 9 additions & 124 deletions src/PolicyTopology.js
Original file line number Diff line number Diff line change
@@ -1,113 +1,10 @@
import React, { useEffect, useRef, useCallback, useState } from 'react';
import React, { useEffect, useRef } from 'react';
import * as d3 from 'd3';
import { graphviz } from 'd3-graphviz'; // eslint-disable-line no-unused-vars
import graphlib from 'graphlib';
import * as dot from 'graphlib-dot';
import { Dropdown, DropdownToggle, DropdownItem, Title, Button } from '@patternfly/react-core';
import './PolicyTopology.css';

const PolicyTopology = ({ dotString }) => {
const PolicyTopology = ({ filteredDot, onNodeClick }) => {
const containerRef = useRef(null);
const graphRef = useRef(null);
const [filteredDot, setFilteredDot] = useState('');
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [dropdownItems, setDropdownItems] = useState([]);
const [selectedLabel, setSelectedLabel] = useState('Select a resource');

const handleNodeSelection = useCallback((nodeId) => {
const graph = graphRef.current;
if (!graph) {
console.error('Graph is not initialized');
return;
}

if (nodeId === null) {
setSelectedLabel('Select a resource');
setFilteredDot(dotString);
return;
}

const selectedNode = graph.node(nodeId);
if (!selectedNode) {
console.error(`Selected node not found in graph. nodeId: ${nodeId}`);
return;
}

const selectedNodeLabel = selectedNode.label;
setSelectedLabel(selectedNodeLabel);

const filteredGraph = new graphlib.Graph();
const nodesToInclude = new Set();

const addPredecessors = (node) => {
if (!nodesToInclude.has(node)) {
nodesToInclude.add(node);
const predecessors = graph.predecessors(node) || [];
predecessors.forEach(addPredecessors);
}
};

const addSuccessors = (node) => {
const successors = graph.successors(node) || [];
successors.forEach(successor => {
nodesToInclude.add(successor);
});
};

addPredecessors(nodeId);
addSuccessors(nodeId);

nodesToInclude.forEach(node => {
filteredGraph.setNode(node, graph.node(node));
});

graph.edges().forEach(edge => {
if (nodesToInclude.has(edge.v) && nodesToInclude.has(edge.w)) {
filteredGraph.setEdge(edge.v, edge.w, graph.edge(edge.v, edge.w));
}
});

const filteredDotString = dot.write(filteredGraph);
setFilteredDot(filteredDotString);
}, [dotString]);

useEffect(() => {
const g = dot.read(dotString);
graphRef.current = g;
setFilteredDot(dotString);

const items = [
<DropdownItem key="reset" component="button" onClick={() => handleNodeSelection(null)}>
-
</DropdownItem>,
...g.nodes().map(node => (
<DropdownItem key={node} component="button" onClick={() => handleNodeSelection(node)}>
{g.node(node).label}
</DropdownItem>
)),
];
setDropdownItems(items);
}, [dotString, handleNodeSelection]);

const handleNodeClick = useCallback((event) => {
const nodeElement = event.target.closest('g.node');
if (!nodeElement) {
console.error('No node element found');
return;
}
const nodeId = nodeElement.querySelector('title').textContent;
console.log(`Node clicked: ${nodeId}`);
handleNodeSelection(nodeId);
}, [handleNodeSelection]);

const onToggle = (isOpen) => {
setIsDropdownOpen(isOpen);
};

const resetGraph = () => {
setSelectedLabel('Select a resource');
setFilteredDot(dotString);
};

useEffect(() => {
if (containerRef.current && filteredDot) {
Expand All @@ -119,32 +16,20 @@ const PolicyTopology = ({ dotString }) => {
.on('end', () => {
const nodes = containerRef.current.querySelectorAll('g.node');
nodes.forEach(node => {
node.addEventListener('click', handleNodeClick);
node.addEventListener('click', (event) => {
const nodeElement = event.target.closest('g.node');
const nodeId = nodeElement.querySelector('title').textContent;
onNodeClick(nodeId);
});
});
});
};

renderGraph();
}
}, [filteredDot, handleNodeClick]);
}, [filteredDot, onNodeClick]);

return (
<div className="policy-topology">
<Title headingLevel="h3" size="md" className="pf-m-md">
Pick a resource to filter by
</Title>
<div className="dropdown-container">
<Dropdown
onSelect={() => setIsDropdownOpen(false)}
toggle={<DropdownToggle onToggle={onToggle}>{selectedLabel}</DropdownToggle>}
isOpen={isDropdownOpen}
dropdownItems={dropdownItems}
/>
<Button variant="secondary" onClick={resetGraph} style={{ marginLeft: '10px' }}>Reset Graph</Button>
</div>
<div ref={containerRef} className="policy-topology-container" />
</div>
);
return <div ref={containerRef} className="policy-topology-container" />;
};

export default PolicyTopology;
12 changes: 12 additions & 0 deletions src/ResetPolicyTopology.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import { Button } from '@patternfly/react-core';

const ResetPolicyTopology = ({ onReset }) => {
return (
<Button variant="secondary" onClick={onReset} style={{ marginLeft: '10px' }}>
Reset Graph
</Button>
);
};

export default ResetPolicyTopology;

0 comments on commit c22b20f

Please sign in to comment.