Skip to content

Commit 9e9e058

Browse files
authored
Merge pull request #103 from nanoapi-io/feature/symbol-level-view
This PR introduces: - A symbol-level view. This view can be accessed currently through inspecting a file node, then double-clicking on the symbol to inspect. - Dependency depth filter: change how deep into the dep graph you want to see dependency nodes (purple). This number can be set to 0 to hide dependencies while only showing dependents, for instance. - Dependent depth filter: same functionality as above, but for dependents. (pink). These two filters can be changes independently. - Refactors to enable control bar extensions through react child elements.
2 parents 86fa205 + 2fd5283 commit 9e9e058

File tree

9 files changed

+1601
-385
lines changed

9 files changed

+1601
-385
lines changed
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import { useState, useEffect, useRef } from "react";
2+
import { Button, DropdownMenu, Checkbox } from "@radix-ui/themes";
3+
import { LuChevronUp } from "react-icons/lu";
4+
import { Core } from "cytoscape";
5+
import { toast } from "react-toastify";
6+
import { MdFilterAlt } from "react-icons/md";
7+
8+
const INITIAL_ELEMENT_LIMIT = 75;
9+
10+
interface FiltersType {
11+
showExternal: boolean;
12+
showInternal: boolean;
13+
showVariables: boolean;
14+
showFunctions: boolean;
15+
showClasses: boolean;
16+
}
17+
18+
export default function FiltersExtension(props: {
19+
busy: boolean;
20+
cy: Core;
21+
onLayout: () => void;
22+
}) {
23+
const initialized = useRef(false);
24+
const [filters, setFilters] = useState<FiltersType>({
25+
showExternal: true,
26+
showInternal: true,
27+
showVariables: true,
28+
showFunctions: true,
29+
showClasses: true,
30+
});
31+
32+
function checkFiltersSet() {
33+
if (!filters) return false;
34+
35+
// For boolean filters, assuming they take the form "show X"
36+
// then we need to return false if they are set to their default value
37+
if (!filters.showExternal) {
38+
return true;
39+
}
40+
41+
if (!filters.showInternal) {
42+
return true;
43+
}
44+
45+
if (!filters.showVariables) {
46+
return true;
47+
}
48+
49+
if (!filters.showFunctions) {
50+
return true;
51+
}
52+
53+
if (!filters.showClasses) {
54+
return true;
55+
}
56+
57+
return false;
58+
}
59+
60+
// Check the number of elements on the file view
61+
// if there are more than 50, show a toast
62+
useEffect(() => {
63+
if (!props.cy) return;
64+
65+
const elements = props.cy.elements();
66+
67+
// Handle strict mode for dev
68+
if (!initialized.current) {
69+
initialized.current = true;
70+
return;
71+
}
72+
73+
if (elements.length > INITIAL_ELEMENT_LIMIT) {
74+
toast.info(
75+
"There are more than 75 elements on screen. We recommend using the filters in the control bar to get a better view of the graph.",
76+
);
77+
}
78+
}, []);
79+
80+
// Show and hide external nodes and edges
81+
useEffect(() => {
82+
if (!props.cy) return;
83+
84+
// Grab only the external nodes
85+
const nodes = props.cy.nodes().filter((node) => {
86+
return node.data("isExternal");
87+
});
88+
89+
if (filters.showExternal) {
90+
nodes.removeClass("hidden");
91+
} else {
92+
nodes.addClass("hidden");
93+
}
94+
}, [filters.showExternal]);
95+
96+
// Show and hide internal nodes and edges
97+
useEffect(() => {
98+
if (!props.cy) return;
99+
100+
// Grab only the internal nodes
101+
const nodes = props.cy.nodes().filter((node) => {
102+
return !node.data("isExternal") && !node.data("isCurrentFile");
103+
});
104+
if (filters.showInternal) {
105+
nodes.removeClass("hidden");
106+
} else {
107+
nodes.addClass("hidden");
108+
}
109+
}, [filters.showInternal]);
110+
111+
// Show and hide variables
112+
useEffect(() => {
113+
if (!props.cy) return;
114+
115+
// Grab only the variable nodes
116+
const nodes = props.cy.nodes().filter((node) => {
117+
return (
118+
node.data("customData").instance?.type === "variable" &&
119+
node.data("isCurrentFile")
120+
);
121+
});
122+
if (filters.showVariables) {
123+
nodes.removeClass("hidden");
124+
} else {
125+
nodes.addClass("hidden");
126+
}
127+
}, [filters.showVariables]);
128+
129+
// Show and hide functions
130+
useEffect(() => {
131+
if (!props.cy) return;
132+
133+
// Grab only the function nodes
134+
const nodes = props.cy.nodes().filter((node) => {
135+
return (
136+
node.data("customData").instance?.type === "function" &&
137+
node.data("isCurrentFile")
138+
);
139+
});
140+
if (filters.showFunctions) {
141+
nodes.removeClass("hidden");
142+
} else {
143+
nodes.addClass("hidden");
144+
}
145+
}, [filters.showFunctions]);
146+
147+
// Show and hide classes
148+
useEffect(() => {
149+
if (!props.cy) return;
150+
151+
// Grab only the class nodes
152+
const nodes = props.cy.nodes().filter((node) => {
153+
return (
154+
node.data("customData").instance?.type === "class" &&
155+
node.data("isCurrentFile")
156+
);
157+
});
158+
159+
if (filters.showClasses) {
160+
nodes.removeClass("hidden");
161+
} else {
162+
nodes.addClass("hidden");
163+
}
164+
}, [filters.showClasses]);
165+
166+
return (
167+
<DropdownMenu.Root>
168+
<DropdownMenu.Trigger>
169+
<Button
170+
size="1"
171+
variant="ghost"
172+
color="violet"
173+
highContrast
174+
className={`${checkFiltersSet() ? "bg-primary-light/20 dark:bg-primary-dark/20" : ""}`}
175+
disabled={props.busy}
176+
onClick={() => props.onLayout()}
177+
>
178+
<MdFilterAlt
179+
className={`text-xl h-5 w-5 ${
180+
checkFiltersSet()
181+
? "text-primary-light dark:text-primary-dark"
182+
: "text-gray-light dark:text-gray-dark"
183+
}`}
184+
/>
185+
<LuChevronUp />
186+
</Button>
187+
</DropdownMenu.Trigger>
188+
<DropdownMenu.Content color="violet" variant="soft">
189+
{/* Add filter options here */}
190+
<DropdownMenu.Label>Supporting files</DropdownMenu.Label>
191+
<DropdownMenu.Item
192+
// This keeps the search typing from changing focus
193+
textValue=""
194+
onClick={(e: React.MouseEvent) => {
195+
e.preventDefault();
196+
setFilters({ ...filters, showExternal: !filters.showExternal });
197+
}}
198+
className="flex justify-between"
199+
>
200+
<span>Show external</span>
201+
<Checkbox
202+
color="violet"
203+
checked={filters.showExternal}
204+
onCheckedChange={(checked) =>
205+
setFilters({ ...filters, showExternal: Boolean(checked) })
206+
}
207+
/>
208+
</DropdownMenu.Item>
209+
<DropdownMenu.Item
210+
// This keeps the search typing from changing focus
211+
textValue=""
212+
onClick={(e: React.MouseEvent) => {
213+
e.preventDefault();
214+
setFilters({ ...filters, showInternal: !filters.showInternal });
215+
}}
216+
className="flex justify-between"
217+
>
218+
<span>Show internal</span>
219+
<Checkbox
220+
checked={filters.showInternal}
221+
onCheckedChange={(checked) =>
222+
setFilters({ ...filters, showInternal: Boolean(checked) })
223+
}
224+
/>
225+
</DropdownMenu.Item>
226+
<DropdownMenu.Separator />
227+
<DropdownMenu.Label>Main file</DropdownMenu.Label>
228+
<DropdownMenu.Item
229+
// This keeps the search typing from changing focus
230+
textValue=""
231+
onClick={(e: React.MouseEvent) => {
232+
e.preventDefault();
233+
setFilters({ ...filters, showVariables: !filters.showVariables });
234+
}}
235+
className="flex justify-between"
236+
>
237+
<span>Show variables</span>
238+
<Checkbox
239+
checked={filters.showVariables}
240+
onCheckedChange={(checked) =>
241+
setFilters({ ...filters, showVariables: Boolean(checked) })
242+
}
243+
/>
244+
</DropdownMenu.Item>
245+
<DropdownMenu.Item
246+
// This keeps the search typing from changing focus
247+
textValue=""
248+
onClick={(e: React.MouseEvent) => {
249+
e.preventDefault();
250+
setFilters({ ...filters, showFunctions: !filters.showFunctions });
251+
}}
252+
className="flex justify-between"
253+
>
254+
<span>Show functions</span>
255+
<Checkbox
256+
checked={filters.showFunctions}
257+
onCheckedChange={(checked) =>
258+
setFilters({ ...filters, showFunctions: Boolean(checked) })
259+
}
260+
/>
261+
</DropdownMenu.Item>
262+
<DropdownMenu.Item
263+
// This keeps the search typing from changing focus
264+
textValue=""
265+
onClick={(e: React.MouseEvent) => {
266+
e.preventDefault();
267+
setFilters({ ...filters, showClasses: !filters.showClasses });
268+
}}
269+
className="flex justify-between"
270+
>
271+
<span>Show classes</span>
272+
<Checkbox
273+
checked={filters.showClasses}
274+
onCheckedChange={(checked) =>
275+
setFilters({ ...filters, showClasses: Boolean(checked) })
276+
}
277+
/>
278+
</DropdownMenu.Item>
279+
<DropdownMenu.Separator />
280+
<Button
281+
variant="ghost"
282+
color="violet"
283+
disabled={!checkFiltersSet()}
284+
onClick={() => {
285+
setFilters({
286+
showExternal: true,
287+
showInternal: true,
288+
showVariables: true,
289+
showFunctions: true,
290+
showClasses: true,
291+
});
292+
}}
293+
className="mx-3"
294+
>
295+
Clear filters
296+
</Button>
297+
</DropdownMenu.Content>
298+
</DropdownMenu.Root>
299+
);
300+
}

0 commit comments

Comments
 (0)