From 04fed23aaaf99decd347f73444a059fbdc50b93e Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Wed, 26 Mar 2025 13:53:27 +0200 Subject: [PATCH 1/9] Add loading indicators for graph fetching and options loading - Introduced `isFetchingGraph` state to manage loading state in the graph component. - Updated `CodeGraph` to display a loading spinner while fetching the graph. - Enhanced `Combobox` to show a loading state when fetching options. --- app/components/code-graph.tsx | 23 ++++++++---- app/components/combobox.tsx | 67 +++++++++++++++++++++++------------ app/components/graphView.tsx | 1 - app/page.tsx | 43 ++++++++++++---------- 4 files changed, 85 insertions(+), 49 deletions(-) diff --git a/app/components/code-graph.tsx b/app/components/code-graph.tsx index fdf31e5f..207ef111 100644 --- a/app/components/code-graph.tsx +++ b/app/components/code-graph.tsx @@ -2,7 +2,7 @@ import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import { Graph, GraphData, Node, Link } from "./model"; import { Toolbar } from "./toolbar"; import { Labels } from "./labels"; -import { Download, GitFork, Search, X } from "lucide-react"; +import { Download, GitFork, Loader2, Search, X } from "lucide-react"; import ElementMenu from "./elementMenu"; import Combobox from "./combobox"; import { toast } from '@/components/ui/use-toast'; @@ -22,6 +22,7 @@ interface Props { data: GraphData, setData: Dispatch>, onFetchGraph: (graphName: string) => Promise, + isFetchingGraph: boolean, onFetchNode: (nodeIds: number[]) => Promise, options: string[] setOptions: Dispatch> @@ -49,9 +50,8 @@ export function CodeGraph({ data, setData, onFetchGraph, + isFetchingGraph, onFetchNode, - options, - setOptions, isShowPath, setPath, chartRef, @@ -82,6 +82,7 @@ export function CodeGraph({ const [commitIndex, setCommitIndex] = useState(0); const [currentCommit, setCurrentCommit] = useState(0); const containerRef = useRef(null); + const [options, setOptions] = useState([]); useEffect(() => { setData({ ...graph.Elements }) @@ -370,10 +371,18 @@ export function CodeGraph({ - :
- -

Select a repo to show its graph here

-
+ : ( + isFetchingGraph ? +
+ +

Fetching graph...

+
+ : +
+ +

Select a repo to show its graph here

+
+ ) } {/* { diff --git a/app/components/combobox.tsx b/app/components/combobox.tsx index c22d9334..63bc877e 100644 --- a/app/components/combobox.tsx +++ b/app/components/combobox.tsx @@ -1,5 +1,6 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { toast } from "@/components/ui/use-toast"; +import { Loader2 } from "lucide-react"; import { useEffect, useState } from "react"; interface Props { @@ -13,55 +14,75 @@ interface Props { export default function Combobox({ options, setOptions, selectedValue, onSelectedValue }: Props) { const [open, setOpen] = useState(false) - const [lastOpened, setLastOpened] = useState(); + const [lastFetch, setLastFetch] = useState(); + const [isFetchingOptions, setIsFetchingOptions] = useState(false) const fetchOptions = async () => { - const result = await fetch(`/api/repo`, { - method: 'GET', - }) + setIsFetchingOptions(true) - if (!result.ok) { - toast({ - variant: "destructive", - title: "Uh oh! Something went wrong.", - description: await result.text(), + try { + const result = await fetch(`/api/repo`, { + method: 'GET', }) - return - } - const json = await result.json() - setOptions(json.result) + if (!result.ok) { + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: await result.text(), + }) + return + } + + const json = await result.json() + setOptions(json.result) + } finally { + setIsFetchingOptions(false) + } } useEffect(() => { fetchOptions() }, []) + //fetch options when the combobox is opened useEffect(() => { if (!open) return const now = Date.now(); - if (lastOpened && now - lastOpened < 30000) return; - - setLastOpened(now); - + //check if last fetch was less than 30 seconds ago + if (lastFetch && now - lastFetch < 30000) return; + + setLastFetch(now); + fetchOptions() }, [open]) return ( - { - options.length !== 0 && - options.map((option) => ( - - {option} + isFetchingOptions ? + +
+ +

Fetching options...

+
- )) + : options.length !== 0 ? + options.map((option) => ( + + {option} + + )) + : + +

No options found

+
}
diff --git a/app/components/graphView.tsx b/app/components/graphView.tsx index c221e0ac..ea9387d1 100644 --- a/app/components/graphView.tsx +++ b/app/components/graphView.tsx @@ -352,7 +352,6 @@ export default function GraphView({ onZoom={() => unsetSelectedObjects()} onEngineStop={() => { setCooldownTicks(0) - debugger handleZoomToFit(chartRef, zoomedNodes.length === 1 ? 4 : 1, (n: NodeObject) => zoomedNodes.some(node => node.id === n.id)) setZoomedNodes([]) }} diff --git a/app/page.tsx b/app/page.tsx index ce2cab5a..d349482c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -79,6 +79,7 @@ export default function Home() { const [activeIndex, setActiveIndex] = useState(0) const [carouselApi, setCarouselApi] = useState() const [zoomedNodes, setZoomedNodes] = useState([]) + const [isFetchingGraph, setIsFetchingGraph] = useState(false) useEffect(() => { if (path?.start?.id && path?.end?.id) { @@ -138,27 +139,31 @@ export default function Home() { async function onFetchGraph(graphName: string) { setGraph(Graph.empty()) + setIsFetchingGraph(true) + try { + const result = await fetch(`/api/repo/${prepareArg(graphName)}`, { + method: 'GET' + }) - const result = await fetch(`/api/repo/${prepareArg(graphName)}`, { - method: 'GET' - }) + if (!result.ok) { + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: await result.text(), + }) + return + } - if (!result.ok) { - toast({ - variant: "destructive", - title: "Uh oh! Something went wrong.", - description: await result.text(), - }) - return + const json = await result.json() + const g = Graph.create(json.result.entities, graphName) + setGraph(g) + setIsPathResponse(false) + chatPanel.current?.expand() + // @ts-ignore + window.graph = g + } finally { + setIsFetchingGraph(false) } - - const json = await result.json() - const g = Graph.create(json.result.entities, graphName) - setGraph(g) - setIsPathResponse(false) - chatPanel.current?.expand() - // @ts-ignore - window.graph = g } // Send the user query to the server to expand a node @@ -391,6 +396,7 @@ export default function Home() { options={options} setOptions={setOptions} onFetchGraph={onFetchGraph} + isFetchingGraph={isFetchingGraph} onFetchNode={onFetchNode} setPath={setPath} isShowPath={!!path} @@ -509,6 +515,7 @@ export default function Home() { options={options} setOptions={setOptions} onFetchGraph={onFetchGraph} + isFetchingGraph={isFetchingGraph} onFetchNode={onFetchNode} setPath={setPath} isShowPath={!!path} From ef7dacae56ad8b9d3132d1a5d0b0afea00fec418 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Wed, 26 Mar 2025 14:47:13 +0200 Subject: [PATCH 2/9] Enhance Combobox to disable when no options are available - Updated the Combobox component to disable the select input when there are no options and not fetching options, improving user experience. --- app/components/combobox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/combobox.tsx b/app/components/combobox.tsx index 63bc877e..98eabbb1 100644 --- a/app/components/combobox.tsx +++ b/app/components/combobox.tsx @@ -60,7 +60,7 @@ export default function Combobox({ options, setOptions, selectedValue, onSelecte }, [open]) return ( - From 17706ca1d12f02305c6e56e8c131d352e4b227ca Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Mon, 16 Mar 2026 16:30:24 +0200 Subject: [PATCH 3/9] Potential fix for pull request finding 'Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- app/src/components/code-graph.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/code-graph.tsx b/app/src/components/code-graph.tsx index 12be7dc6..d4309d61 100644 --- a/app/src/components/code-graph.tsx +++ b/app/src/components/code-graph.tsx @@ -2,7 +2,7 @@ import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import { Graph, GraphData, Node, Link } from "./model"; import { Toolbar } from "./toolbar"; import { Labels } from "./labels"; -import { Download, GitFork, Loader2, Search, X } from "lucide-react"; +import { GitFork, Loader2, Search, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import ElementMenu from "./elementMenu"; import Combobox from "./combobox"; From 1bab8620297e3595a90016b3b8613dd0e76786ca Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Wed, 26 Mar 2025 13:53:27 +0200 Subject: [PATCH 4/9] Add loading indicators for graph fetching and options loading - Introduced `isFetchingGraph` state to manage loading state in the graph component. - Updated `CodeGraph` to display a loading spinner while fetching the graph. - Enhanced `Combobox` to show a loading state when fetching options. --- app/src/components/code-graph.tsx | 23 +++++++--- app/src/components/combobox.tsx | 75 +++++++++++++++++++++---------- 2 files changed, 67 insertions(+), 31 deletions(-) diff --git a/app/src/components/code-graph.tsx b/app/src/components/code-graph.tsx index c0b2a083..12be7dc6 100644 --- a/app/src/components/code-graph.tsx +++ b/app/src/components/code-graph.tsx @@ -2,7 +2,7 @@ import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import { Graph, GraphData, Node, Link } from "./model"; import { Toolbar } from "./toolbar"; import { Labels } from "./labels"; -import { Download, GitFork, Search, X } from "lucide-react"; +import { Download, GitFork, Loader2, Search, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import ElementMenu from "./elementMenu"; import Combobox from "./combobox"; @@ -28,6 +28,7 @@ interface Props { data: GraphData, setData: Dispatch>, onFetchGraph: (graphName: string) => Promise, + isFetchingGraph: boolean, onFetchNode: (nodeIds: number[]) => Promise, options: string[] setOptions: Dispatch> @@ -58,9 +59,8 @@ export function CodeGraph({ data, setData, onFetchGraph, + isFetchingGraph, onFetchNode, - options, - setOptions, isShowPath, setPath, canvasRef, @@ -93,6 +93,7 @@ export function CodeGraph({ const [commitIndex, setCommitIndex] = useState(0); const [currentCommit, setCurrentCommit] = useState(0); const containerRef = useRef(null); + const [options, setOptions] = useState([]); useEffect(() => { setData({ ...graph.Elements }) @@ -516,10 +517,18 @@ export function CodeGraph({ - :
- -

Select a repo to show its graph here

-
+ : ( + isFetchingGraph ? +
+ +

Fetching graph...

+
+ : +
+ +

Select a repo to show its graph here

+
+ ) } {/* { diff --git a/app/src/components/combobox.tsx b/app/src/components/combobox.tsx index 93c6720c..75c69872 100644 --- a/app/src/components/combobox.tsx +++ b/app/src/components/combobox.tsx @@ -1,5 +1,6 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { toast } from "@/components/ui/use-toast"; +import { Loader2 } from "lucide-react"; import { useEffect, useState } from "react"; const AUTH_HEADERS: HeadersInit = import.meta.env.VITE_SECRET_TOKEN @@ -17,60 +18,86 @@ interface Props { export default function Combobox({ options, setOptions, selectedValue, onSelectedValue }: Props) { const [open, setOpen] = useState(false) - const [lastOpened, setLastOpened] = useState(); + const [lastFetch, setLastFetch] = useState(); + const [isFetchingOptions, setIsFetchingOptions] = useState(false) const fetchOptions = async () => { - const result = await fetch(`/api/list_repos`, { - method: 'GET', - headers: { - ...AUTH_HEADERS, - }, - }) + setIsFetchingOptions(true) - if (!result.ok) { + try { + const result = await fetch(`/api/list_repos`, { + method: 'GET', + headers: { + ...AUTH_HEADERS, + }, + }) + + if (!result.ok) { + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: await result.text(), + }) + return + } + + const json = await result.json() + setOptions(json.repositories) + } catch { toast({ variant: "destructive", title: "Uh oh! Something went wrong.", - description: await result.text(), + description: "Failed to fetch repositories. Please try again.", }) - return + } finally { + setIsFetchingOptions(false) } - - const json = await result.json() - setOptions(json.repositories) } useEffect(() => { fetchOptions() }, []) + //fetch options when the combobox is opened useEffect(() => { if (!open) return const now = Date.now(); - if (lastOpened && now - lastOpened < 30000) return; - - setLastOpened(now); - + //check if last fetch was less than 30 seconds ago + if (lastFetch && now - lastFetch < 30000) return; + + setLastFetch(now); + fetchOptions() }, [open]) return ( - { - options.length !== 0 && - options.map((option) => ( - - {option} + isFetchingOptions ? + +
+ +

Fetching options...

+
- )) + : options.length !== 0 ? + options.map((option) => ( + + {option} + + )) + : + +

No options found

+
}
) -} \ No newline at end of file +} From 1b62f1917fb9836f83c2b70a51f7aea5fe7e7dbe Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Mon, 16 Mar 2026 16:30:24 +0200 Subject: [PATCH 5/9] Potential fix for pull request finding 'Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- app/src/components/code-graph.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/code-graph.tsx b/app/src/components/code-graph.tsx index 12be7dc6..d4309d61 100644 --- a/app/src/components/code-graph.tsx +++ b/app/src/components/code-graph.tsx @@ -2,7 +2,7 @@ import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import { Graph, GraphData, Node, Link } from "./model"; import { Toolbar } from "./toolbar"; import { Labels } from "./labels"; -import { Download, GitFork, Loader2, Search, X } from "lucide-react"; +import { GitFork, Loader2, Search, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import ElementMenu from "./elementMenu"; import Combobox from "./combobox"; From c7fcd10266fb4bf51c49f509e5ed406ffe554f22 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Sat, 21 Mar 2026 13:35:00 +0200 Subject: [PATCH 6/9] fix(frontend): pass isFetchingGraph prop to CodeGraph components Add isFetchingGraph state to App.tsx and pass it to both desktop and mobile CodeGraph components. Set true on fetch start, false in finally block so the loading spinner renders correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/src/App.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/App.tsx b/app/src/App.tsx index 156b1313..7bdf15dd 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -90,6 +90,7 @@ export default function App() { const [carouselApi, setCarouselApi] = useState() const [zoomedNodes, setZoomedNodes] = useState([]) const [hasHiddenElements, setHasHiddenElements] = useState(false); + const [isFetchingGraph, setIsFetchingGraph] = useState(false); useEffect(() => { if (path?.start?.id && path?.end?.id) { @@ -153,6 +154,7 @@ export default function App() { } async function onFetchGraph(graphName: string) { + setIsFetchingGraph(true); try { const result = await fetch(`/api/graph_entities?repo=${prepareArg(graphName)}`, { method: 'GET', @@ -186,6 +188,8 @@ export default function App() { title: "Uh oh! Something went wrong.", description: "Failed to load repository graph. Please try again.", }) + } finally { + setIsFetchingGraph(false); } } @@ -527,6 +531,7 @@ export default function App() { options={options} setOptions={setOptions} onFetchGraph={onFetchGraph} + isFetchingGraph={isFetchingGraph} onFetchNode={onFetchNode} setPath={setPath} isShowPath={!!path} @@ -654,6 +659,7 @@ export default function App() { options={options} setOptions={setOptions} onFetchGraph={onFetchGraph} + isFetchingGraph={isFetchingGraph} onFetchNode={onFetchNode} setPath={setPath} isShowPath={!!path} From 78780de10d0736774e6d396ae9b8ec671c69f6b5 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Tue, 24 Mar 2026 23:17:40 +0200 Subject: [PATCH 7/9] fix: revert broken backend/test changes to match staging entity model The PR bundled test refactors that assumed entity classes (Function, Struct, Class) and a File(path, name, ext) constructor that don't exist on staging. Also reverts incorrect pygit2 -> gitpython switch. Keeps the frontend loading state changes (the actual purpose of this PR) intact while fixing all File.__init__ wrong-argument issues. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/graph.py | 25 ++++------------- tests/test_c_analyzer.py | 48 ++++++++++++++++++------------- tests/test_git_history.py | 21 +++++++------- tests/test_graph_ops.py | 59 +++++++++++++++++++++++---------------- tests/test_py_analyzer.py | 50 +++++++++++++++++++++------------ 5 files changed, 111 insertions(+), 92 deletions(-) diff --git a/api/graph.py b/api/graph.py index 085dfde1..eda72e63 100644 --- a/api/graph.py +++ b/api/graph.py @@ -426,9 +426,10 @@ def delete_files(self, files: list[Path]) -> tuple[str, dict, list[int]]: return None - def get_file(self, path: str, name: str, ext: str) -> Optional[File]: + def get_file(self, path: str, name: str, ext: str) -> Optional[Node]: """ - Retrieves a File entity from the graph database based on its path, name, and extension. + Retrieves a File node from the graph database based on its path, name, + and extension. Args: path (str): The file path. @@ -436,12 +437,7 @@ def get_file(self, path: str, name: str, ext: str) -> Optional[File]: ext (str): The file extension. Returns: - Optional[File]: The File object if found, otherwise None. - - This method constructs and executes a query to find a file node in the graph - database with the specified path, name, and extension. If the file node is found, - it creates and returns a File object with its properties and ID. If no such node - is found, it returns None. + Optional[Node]: The File node if found, otherwise None. Example: file = self.get_file('/path/to/file', 'filename', '.py') @@ -452,19 +448,10 @@ def get_file(self, path: str, name: str, ext: str) -> Optional[File]: params = {'path': path, 'name': name, 'ext': ext} res = self._query(q, params) - if(len(res.result_set) == 0): + if len(res.result_set) == 0: return None - node = res.result_set[0][0] - - ext = node.properties['ext'] - path = node.properties['path'] - name = node.properties['name'] - file = File(path, name, ext) - - file.id = node.id - - return file + return res.result_set[0][0] # set file code coverage # if file coverage is 100% set every defined function coverage to 100% aswell diff --git a/tests/test_c_analyzer.py b/tests/test_c_analyzer.py index ac7f90bd..ea8a16b7 100644 --- a/tests/test_c_analyzer.py +++ b/tests/test_c_analyzer.py @@ -2,7 +2,8 @@ import unittest from pathlib import Path -from api import SourceAnalyzer, File, Struct, Function, Graph +from api import SourceAnalyzer, Graph + class Test_C_Analyzer(unittest.TestCase): def test_analyzer(self): @@ -24,37 +25,44 @@ def test_analyzer(self): analyzer.analyze_local_folder(path, g) f = g.get_file('', 'src.c', '.c') - self.assertEqual(File('', 'src.c', '.c'), f) + self.assertIsNotNone(f) + self.assertEqual(f.properties['name'], 'src.c') + self.assertEqual(f.properties['ext'], '.c') s = g.get_struct_by_name('exp') - expected_s = Struct('src.c', 'exp', '', 9, 13) - expected_s.add_field('i', 'int') - expected_s.add_field('f', 'float') - expected_s.add_field('data', 'char[]') - self.assertEqual(expected_s, s) + self.assertIsNotNone(s) + self.assertEqual(s.properties['name'], 'exp') + self.assertEqual(s.properties['path'], 'src.c') + self.assertEqual(s.properties['src_start'], 9) + self.assertEqual(s.properties['src_end'], 13) + self.assertEqual(s.properties['fields'], [['i', 'int'], ['f', 'float'], ['data', 'char[]']]) add = g.get_function_by_name('add') - - expected_add = Function('src.c', 'add', '', 'int', '', 0, 7) - expected_add.add_argument('a', 'int') - expected_add.add_argument('b', 'int') - self.assertEqual(expected_add, add) - self.assertIn('a + b', add.src) + self.assertIsNotNone(add) + self.assertEqual(add.properties['name'], 'add') + self.assertEqual(add.properties['path'], 'src.c') + self.assertEqual(add.properties['ret_type'], 'int') + self.assertEqual(add.properties['src_start'], 0) + self.assertEqual(add.properties['src_end'], 7) + self.assertEqual(add.properties['args'], [['a', 'int'], ['b', 'int']]) + self.assertIn('a + b', add.properties['src']) main = g.get_function_by_name('main') - - expected_main = Function('src.c', 'main', '', 'int', '', 15, 18) - expected_main.add_argument('argv', 'const char**') - expected_main.add_argument('argc', 'int') - self.assertEqual(expected_main, main) - self.assertIn('x = add', main.src) + self.assertIsNotNone(main) + self.assertEqual(main.properties['name'], 'main') + self.assertEqual(main.properties['path'], 'src.c') + self.assertEqual(main.properties['ret_type'], 'int') + self.assertEqual(main.properties['src_start'], 15) + self.assertEqual(main.properties['src_end'], 18) + self.assertEqual(main.properties['args'], [['argv', 'const char**'], ['argc', 'int']]) + self.assertIn('x = add', main.properties['src']) callees = g.function_calls(main.id) self.assertEqual(len(callees), 1) self.assertEqual(callees[0], add) callers = g.function_called_by(add.id) - callers = [caller.name for caller in callers] + callers = [caller.properties['name'] for caller in callers] self.assertEqual(len(callers), 2) self.assertIn('add', callers) diff --git a/tests/test_git_history.py b/tests/test_git_history.py index 1dd5bb28..91d744fe 100644 --- a/tests/test_git_history.py +++ b/tests/test_git_history.py @@ -1,8 +1,7 @@ import os import unittest -from git import Repo +import pygit2 from api import ( - Graph, Project, switch_commit ) @@ -30,8 +29,8 @@ def setUpClass(cls): repo_dir = os.path.join(current_dir, 'git_repo') # Checkout HEAD commit - repo = Repo(repo_dir) - repo.git.checkout("HEAD") + repo = pygit2.Repository(repo_dir) + repo.checkout_head() proj = Project.from_local_repository(repo_dir) graph = proj.analyze_sources() @@ -45,13 +44,13 @@ def assert_file_exists(self, path: str, name: str, ext: str) -> None: f = graph.get_file(path, name, ext) self.assertIsNotNone(f) - self.assertEqual(f.ext, ext) - self.assertEqual(f.path, path) - self.assertEqual(f.name, name) + self.assertEqual(f.properties['ext'], ext) + self.assertEqual(f.properties['path'], path) + self.assertEqual(f.properties['name'], name) def test_git_graph_structure(self): # validate git graph structure - c = repo.commit("HEAD") + c = repo.revparse_single("HEAD") while True: commits = git_graph.get_commits([c.short_id]) @@ -62,13 +61,13 @@ def test_git_graph_structure(self): self.assertEqual(c.short_id, actual['hash']) self.assertEqual(c.message, actual['message']) self.assertEqual(c.author.name, actual['author']) - self.assertEqual(c.committed_date, actual['date']) + self.assertEqual(c.commit_time, actual['date']) # Advance to previous commit - if len(c.parents) == 0: + if len(c.parent_ids) == 0: break - c = c.parents[0] + c = repo.get(c.parent_ids[0]) def test_git_transitions(self): # our test git repo: diff --git a/tests/test_graph_ops.py b/tests/test_graph_ops.py index 83278469..aa137832 100644 --- a/tests/test_graph_ops.py +++ b/tests/test_graph_ops.py @@ -1,7 +1,8 @@ import unittest +from pathlib import Path from falkordb import FalkorDB -from typing import List, Optional -from api import * +from api import Graph +from api.entities import File class TestGraphOps(unittest.TestCase): @@ -11,50 +12,60 @@ def setUp(self): self.graph = Graph(name='test') def test_add_function(self): - # Create function - func = Function('/path/to/function', 'func', '', 'int', '', 1, 10) - func.add_argument('x', 'int') - func.add_argument('y', 'float') - - self.graph.add_function(func) - self.assertEqual(func, self.graph.get_function(func.id)) + func_id = self.graph.add_entity( + 'Function', 'func', '', '/path/to/function', 1, 10, + {'ret_type': 'int', 'src': '', 'args': [['x', 'int'], ['y', 'float']]} + ) + result = self.graph.get_function(func_id) + self.assertIsNotNone(result) + self.assertEqual(result.properties['name'], 'func') + self.assertEqual(result.properties['ret_type'], 'int') + self.assertEqual(result.properties['args'], [['x', 'int'], ['y', 'float']]) def test_add_file(self): - file = File('/path/to/file', 'file', 'txt') - + file = File(Path('/path/to/file.txt'), None) self.graph.add_file(file) - self.assertEqual(file, self.graph.get_file('/path/to/file', 'file', 'txt')) + result = self.graph.get_file('/path/to/file.txt', 'file.txt', '.txt') + self.assertIsNotNone(result) + self.assertEqual(result.properties['name'], 'file.txt') + self.assertEqual(result.properties['ext'], '.txt') def test_file_add_function(self): - file = File('/path/to/file', 'file', 'txt') - func = Function('/path/to/function', 'func', '', 'int', '', 1, 10) - + file = File(Path('/path/to/file.txt'), None) self.graph.add_file(file) - self.graph.add_function(func) - self.graph.connect_entities("CONTAINS", file.id, func.id) + func_id = self.graph.add_entity( + 'Function', 'func', '', '/path/to/function', 1, 10, + {'ret_type': 'int', 'src': '', 'args': []} + ) + + self.graph.connect_entities("CONTAINS", file.id, func_id) query = """MATCH (file:File)-[:CONTAINS]->(func:Function) WHERE ID(func) = $func_id AND ID(file) = $file_id RETURN true""" - params = {'file_id': file.id, 'func_id': func.id} + params = {'file_id': file.id, 'func_id': func_id} res = self.g.query(query, params).result_set self.assertTrue(res[0][0]) def test_function_calls_function(self): - caller = Function('/path/to/function', 'func_A', '', 'int', '', 1, 10) - callee = Function('/path/to/function', 'func_B', '', 'int', '', 11, 21) + caller_id = self.graph.add_entity( + 'Function', 'func_A', '', '/path/to/function', 1, 10, + {'ret_type': 'int', 'src': '', 'args': []} + ) + callee_id = self.graph.add_entity( + 'Function', 'func_B', '', '/path/to/function', 11, 21, + {'ret_type': 'int', 'src': '', 'args': []} + ) - self.graph.add_function(caller) - self.graph.add_function(callee) - self.graph.function_calls_function(caller.id, callee.id, 10) + self.graph.function_calls_function(caller_id, callee_id, 10) query = """MATCH (caller:Function)-[:CALLS]->(callee:Function) WHERE ID(caller) = $caller_id AND ID(callee) = $callee_id RETURN true""" - params = {'caller_id': caller.id, 'callee_id': callee.id} + params = {'caller_id': caller_id, 'callee_id': callee_id} res = self.g.query(query, params).result_set self.assertTrue(res[0][0]) diff --git a/tests/test_py_analyzer.py b/tests/test_py_analyzer.py index f0040a0d..dd199c02 100644 --- a/tests/test_py_analyzer.py +++ b/tests/test_py_analyzer.py @@ -2,7 +2,8 @@ import unittest from pathlib import Path -from api import SourceAnalyzer, File, Class, Function, Graph +from api import SourceAnalyzer, Graph + class Test_PY_Analyzer(unittest.TestCase): def test_analyzer(self): @@ -15,7 +16,7 @@ def test_analyzer(self): # Get the directory of the current file current_dir = os.path.dirname(current_file_path) - # Append 'source_files/c' to the current directory + # Append 'source_files/py' to the current directory path = os.path.join(current_dir, 'source_files') path = os.path.join(path, 'py') path = str(path) @@ -24,29 +25,42 @@ def test_analyzer(self): analyzer.analyze_local_folder(path, g) f = g.get_file('', 'src.py', '.py') - self.assertEqual(File('', 'src.py', '.py'), f) + self.assertIsNotNone(f) + self.assertEqual(f.properties['name'], 'src.py') + self.assertEqual(f.properties['ext'], '.py') log = g.get_function_by_name('log') - expected_log = Function('src.py', 'log', None, 'None', '', 0, 1) - expected_log.add_argument('msg', 'str') - self.assertEqual(expected_log, log) + self.assertIsNotNone(log) + self.assertEqual(log.properties['name'], 'log') + self.assertEqual(log.properties['path'], 'src.py') + self.assertEqual(log.properties['ret_type'], 'None') + self.assertEqual(log.properties['src_start'], 0) + self.assertEqual(log.properties['src_end'], 1) + self.assertEqual(log.properties['args'], [['msg', 'str']]) abort = g.get_function_by_name('abort') - expected_abort = Function('src.py', 'abort', None, 'Task', '', 9, 11) - expected_abort.add_argument('self', 'Unknown') - expected_abort.add_argument('delay', 'float') - self.assertEqual(expected_abort, abort) + self.assertIsNotNone(abort) + self.assertEqual(abort.properties['name'], 'abort') + self.assertEqual(abort.properties['path'], 'src.py') + self.assertEqual(abort.properties['ret_type'], 'Task') + self.assertEqual(abort.properties['src_start'], 9) + self.assertEqual(abort.properties['src_end'], 11) + self.assertEqual(abort.properties['args'], [['self', 'Unknown'], ['delay', 'float']]) init = g.get_function_by_name('__init__') - expected_init = Function('src.py', '__init__', None, None, '', 4, 7) - expected_init.add_argument('self', 'Unknown') - expected_init.add_argument('name', 'str') - expected_init.add_argument('duration', 'int') - self.assertEqual(expected_init, init) + self.assertIsNotNone(init) + self.assertEqual(init.properties['name'], '__init__') + self.assertEqual(init.properties['path'], 'src.py') + self.assertEqual(init.properties['src_start'], 4) + self.assertEqual(init.properties['src_end'], 7) + self.assertEqual(init.properties['args'], [['self', 'Unknown'], ['name', 'str'], ['duration', 'int']]) task = g.get_class_by_name('Task') - expected_task = Class('src.py', 'Task', None, 3, 11) - self.assertEqual(expected_task, task) + self.assertIsNotNone(task) + self.assertEqual(task.properties['name'], 'Task') + self.assertEqual(task.properties['path'], 'src.py') + self.assertEqual(task.properties['src_start'], 3) + self.assertEqual(task.properties['src_end'], 11) callees = g.function_calls(abort.id) self.assertEqual(len(callees), 1) @@ -54,7 +68,7 @@ def test_analyzer(self): print_func = g.get_function_by_name('print') callers = g.function_called_by(print_func.id) - callers = [caller.name for caller in callers] + callers = [caller.properties['name'] for caller in callers] self.assertIn('__init__', callers) self.assertIn('log', callers) From dafc54921c8e2e688f79b41b909c8ef4d4a5b16d Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Tue, 24 Mar 2026 23:25:23 +0200 Subject: [PATCH 8/9] fix(combobox): add error handling and fix placeholder selection - Add catch block for network errors with toast notification - Disable Select during loading and when no options available - Use placeholder prop instead of selectable placeholder items - Move lastFetch timestamp update to after successful fetch Addresses CodeRabbit review comments on combobox.tsx. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/src/components/combobox.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/components/combobox.tsx b/app/src/components/combobox.tsx index 418de2a7..dbf751ee 100644 --- a/app/src/components/combobox.tsx +++ b/app/src/components/combobox.tsx @@ -43,6 +43,13 @@ export default function Combobox({ options, setOptions, selectedValue, onSelecte const json = await result.json() setOptions(json.repositories) + setLastFetch(Date.now()) + } catch (error) { + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: error instanceof Error ? error.message : "Failed to fetch repositories.", + }) } finally { setIsFetchingOptions(false) } @@ -60,16 +67,14 @@ export default function Combobox({ options, setOptions, selectedValue, onSelecte //check if last fetch was less than 30 seconds ago if (lastFetch && now - lastFetch < 30000) return; - - setLastFetch(now); - + fetchOptions() }, [open]) return ( - - + { From e3fcf8efed8380b9e60172fe777f862ea863da19 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Tue, 24 Mar 2026 23:25:32 +0200 Subject: [PATCH 9/9] refactor(code-graph): remove unused options/setOptions props CodeGraph now manages options locally instead of receiving them from App. Remove the dead props from the interface and stop passing them from App.tsx. The Combobox inside CodeGraph fetches options on its own. Addresses typo-app review comment about local state shadowing props. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/src/App.tsx | 6 ------ app/src/components/code-graph.tsx | 2 -- 2 files changed, 8 deletions(-) diff --git a/app/src/App.tsx b/app/src/App.tsx index 081e54ef..17ab27db 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -71,7 +71,6 @@ export default function App() { const [createURL, setCreateURL] = useState("") const [createOpen, setCreateOpen] = useState(false) const [tipOpen, setTipOpen] = useState(false) - const [options, setOptions] = useState([]); const [path, setPath] = useState(); const [isSubmit, setIsSubmit] = useState(false); const desktopChartRef = useRef(null) @@ -141,7 +140,6 @@ export default function App() { const graphName = createURL.split('/').pop()! - setOptions(prev => [...prev, graphName]) setSelectedValue(graphName) setCreateURL("") setCreateOpen(false) @@ -528,8 +526,6 @@ export default function App() { data={data} setData={setData} canvasRef={desktopChartRef} - options={options} - setOptions={setOptions} onFetchGraph={onFetchGraph} isFetchingGraph={isFetchingGraph} onFetchNode={onFetchNode} @@ -656,8 +652,6 @@ export default function App() { data={data} setData={setData} canvasRef={mobileChartRef} - options={options} - setOptions={setOptions} onFetchGraph={onFetchGraph} isFetchingGraph={isFetchingGraph} onFetchNode={onFetchNode} diff --git a/app/src/components/code-graph.tsx b/app/src/components/code-graph.tsx index d4309d61..e2b9a911 100644 --- a/app/src/components/code-graph.tsx +++ b/app/src/components/code-graph.tsx @@ -30,8 +30,6 @@ interface Props { onFetchGraph: (graphName: string) => Promise, isFetchingGraph: boolean, onFetchNode: (nodeIds: number[]) => Promise, - options: string[] - setOptions: Dispatch> isShowPath: boolean setPath: Dispatch> canvasRef: GraphRef