better component hierarchy; display enable help when no environments (#84)

This commit is contained in:
Michael Quigley 2022-10-31 14:59:37 -04:00
parent 5a2d5fcbb1
commit 7d1c69ad0e
No known key found for this signature in database
GPG Key ID: 9B60314A9DD20A62
6 changed files with 184 additions and 127 deletions

137
ui/src/Account.js Normal file
View File

@ -0,0 +1,137 @@
import {useEffect, useState} from "react";
import {useReactFlow} from "react-flow-renderer";
import Icon from "@mdi/react";
import {mdiAccessPointNetwork, mdiDesktopClassic} from "@mdi/js";
import dagre from "dagre";
import * as metadata from "./api/metadata";
import Network from "./Network";
import Enable from "./Enable";
const Account = (props) => {
const [mode, setMode] = useState(<></>);
const reactFlow = useReactFlow();
useEffect(() => {
let mounted = true
metadata.overview().then(resp => {
if(mounted) {
let overview = resp.data
let g = buildGraph(resp.data)
let nodes = getLayout(g)
let edges = g.edges
if(resp.data.length > 0) {
setMode(<Network
nodes={nodes}
edges={edges}
overview={overview}
/>)
reactFlow.fitView({maxZoom: 1})
} else {
setMode(<Enable token={props.user.token}/>)
}
}
});
}, [])
useEffect(() => {
let mounted = true
let interval = setInterval(() => {
metadata.overview().then(resp => {
if(mounted) {
let overview = resp.data
let g = buildGraph(resp.data)
let nodes = getLayout(g)
let edges = g.edges
if(resp.data.length > 0) {
setMode(<Network
nodes={nodes}
edges={edges}
overview={overview}
/>)
reactFlow.fitView({maxZoom: 1})
} else {
setMode(<Enable token={props.user.token}/>)
}
}
})
}, 1000)
return () => {
mounted = false
clearInterval(interval)
}
}, [])
return <>{mode}</>
}
function buildGraph(overview) {
let out = {
nodes: [],
edges: []
}
let id = 1
overview.forEach((item) => {
let envId = id
out.nodes.push({
id: '' + envId,
data: { label: <div><Icon path={mdiDesktopClassic} size={0.75} className={"flowNode"}/> { item.environment.description } </div> },
position: { x: (id * 25), y: 0 },
style: { width: 'fit-content', backgroundColor: '#aaa', color: 'white' },
type: 'input',
draggable: true
});
id++
if(item.services != null) {
item.services.forEach((item) => {
out.nodes.push({
id: '' + id,
data: {label: <div><Icon path={mdiAccessPointNetwork} size={0.75} className={"flowNode"}/> { item.frontend }</div>},
position: {x: (id * 25), y: 0},
style: { width: 'fit-content', backgroundColor: '#9367ef', color: 'white' },
type: 'output',
draggable: true
})
out.edges.push({
id: 'e' + envId + '-' + id,
source: '' + envId,
target: '' + id,
animated: true
})
id++
});
}
});
return out
}
const nodeWidth = 215;
const nodeHeight = 75;
function getLayout(overview) {
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setGraph({ rankdir: 'TB' });
dagreGraph.setDefaultEdgeLabel(() => ({}));
overview.nodes.forEach((n) => {
dagreGraph.setNode(n.id, { width: nodeWidth, height: nodeHeight });
})
overview.edges.forEach((e) => {
dagreGraph.setEdge(e.source, e.target);
})
dagre.layout(dagreGraph);
return overview.nodes.map((n) => {
const nodeWithPosition = dagreGraph.node(n.id);
n.targetPosition = 'top';
n.sourcePosition = 'bottom';
n.position = {
x: nodeWithPosition.x - (nodeWidth / 2) + (Math.random() / 1000) + 50,
y: nodeWithPosition.y - (nodeHeight / 2) + 50,
}
return n;
});
}
export default Account

View File

@ -1,17 +1,16 @@
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Register from "./Register";
import Overview from "./Overview";
import Console from "./Console";
const App = () => {
return (
<Router>
<Routes>
<Route path={"/"} element={<Overview />}/>
<Route path={"register/:token"} element={<Register />} />
<Route path={"/"} element={<Console/>}/>
<Route path={"register/:token"} element={<Register/>} />
</Routes>
</Router>
);
}
export default App;
export default App

View File

@ -3,10 +3,10 @@ import Login from "./Login";
import Version from "./Version";
import Token from "./Token";
import Logout from "./Logout";
import Network from "./Network";
import Account from "./Account";
import {ReactFlowProvider} from "react-flow-renderer";
const Overview = () => {
const Console = () => {
const [user, setUser] = useState();
useEffect(() => {
@ -47,7 +47,7 @@ const Overview = () => {
</div>
<div className="main">
<ReactFlowProvider>
<Network />
<Account user={user}/>
</ReactFlowProvider>
</div>
</div>
@ -55,4 +55,4 @@ const Overview = () => {
);
}
export default Overview;
export default Console;

26
ui/src/Enable.js Normal file
View File

@ -0,0 +1,26 @@
import Icon from "@mdi/react";
import {mdiContentCopy} from "@mdi/js";
const Enable = (props) => {
const handleCopy = async () => {
let copiedText = document.getElementById("zrok-enable-command").innerHTML;
try {
await navigator.clipboard.writeText(copiedText);
console.log("copied enable command");
} catch(err) {
console.error("failed to copy", err);
}
}
return <>
<div id={"zrok-enable"}>
<h1>Enable an Environment</h1>
<p>To enable your shell for zrok, use this command:</p>
<pre>
$ <span id={"zrok-enable-command"}>zrok enable {props.token}</span> <Icon path={mdiContentCopy} size={0.7} onClick={handleCopy}/>
</pre>
</div>
</>
}
export default Enable

View File

@ -1,134 +1,21 @@
import Environments from './Environments';
import * as metadata from './api/metadata';
import {useEffect, useLayoutEffect, useRef, useState} from "react";
import ReactFlow, {isNode, useNodesState, useReactFlow} from "react-flow-renderer";
import dagre from 'dagre';
import { mdiDesktopClassic, mdiAccessPointNetwork } from '@mdi/js';
import Icon from "@mdi/react";
const Network = () => {
const [overview, setOverview] = useState([]);
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges] = useState([]);
const reactFlow = useReactFlow();
useEffect(() => {
let mounted = true
metadata.overview().then(resp => {
if(mounted) {
setOverview(resp.data)
let g = buildGraph(resp.data)
setNodes(getLayout(g))
setEdges(g.edges)
reactFlow.fitView({maxZoom: 1})
}
});
}, [])
useEffect(() => {
let mounted = true
let interval = setInterval(() => {
metadata.overview().then(resp => {
if(mounted) {
setOverview(resp.data)
let g = buildGraph(resp.data)
setNodes(getLayout(g))
setEdges(g.edges)
}
})
}, 1000)
return () => {
mounted = false
clearInterval(interval)
}
}, [])
import ReactFlow from "react-flow-renderer";
const Network = (props) => {
return (
<div>
<div className={"network"}>
<h1>Network</h1>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
nodes={props.nodes}
edges={props.edges}
/>
</div>
<Environments
overview={overview}
overview={props.overview}
/>
</div>
)
}
function buildGraph(overview) {
let out = {
nodes: [],
edges: []
}
let id = 1
overview.forEach((item) => {
let envId = id
out.nodes.push({
id: '' + envId,
data: { label: <div><Icon path={mdiDesktopClassic} size={0.75} className={"flowNode"}/> { item.environment.description } </div> },
position: { x: (id * 25), y: 0 },
style: { width: 'fit-content', backgroundColor: '#aaa', color: 'white' },
type: 'input',
draggable: true
});
id++
if(item.services != null) {
item.services.forEach((item) => {
out.nodes.push({
id: '' + id,
data: {label: <div><Icon path={mdiAccessPointNetwork} size={0.75} className={"flowNode"}/> { item.frontend }</div>},
position: {x: (id * 25), y: 0},
style: { width: 'fit-content', backgroundColor: '#9367ef', color: 'white' },
type: 'output',
draggable: true
})
out.edges.push({
id: 'e' + envId + '-' + id,
source: '' + envId,
target: '' + id,
animated: true
})
id++
});
}
});
return out
}
const nodeWidth = 215;
const nodeHeight = 75;
function getLayout(overview) {
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setGraph({ rankdir: 'TB' });
dagreGraph.setDefaultEdgeLabel(() => ({}));
overview.nodes.forEach((n) => {
dagreGraph.setNode(n.id, { width: nodeWidth, height: nodeHeight });
})
overview.edges.forEach((e) => {
dagreGraph.setEdge(e.source, e.target);
})
dagre.layout(dagreGraph);
return overview.nodes.map((n) => {
const nodeWithPosition = dagreGraph.node(n.id);
n.targetPosition = 'top';
n.sourcePosition = 'bottom';
n.position = {
x: nodeWithPosition.x - (nodeWidth / 2) + (Math.random() / 1000) + 50,
y: nodeWithPosition.y - (nodeHeight / 2) + 50,
}
return n;
});
}
export default Network;

View File

@ -14,6 +14,14 @@ h1, h2, h3, h4, h5, h6 {
font-family: 'Russo One', sans-serif;
}
#zrok-enable {
text-align: center;
margin-top: 100px;
}
#zrok-enable h1 {
margin-bottom: 50px;
}
.container {
display: grid;
margin-left: 10%;