Skip to content

Commit ba53c2c

Browse files
committed
Add FilterMenu, ModalitiesFilter, and KeywordFilter components for database names and modalities filtering
1 parent f01065a commit ba53c2c

File tree

5 files changed

+323
-4
lines changed

5 files changed

+323
-4
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import KeywordFilter from "./KeywordFilter";
2+
import ModalitiesFilter from "./ModalitiesFilter";
3+
import FilterListIcon from "@mui/icons-material/FilterList";
4+
import {
5+
IconButton,
6+
Menu,
7+
MenuItem,
8+
Box,
9+
Typography,
10+
Divider,
11+
} from "@mui/material";
12+
import { Colors } from "design/theme";
13+
import React, { useState, useEffect } from "react";
14+
15+
interface FilterMenuProps {
16+
onKeywordFilter: (query: string) => void;
17+
onModalitiesFilter: (selectedModalities: string[]) => void;
18+
filterKeyword: string; // receive from parent
19+
homeSelectedModalities: string[]; // receive from parent
20+
}
21+
22+
const FilterMenu: React.FC<FilterMenuProps> = ({
23+
onKeywordFilter,
24+
onModalitiesFilter,
25+
filterKeyword, //receive from home parent
26+
homeSelectedModalities, // receive from parent
27+
}) => {
28+
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
29+
// const [filterType, setFilterType] = useState<string | null>(null);
30+
const [menuKey, setMenuKey] = useState(0); // Forces re-render
31+
32+
useEffect(() => {
33+
const handleResize = () => {
34+
setMenuKey((prevKey) => prevKey + 1);
35+
};
36+
37+
window.addEventListener("resize", handleResize);
38+
return () => window.removeEventListener("resize", handleResize);
39+
}, []);
40+
41+
// Handle menu open and close
42+
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
43+
setAnchorEl(event.currentTarget);
44+
};
45+
46+
const handleClose = () => {
47+
setAnchorEl(null);
48+
// setFilterType(null); //reset menu state when closing
49+
};
50+
51+
return (
52+
<Box>
53+
{/* Filter Icon Button */}
54+
<IconButton onClick={handleClick}>
55+
<FilterListIcon sx={{ color: Colors.lightGray }} />
56+
<Typography sx={{ color: Colors.lightGray, fontWeight: "bold" }}>
57+
Databases Filter
58+
</Typography>
59+
</IconButton>
60+
61+
{/* Dropdown Menu */}
62+
<Menu
63+
anchorEl={anchorEl}
64+
open={Boolean(anchorEl)}
65+
onClose={handleClose}
66+
// sx={{ padding: "10px" }}
67+
disablePortal // Ensures positioning inside the DOM
68+
sx={{
69+
transition: "transform 0.3s ease, opacity 0.3s ease",
70+
transformOrigin: "top right",
71+
}}
72+
>
73+
{/* unified panel version */}
74+
<Box sx={{ minWidth: 300, padding: "10px" }}>
75+
{/* Keyword Filter */}
76+
<Box
77+
sx={{
78+
display: "flex",
79+
flexDirection: "column",
80+
// position: "relative",
81+
gap: "2px",
82+
}}
83+
>
84+
<Typography variant="subtitle1" sx={{ fontWeight: "bold" }}>
85+
Filter by Keyword
86+
</Typography>
87+
<KeywordFilter
88+
onFilter={onKeywordFilter}
89+
filterKeyword={filterKeyword}
90+
/>
91+
</Box>
92+
93+
<Divider sx={{ marginY: 2 }} />
94+
95+
{/* Modalities Filter */}
96+
<Box
97+
sx={{
98+
display: "flex",
99+
flexDirection: "column",
100+
position: "relative",
101+
gap: "2px",
102+
}}
103+
>
104+
<Typography variant="subtitle1" sx={{ fontWeight: "bold" }}>
105+
Filter by Modalities
106+
</Typography>
107+
<ModalitiesFilter
108+
onFilter={onModalitiesFilter}
109+
homeSeletedModalities={homeSelectedModalities}
110+
/>
111+
</Box>
112+
</Box>
113+
114+
{/* split panel version*/}
115+
{/* <Box sx={{ display: "flex", minWidth: 400, padding: "10px" }}>
116+
Left Side - Filter Options
117+
<Box sx={{ width: "40%", paddingRight: "10px" }}>
118+
<Typography variant="subtitle1" sx={{ fontWeight: "bold" }}>
119+
Filter Options
120+
</Typography>
121+
<Divider sx={{ marginY: 1 }} />
122+
<MenuItem
123+
onClick={() => setFilterType("keyword")}
124+
selected={filterType === "keyword"}
125+
>
126+
Filter by Keyword
127+
</MenuItem>
128+
<MenuItem
129+
onClick={() => setFilterType("modalities")}
130+
selected={filterType === "modalities"}
131+
>
132+
Filter by Modalities
133+
</MenuItem>
134+
</Box>
135+
Right Side - Dynamic Filter Panel
136+
<Box sx={{ width: "60%", paddingLeft: "10px" }}>
137+
{filterType === "keyword" && (
138+
<KeywordFilter onFilter={onKeywordFilter} />
139+
)}
140+
{filterType === "modalities" && (
141+
<ModalitiesFilter onFilter={onModalitiesFilter} />
142+
)}
143+
{!filterType && (
144+
<Typography variant="body2" sx={{ color: Colors.textSecondary }}>
145+
Select a filter option on the left.
146+
</Typography>
147+
)}
148+
</Box>
149+
</Box> */}
150+
</Menu>
151+
</Box>
152+
);
153+
};
154+
155+
export default FilterMenu;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Autocomplete, TextField } from "@mui/material";
2+
import React, { useEffect, useState } from "react";
3+
4+
interface FilterSearchProps {
5+
onFilter: (query: string) => void;
6+
filterKeyword: string; // add prop to receive parent state
7+
}
8+
9+
const KeywordFilter: React.FC<FilterSearchProps> = ({
10+
onFilter,
11+
filterKeyword,
12+
}) => {
13+
const [inputValue, setInputValue] = useState<string>(filterKeyword);
14+
15+
useEffect(() => {
16+
setInputValue(filterKeyword);
17+
}, [filterKeyword]);
18+
19+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
20+
const value = e.target.value;
21+
setInputValue(value);
22+
onFilter(value); // Pass value to parent component
23+
};
24+
25+
return (
26+
<TextField
27+
label="Browse databases by keyword"
28+
variant="outlined"
29+
size="small"
30+
value={inputValue}
31+
onChange={handleChange}
32+
sx={{
33+
width: 250,
34+
// position: "absolute",
35+
// top: 20,
36+
// right: 20,
37+
background: "white",
38+
borderRadius: 1,
39+
// zIndex: 10,
40+
}}
41+
/>
42+
);
43+
};
44+
45+
export default KeywordFilter;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Box, FormControlLabel, Checkbox, Typography } from "@mui/material";
2+
import React, { useEffect, useState } from "react";
3+
4+
interface ModalitiesFilterProps {
5+
onFilter: (selectedModalities: string[]) => void;
6+
homeSeletedModalities: string[]; // add prop to receive parent state
7+
}
8+
9+
const modalitiesList = ["mri", "pet", "meg", "eeg", "ieeg", "dwi", "fnirs"];
10+
11+
const ModalitiesFilter: React.FC<ModalitiesFilterProps> = ({
12+
onFilter,
13+
homeSeletedModalities,
14+
}) => {
15+
const [selectedModalities, setSelectedModalities] = useState<string[]>(
16+
homeSeletedModalities
17+
);
18+
19+
useEffect(() => {
20+
setSelectedModalities(homeSeletedModalities);
21+
}, [homeSeletedModalities]);
22+
23+
const handleModalityChange = (modality: string) => {
24+
const updatedModalities = selectedModalities.includes(modality)
25+
? selectedModalities.filter((m) => m !== modality)
26+
: [...selectedModalities, modality];
27+
setSelectedModalities(updatedModalities);
28+
onFilter(updatedModalities);
29+
};
30+
31+
return (
32+
<Box>
33+
{/* <Typography variant="subtitle1"> Select Modalities</Typography> */}
34+
{modalitiesList.map((modality) => (
35+
<Box sx={{ display: "flex", flexDirection: "column" }}>
36+
<FormControlLabel
37+
key={modality}
38+
control={
39+
<Checkbox
40+
checked={selectedModalities.includes(modality)}
41+
onChange={() => handleModalityChange(modality)}
42+
/>
43+
}
44+
label={modality}
45+
/>
46+
</Box>
47+
))}
48+
</Box>
49+
);
50+
};
51+
52+
export default ModalitiesFilter;

