Skip to content

Commit d53e6cd

Browse files
authored
feat: add path navigator & table for state variable (#319)
* feat: add path navigator & table for state variable Features: - Added path navigator to graph in ViewGroup page - Added table to display state variables in ViewGroup page - Dashboard tables now have pagination and column sorting Misc: - Removed "completed" card when viewing the dashboard * chore: lint
1 parent f72cb70 commit d53e6cd

File tree

9 files changed

+523
-41
lines changed

9 files changed

+523
-41
lines changed

frontend/src/features/dashboard/Dashboard.jsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,8 @@ export default function Dashboard() {
3535
(group) => group.path.length != 0
3636
).length;
3737
// Placeholder here, needs an update for when the group completes a scenario it sets a var in the database, probs alr exist but will implement this later
38-
const groupsCompleted = 0;
3938
return (
40-
<div className="grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
39+
<div className="grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
4140
<div className="bg-white col-auto border rounded-xl p-2 text-center">
4241
<h3 className="text-xl font-mona font-bold">Groups</h3>
4342
<p className="text-lg">{totalGroups}</p>
@@ -50,14 +49,9 @@ export default function Dashboard() {
5049
<h3 className="text-xl font-mona font-bold ">Started</h3>
5150
<p className="text-lg">{groupsStarted}</p>
5251
</div>
53-
<div className="bg-green-300 col-auto border rounded-xl p-2 text-center">
54-
<h3 className="text-xl font-mona font-bold">Completed</h3>
55-
<p className="text-lg">{groupsCompleted}</p>
56-
</div>
5752
</div>
5853
);
5954
};
60-
// Is it better to just show a loading screen instead of conditionally rendering the content to stop it flashing?
6155
/**
6256
* Is it better to just show a loading screen instead of conditionally rendering the content to stop it
6357
* flashing?

frontend/src/features/dashboard/ViewGroup.jsx

Lines changed: 160 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@ import { MarkerType, ReactFlowProvider } from "@xyflow/react";
1010
import ScenarioGraph from "./components/Graph";
1111
import TestTable from "./components/Table";
1212
import { Skeleton } from "@mui/material";
13+
import Table from "@material-ui/core/Table";
14+
import TableBody from "@material-ui/core/TableBody";
15+
import TableCell from "@material-ui/core/TableCell";
16+
import TableContainer from "@material-ui/core/TableContainer";
17+
import TableHead from "@material-ui/core/TableHead";
18+
import TableRow from "@material-ui/core/TableRow";
19+
import TableFooter from "@mui/material/TableFooter";
20+
import TablePagination from "@mui/material/TablePagination";
21+
import { Paper, Typography } from "@material-ui/core";
22+
import TablePaginationActions from "./components/TablePaginationAction";
23+
import useStyles from "./components/TableStyle";
24+
import TableSortLabel from "@mui/material/TableSortLabel";
25+
import getComparator from "./components/TableHelper";
1326
/**
1427
* Might move this logic to it's own file, this way we can render a basic path on the dasboard as well
1528
* as the viewgroup page.
@@ -28,7 +41,7 @@ export default function ViewGroupPage() {
2841
height: 15,
2942
};
3043

31-
const { nodes, edges, groupEdges } = useMemo(() => {
44+
const { nodes, edges, groupEdges, groupPath, sceneMap } = useMemo(() => {
3245
var sceneMap = [];
3346
var groupPath = [];
3447
const nodes = [];
@@ -58,6 +71,7 @@ export default function ViewGroupPage() {
5871
components: scene.components,
5972
visited: visitCounter.get(scene._id) != undefined,
6073
visitCounter: visitCounter.get(scene._id),
74+
isHighlighted: false,
6175
},
6276
});
6377
});
@@ -116,8 +130,9 @@ export default function ViewGroupPage() {
116130
edge.data = { label: edgeCounter.get(edge.id) };
117131
});
118132

119-
return { nodes, edges, groupEdges };
133+
return { nodes, edges, groupEdges, groupPath, sceneMap };
120134
}, [scenes, groupInfo]);
135+
121136
return (
122137
<ScreenContainer vertical>
123138
<DashTopBar back={`/dashboard/${scenarioId}`}>
@@ -129,7 +144,9 @@ export default function ViewGroupPage() {
129144
<h1 className="text-3xl font-mona font-bold my-3">
130145
Viewing Group {groupInfo.users[0].group}
131146
</h1>
147+
132148
<TestTable groupInfo={groupInfo} />
149+
<StateVarTable data={groupInfo.stateVariables} />
133150
</div>
134151
<div className="w-full h-full">
135152
<h1 className="text-3xl font-mona font-bold px-10 my-3">
@@ -161,6 +178,8 @@ export default function ViewGroupPage() {
161178
inNodes={nodes}
162179
inEdges={edges}
163180
inGPathEdges={groupEdges}
181+
inGPath={groupPath}
182+
inSceneMap={sceneMap}
164183
onLoaded={() => {
165184
setGraphLoading(false);
166185
}}
@@ -175,3 +194,142 @@ export default function ViewGroupPage() {
175194
</ScreenContainer>
176195
);
177196
}
197+
198+
const StateVarTable = ({ data }) => {
199+
const classes = useStyles();
200+
const [page, setPage] = useState(0);
201+
const [rowsPerPage, setRowsPerPage] = useState(5);
202+
const emptyRows =
203+
page > 0 ? Math.max(0, (1 + page) * rowsPerPage - data.length) : 0;
204+
const [order, setOrder] = useState("asc");
205+
const [orderBy, setOrderBy] = useState("name");
206+
207+
const handleRequestSort = (property) => {
208+
const isAsc = orderBy === property && order === "asc";
209+
setOrder(isAsc ? "desc" : "asc");
210+
setOrderBy(property);
211+
};
212+
const handleChangePage = (event, newPage) => {
213+
setPage(newPage);
214+
};
215+
const handleChangeRowsPerPage = (event) => {
216+
setRowsPerPage(parseInt(event.target.value, 10));
217+
setPage(0);
218+
};
219+
220+
const visibleRows = useMemo(
221+
() =>
222+
[...data]
223+
.sort(getComparator(order, orderBy))
224+
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage),
225+
[order, orderBy, page, rowsPerPage]
226+
);
227+
228+
const PaperContainer = ({ children }) => (
229+
<div className={classes.root}>
230+
<Paper>
231+
<Typography variant="h5" component="h1" className={classes.heading}>
232+
State Variables
233+
</Typography>
234+
{children}
235+
</Paper>
236+
</div>
237+
);
238+
if (data.length == 0) {
239+
return (
240+
<PaperContainer>
241+
<div>
242+
State variables will show up once the group has scdtarted the
243+
scenario.
244+
</div>
245+
</PaperContainer>
246+
);
247+
} else {
248+
return (
249+
<PaperContainer>
250+
<TableContainer>
251+
<Table>
252+
<TableHead>
253+
<TableRow>
254+
<TableCell>
255+
<TableSortLabel
256+
active={orderBy === "name"}
257+
direction={orderBy === "name" ? order : "asc"}
258+
onClick={() => handleRequestSort("name")}
259+
>
260+
Name
261+
</TableSortLabel>
262+
</TableCell>
263+
<TableCell>
264+
<TableSortLabel
265+
active={orderBy === "type"}
266+
direction={orderBy === "type" ? order : "asc"}
267+
onClick={() => handleRequestSort("type")}
268+
>
269+
Type
270+
</TableSortLabel>
271+
</TableCell>
272+
<TableCell>
273+
<TableSortLabel
274+
active={orderBy === "value"}
275+
direction={orderBy === "value" ? order : "asc"}
276+
onClick={() => handleRequestSort("value")}
277+
>
278+
Value
279+
</TableSortLabel>
280+
</TableCell>
281+
</TableRow>
282+
</TableHead>
283+
<TableBody>
284+
{visibleRows.map((stateVar) => (
285+
<TableRow key={stateVar.id}>
286+
<TableCell>{stateVar.name}</TableCell>
287+
<TableCell>{stateVar.type}</TableCell>
288+
<TableCell>
289+
{stateVar.type === "boolean"
290+
? stateVar.value
291+
? "True"
292+
: "False"
293+
: stateVar.value}
294+
</TableCell>
295+
</TableRow>
296+
))}
297+
{emptyRows > 0 && (
298+
<TableRow style={{ height: 53 * emptyRows }}>
299+
<TableCell colSpan={3} />
300+
</TableRow>
301+
)}
302+
</TableBody>
303+
<TableFooter>
304+
<TableRow>
305+
<TablePagination
306+
rowsPerPageOptions={[
307+
5,
308+
10,
309+
25,
310+
{ label: "All", value: data.length },
311+
]}
312+
colSpan={3}
313+
count={data.length}
314+
rowsPerPage={rowsPerPage}
315+
page={page}
316+
slotProps={{
317+
select: {
318+
inputProps: {
319+
"aria-label": "rows per page",
320+
},
321+
native: true,
322+
},
323+
}}
324+
onPageChange={handleChangePage}
325+
onRowsPerPageChange={handleChangeRowsPerPage}
326+
ActionsComponent={TablePaginationActions}
327+
/>
328+
</TableRow>
329+
</TableFooter>
330+
</Table>
331+
</TableContainer>
332+
</PaperContainer>
333+
);
334+
}
335+
};

frontend/src/features/dashboard/components/CustomTypes.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Badge } from "@material-ui/core";
66

77
const NodeBase = ({ data }) => (
88
<div
9-
className={`bg-black w-40 text-center ${data.visited ? "" : "brightness-60"}`}
9+
className={`bg-black w-40 text-center ${data.visited ? "" : "brightness-60"} ${data.isHighlighted ? "border-red-500 border-4" : ""}`}
1010
>
1111
<div>
1212
<Thumbnail components={data.components} />

frontend/src/features/dashboard/components/DashTopBar.jsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export default function DashTopBar({ back = "/", children = [] }) {
2626
<div className={styles.topBar}>
2727
<ul className={styles.leftTopBarList}>
2828
<li className={styles.listItem}>
29-
(
3029
<button
3130
className="btn vps w-[100px]"
3231
onClick={() => {
@@ -35,7 +34,6 @@ export default function DashTopBar({ back = "/", children = [] }) {
3534
>
3635
Back
3736
</button>
38-
)
3937
</li>
4038
</ul>
4139
<div className={styles.rightTopBarList}>{children}</div>

frontend/src/features/dashboard/components/Graph.jsx

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export default function ScenarioGraph({
1818
inNodes,
1919
inEdges,
2020
inGPathEdges,
21+
inSceneMap,
22+
inGPath,
2123
onLoaded,
2224
}) {
2325
/**
@@ -42,6 +44,7 @@ export default function ScenarioGraph({
4244
const [nodes, setNodes, onNodesChange] = useNodesState([]);
4345
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
4446
const [sizes, setSizes] = useState({});
47+
const [currentIdx, setCurrentIdx] = useState(0);
4548

4649
// Set up initial nodes and update edges with the group path if it exists
4750
const [newEdges, setNewEdges] = useState([]);
@@ -91,12 +94,44 @@ export default function ScenarioGraph({
9194
RenderGraph();
9295
}, [direction]);
9396

97+
// Update the nodes when a new selection is made
98+
useEffect(() => {
99+
handleNodeHighlighting();
100+
}, [currentIdx, setNodes]);
101+
102+
function handleNodeHighlighting() {
103+
if (inGPath.length == 0) return;
104+
setNodes((tempNodes) =>
105+
tempNodes.map((node) => {
106+
if (node.id == inSceneMap[inGPath[currentIdx]]._id) {
107+
return {
108+
...node,
109+
data: {
110+
...node.data,
111+
isHighlighted: true,
112+
},
113+
};
114+
}
115+
return {
116+
...node,
117+
data: {
118+
...node.data,
119+
isHighlighted: false,
120+
},
121+
};
122+
})
123+
);
124+
}
125+
94126
const gle = async (nodes, edges, options = {}, tempSizes) => {
95127
const graph = {
96128
id: "root",
97129
layoutOptions: options,
98130
children: nodes.map((node) => ({
99131
...node,
132+
data: {
133+
...node.data,
134+
},
100135
})),
101136
edges: edges,
102137
};
@@ -125,6 +160,7 @@ export default function ScenarioGraph({
125160
setEdges(layoutedEdges);
126161
fitView();
127162
onLoaded();
163+
handleNodeHighlighting();
128164
}
129165
);
130166
// console.log("Updated layout")
@@ -135,7 +171,6 @@ export default function ScenarioGraph({
135171
* Need to add a counter to the nodes to show how many times a group has visited that scene
136172
* Could make the nodes be grayed out and only show them normally if the group has visited them, makes it easier to visualize progress
137173
*/
138-
139174
return (
140175
<div className="h-full">
141176
<span className="text-sm opacity-80">
@@ -202,8 +237,57 @@ export default function ScenarioGraph({
202237
Reset Nodes
203238
</button>
204239
</Panel>
240+
<Panel position="center-right">
241+
{/* <NavigatorTable sceneMap={inSceneMap} groupPath={inGPath}/> */}
242+
<PathNavigator
243+
sceneMap={inSceneMap}
244+
groupPath={inGPath}
245+
idx={currentIdx}
246+
setCurrentIdx={setCurrentIdx}
247+
/>
248+
</Panel>
205249
<Background />
206250
</ReactFlow>
207251
</div>
208252
);
209253
}
254+
255+
const PathNavigator = ({ sceneMap, groupPath, idx, setCurrentIdx }) => {
256+
if (groupPath.length == 0) return <></>;
257+
function updateIndex(d) {
258+
setCurrentIdx((prev) => {
259+
const newIdx = prev + d;
260+
if (newIdx < 0 || newIdx >= groupPath.length) return prev;
261+
return newIdx;
262+
});
263+
}
264+
265+
return (
266+
<div>
267+
<div className="grid justify-items-end">
268+
<p>Current Node:</p>
269+
<p className="">{sceneMap[groupPath[idx]].name}</p>
270+
<div>
271+
<button
272+
className="bg-white text-black rounded-lg px-2 hover:cursor-pointer hover:bg-gray-200 disabled:opacity-50"
273+
disabled={idx == 0}
274+
onClick={() => {
275+
updateIndex(-1);
276+
}}
277+
>
278+
Prev
279+
</button>
280+
<button
281+
className="bg-white text-black rounded-lg px-2 hover:cursor-pointer hover:bg-gray-200 disabled:opacity-50"
282+
disabled={idx + 1 == groupPath.length}
283+
onClick={() => {
284+
updateIndex(1);
285+
}}
286+
>
287+
Next
288+
</button>
289+
</div>
290+
</div>
291+
</div>
292+
);
293+
};

0 commit comments

Comments
 (0)