diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bd027ef..1a7464b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **`react-hooks/set-state-in-effect` cascading renders across 12 Explorer workspace files** (#769, #796) by @Sameer6305 and @KaifAhmad1 + - Replaced synchronous `setState` calls inside `useEffect` bodies with React's recommended "adjust state during render" pattern (`if (x !== prevX) { setPrevX(x); ...setState... }`) across `OntologyWorkspace`, `ManageWorkspace`, `LineageWorkspace`, and `GraphWorkspace`, and inlined async data-fetching effects with `ignore` flags to prevent race conditions and stale writes after unmount + - Fixed a regression the inlining itself introduced: `AlignmentsTab.tsx`, `KGOverviewTab.tsx`, `OntologyManager.tsx`, and `VersionsTab.tsx` each duplicated their existing fetch callback (`reload` / `fetchOverview` / `fetchRegistry` / `loadVersions`+`loadProposals`) into a second, inline copy for the mount effect, and the copy silently dropped the `setError`/`flashMsg` calls the original had — re-introducing, on the very first page load, the exact error-swallowing behavior that #767/#790 had already fixed for these same files. The inline copies now mirror the original's error handling (including `207` partial-success messages) exactly + - Fixed `LineageDiagram.tsx` only clearing the previously-rendered nodes/edges when the new `activeId` was falsy instead of on every id change, so switching directly between two lineage views briefly kept showing the *previous* view's stale diagram instead of clearing before the new fetch resolved + - `GraphWorkspace.tsx` and `GraphLoadingOverlay.tsx` still have unrelated `react-hooks/set-state-in-effect` violations outside this PR's 12-file scope (confirmed via `npx eslint .`); left as follow-up work rather than expanding this PR further + - **Checkov flagged the knowledge-explorer Helm chart for using the default Kubernetes namespace** ([code scanning alert #779](https://github.com/semantica-agi/semantica/security/code-scanning/779), [#778](https://github.com/semantica-agi/semantica/security/code-scanning/778), [#777](https://github.com/semantica-agi/semantica/security/code-scanning/777), `CKV_K8S_21`) by @KaifAhmad1 - `templates/service.yaml`, `templates/deployment.yaml`, and `templates/configmap.yaml` all already set `metadata.namespace` to `{{ .Release.Namespace }}`, which is only bound at `helm install`/`helm template` time; Checkov's helm framework renders the chart without a namespace override, so it always resolves to `default` and trips `CKV_K8S_21` even though the chart is namespace-agnostic by design - Added a `checkov.io/skip1: CKV_K8S_21` metadata annotation to each of the three files to suppress the scanner artifact false-positive properly in Helm templates, and documented the reasoning in `.checkov.yaml` diff --git a/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx b/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx index 7f10e40b..b83fd3fe 100644 --- a/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx +++ b/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx @@ -55,11 +55,9 @@ export function LineageDiagram() { const [prevActiveId, setPrevActiveId] = useState(activeId); if (activeId !== prevActiveId) { setPrevActiveId(activeId); - if (!activeId) { - setError(""); - setNodes([]); - setEdges([]); - } + setError(""); + setNodes([]); + setEdges([]); } useEffect(() => { diff --git a/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx b/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx index b7b248e0..f5dd69b2 100644 --- a/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx +++ b/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx @@ -134,36 +134,45 @@ export function KGOverviewTab() { fetch("/api/graph/nodes?limit=500"), ]); + if (!statsRes.ok) throw new Error(`Stats fetch failed (${statsRes.status})`); + if (!nodesRes.ok) throw new Error(`Nodes fetch failed (${nodesRes.status})`); + + const statsData: KGStats = await statsRes.json(); if (!ignore) { - if (statsRes.ok) { - const statsData: KGStats = await statsRes.json(); - setStats(statsData); - } - - if (nodesRes.ok) { - const nodesData: NodeListResponse = await nodesRes.json(); - const nodes = nodesData.nodes ?? []; - setNodeTypeMap(buildTypeMap(nodes, "type")); - - const edgesRes = await fetch("/api/graph/edges?limit=2000"); - if (edgesRes.ok) { - const edgesData = await edgesRes.json(); - const edges: { source: string; target: string }[] = edgesData.edges ?? []; - const degreeMap: Record = {}; - for (const edge of edges) { - degreeMap[edge.source] = (degreeMap[edge.source] ?? 0) + 1; - degreeMap[edge.target] = (degreeMap[edge.target] ?? 0) + 1; - } - const sorted = nodes - .map((n) => ({ node: n, neighborCount: degreeMap[n.id] ?? 0 })) - .sort((a, b) => b.neighborCount - a.neighborCount) - .slice(0, 10); - setTopNodes(sorted); - } + setStats(statsData); + if (statsRes.status === 207) { + setError((statsData as { message?: string }).message || "Warning: Partial success loading stats."); } } - } catch { - if (!ignore) setError("Failed to load graph overview. Ensure the server is running."); + + const nodesData: NodeListResponse = await nodesRes.json(); + const nodes = nodesData.nodes ?? []; + if (!ignore) { + setNodeTypeMap(buildTypeMap(nodes, "type")); + if (nodesRes.status === 207) { + const nodesMessage = (nodesData as { message?: string }).message || "Warning: Partial success loading nodes."; + setError((prev) => (prev ? `${prev} ${nodesMessage}` : nodesMessage)); + } + } + + // Simulate neighbor counts via edges fetch for top-N + const edgesRes = await fetch("/api/graph/edges?limit=2000"); + if (edgesRes.ok) { + const edgesData = await edgesRes.json(); + const edges: { source: string; target: string }[] = edgesData.edges ?? []; + const degreeMap: Record = {}; + for (const edge of edges) { + degreeMap[edge.source] = (degreeMap[edge.source] ?? 0) + 1; + degreeMap[edge.target] = (degreeMap[edge.target] ?? 0) + 1; + } + const sorted = nodes + .map((n) => ({ node: n, neighborCount: degreeMap[n.id] ?? 0 })) + .sort((a, b) => b.neighborCount - a.neighborCount) + .slice(0, 10); + if (!ignore) setTopNodes(sorted); + } + } catch (err) { + if (!ignore) setError(err instanceof Error ? err.message : "Failed to load graph overview. Ensure the server is running."); } finally { if (!ignore) setLoading(false); } diff --git a/explorer/src/workspaces/OntologyWorkspace/AlignmentsTab.tsx b/explorer/src/workspaces/OntologyWorkspace/AlignmentsTab.tsx index c8f368b5..ffcfebdc 100644 --- a/explorer/src/workspaces/OntologyWorkspace/AlignmentsTab.tsx +++ b/explorer/src/workspaces/OntologyWorkspace/AlignmentsTab.tsx @@ -77,17 +77,22 @@ export function AlignmentsTab() { loadOntologyRegistry(), loadAlignments(), ]); + if (ignore) return; - if (!ignore) { - if (registryResult.status === "fulfilled") { - setRegistry(registryResult.value); - setSourceOntology((current) => current || registryResult.value[0]?.uri || ""); - setTargetOntology((current) => current || registryResult.value[1]?.uri || registryResult.value[0]?.uri || ""); - } - if (alignmentResult.status === "fulfilled") { - setAlignments(alignmentResult.value); - } + const errors: string[] = []; + if (registryResult.status === "fulfilled") { + setRegistry(registryResult.value); + setSourceOntology((current) => current || registryResult.value[0]?.uri || ""); + setTargetOntology((current) => current || registryResult.value[1]?.uri || registryResult.value[0]?.uri || ""); + } else { + errors.push(registryResult.reason instanceof Error ? registryResult.reason.message : "Failed to load ontology registry."); } + if (alignmentResult.status === "fulfilled") { + setAlignments(alignmentResult.value); + } else { + errors.push(alignmentResult.reason instanceof Error ? alignmentResult.reason.message : "Failed to load alignments."); + } + if (errors.length) setError(errors.join(" ")); } void fetchInitial(); return () => { ignore = true; }; diff --git a/explorer/src/workspaces/OntologyWorkspace/OntologyManager.tsx b/explorer/src/workspaces/OntologyWorkspace/OntologyManager.tsx index ca80615c..ca90877b 100644 --- a/explorer/src/workspaces/OntologyWorkspace/OntologyManager.tsx +++ b/explorer/src/workspaces/OntologyWorkspace/OntologyManager.tsx @@ -286,21 +286,23 @@ export function OntologyManager() { const params = new URLSearchParams(); if (searchQ) params.set("q", searchQ); const res = await fetch(`/api/ontology/registry?${params}`); - if (res.ok) { - const data = await res.json(); - if (!ignore) setEntries(data); - } else { - if (!ignore) setEntries([]); - } + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + if (ignore) return; + setEntries(data); + if (res.status === 207) flashMsg("err", data.message || "Warning: Partial success loading registry."); } catch { - if (!ignore) setEntries([]); + if (!ignore) { + setEntries([]); + flashMsg("err", "Failed to load ontology registry"); + } } finally { if (!ignore) setLoading(false); } } void fetchInitial(); return () => { ignore = true; }; - }, [searchQ]); + }, [searchQ, flashMsg]); const handleToggle = useCallback(async (uri: string) => { try { diff --git a/explorer/src/workspaces/OntologyWorkspace/VersionsTab.tsx b/explorer/src/workspaces/OntologyWorkspace/VersionsTab.tsx index 52f72f12..e0bfa7c4 100644 --- a/explorer/src/workspaces/OntologyWorkspace/VersionsTab.tsx +++ b/explorer/src/workspaces/OntologyWorkspace/VersionsTab.tsx @@ -85,27 +85,36 @@ export function VersionsTab() { useEffect(() => { let ignore = false; async function fetchInitial() { + if (!ignore) setError(""); try { - const [propRes] = await Promise.all([ - fetch("/api/ontology/proposals"), - ]); + const propRes = await fetch("/api/ontology/proposals"); if (propRes.ok) { const propData = await propRes.json(); - if (!ignore) setProposals(propData); + if (!ignore) { + setProposals(propData); + if (propRes.status === 207) setError(propData.message || "Warning: Partial success loading proposals."); + } + } else if (!ignore) { + setError(`Failed to load proposals (${propRes.status})`); } - } catch (e) { - console.error("Failed to load proposals", e); + } catch (err) { + if (!ignore) setError(err instanceof Error ? err.message : "Failed to load proposals."); } - + if (!ontologyUri) return; try { const verRes = await fetch(`/api/ontology/versions/${encodeURIComponent(ontologyUri)}`); if (verRes.ok) { const verData = await verRes.json(); - if (!ignore) setVersions(verData); + if (!ignore) { + setVersions(verData); + if (verRes.status === 207) setError(verData.message || "Warning: Partial success loading versions."); + } + } else if (!ignore) { + setError((prev) => prev || `Failed to load versions (${verRes.status})`); } - } catch (e) { - console.error("Failed to load versions", e); + } catch (err) { + if (!ignore) setError((prev) => prev || (err instanceof Error ? err.message : "Failed to load versions.")); } } void fetchInitial();