src/modules/universe/NeuroJsonGraph.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ const NeuroJsonGraph: React.FC<{
282282
ref={graphRef}
283283
style={{
284284
width: "100%",
285+
marginLeft: "5%",
285286
maxHeight: "99%",
286287
backgroundColor: "transparent",
287288
position: "relative",

src/pages/Home.tsx

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import {
77
CircularProgress,
88
} from "@mui/material";
99
import NodeInfoPanel from "components/NodeInfoPanel";
10+
// import KeywordFilter from "components/NodesFilter/KeywordFilter";
11+
// import ModalitiesFilter from "components/NodesFilter/ModalitiesFilter";
12+
import FilterMenu from "components/NodesFilter/FilterMenu";
1013
import { Colors } from "design/theme";
1114
import { useAppDispatch } from "hooks/useAppDispatch";
1215
import { useAppSelector } from "hooks/useAppSelector";
@@ -25,6 +28,8 @@ const Home: React.FC = () => {
2528
// State for selected node and panel visibility
2629
const [selectedNode, setSelectedNode] = useState<NodeObject | null>(null);
2730
const [panelOpen, setPanelOpen] = useState(false);
31+
const [filterKeyword, setFilterKeyword] = useState<string>(""); // State for filter input
32+
const [selectedModalities, setSelectedModalities] = useState<string[]>([]);
2833

2934
useEffect(() => {
3035
dispatch(fetchRegistry());
@@ -36,6 +41,37 @@ const Home: React.FC = () => {
3641
setPanelOpen(true);
3742
};
3843

44+
// const filteredRegistry = registry
45+
// ? registry.filter((node) =>
46+
// node.name.toLowerCase().includes(filterKeyword.toLowerCase())
47+
// )
48+
// : [];
49+
50+
// filter logic
51+
const filteredRegistry = registry
52+
? registry.filter((node) => {
53+
const matchKeyword = node.name
54+
.toLowerCase()
55+
.includes(filterKeyword.toLowerCase());
56+
const matchModalities =
57+
selectedModalities.length === 0 ||
58+
// selectedModalities.some((modality) =>
59+
// node.datatype.includes(modality)
60+
// );
61+
selectedModalities.some((modality) =>
62+
Array.isArray(node.datatype)
63+
? node.datatype.includes(modality)
64+
: node.datatype === modality
65+
);
66+
67+
return matchKeyword && matchModalities;
68+
})
69+
: [];
70+
71+
// const handleModalitiesFilter = (modalities: string[]) => {
72+
// setSelectedModalities(modalities);
73+
// };
74+
3975
return (
4076
<Container
4177
style={{
@@ -44,11 +80,38 @@ const Home: React.FC = () => {
4480
padding: 0,
4581
overflow: "hidden",
4682
position: "relative",
83+
minHeight: "500px", // make sure the view databases card won't be cut when no nodes showing
4784
}}
4885
>
86+
{/* <Box sx={{ position: "absolute", top: 20, right: 20, zIndex: 10 }}>
87+
<KeywordFilter onFilter={(query: string) => setFilterKeyword(query)} />
88+
</Box>
89+
<Box
90+
sx={{
91+
position: "absolute",
92+
top: 100,
93+
right: 20,
94+
zIndex: 10,
95+
backgroundColor: "white",
96+
p: 2,
97+
borderRadius: 2,
98+
}}
99+
>
100+
<ModalitiesFilter onFilter={handleModalitiesFilter} />
101+
</Box> */}
102+
103+
{/* Filter Menu Button */}
104+
<Box sx={{ position: "absolute", top: 20, right: 20, zIndex: 10 }}>
105+
<FilterMenu
106+
onKeywordFilter={setFilterKeyword}
107+
onModalitiesFilter={setSelectedModalities}
108+
filterKeyword={filterKeyword}
109+
homeSelectedModalities={selectedModalities}
110+
/>
111+
</Box>
49112
<Box
50113
sx={{
51-
zIndex: "2",
114+
zIndex: "4",
52115
position: "relative",
53116
width: "100%",
54117
overflow: "hidden",
@@ -58,12 +121,15 @@ const Home: React.FC = () => {
58121
<Box sx={{ display: "flex", justifyContent: "center", mt: 4 }}>
59122
<CircularProgress sx={{ color: Colors.primary.main }} />
60123
</Box>
61-
) : registry && registry.length > 0 ? (
62-
<NeuroJsonGraph registry={registry} onNodeClick={handleNodeClick} />
124+
) : filteredRegistry.length > 0 ? (
125+
<NeuroJsonGraph
126+
registry={filteredRegistry}
127+
onNodeClick={handleNodeClick}
128+
/>
63129
) : (
64130
<Box sx={{ textAlign: "center", mt: 4 }}>
65131
<Typography variant="h6" color={Colors.textSecondary}>
66-
No data available to display
132+
No matching nodes found
67133
</Typography>
68134
</Box>
69135
)}

0 commit comments

Comments
 (0)