Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions src/components/EditorView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export default function EditorView() {
const editorContentRef = useRef(editorContent);
const currentFileRef = useRef(currentFile);
const unsavedChangesRef = useRef(unsavedChanges);
const loadFileRef = useRef(null);
const suppressReloadForPathRef = useRef({});

useEffect(() => {
Expand Down Expand Up @@ -323,7 +324,7 @@ export default function EditorView() {
if (targetFile) {
const shouldLoad = targetFile !== currentFileRef.current || editorContentRef.current === "";
if (shouldLoad) {
loadFile(targetFile);
loadFileRef.current?.(targetFile);
}
} else {
setCurrentFile(null);
Expand Down Expand Up @@ -361,7 +362,7 @@ export default function EditorView() {
toast.info("File changed on disk. Save or reload to see changes.");
return;
}
loadFile(current);
loadFileRef.current?.(current);
});
return () => unsub?.();
}, [projectPath]);
Expand Down Expand Up @@ -429,6 +430,7 @@ export default function EditorView() {
setIsFileLoading(false);
}
};
loadFileRef.current = loadFile;

const handleFileSelect = async (node) => {
if (node.type === "file") {
Expand All @@ -448,14 +450,14 @@ export default function EditorView() {
[setExplorerExpanded]
);

const handleSelectTab = (tabId) => {
const handleSelectTab = useCallback((tabId) => {
const tab = openTabs.find((t) => t.id === tabId);
if (!tab) return;
setActiveTabId(tabId);
if (tab.path !== currentFileRef.current) {
loadFile(tab.path);
loadFileRef.current?.(tab.path);
}
};
}, [openTabs]);

const performCloseTab = useCallback(
(tabId) => {
Expand All @@ -477,7 +479,7 @@ export default function EditorView() {
setActiveTabId(replacement ? replacement.id : null);
if (replacement) {
if (replacement.path !== currentFileRef.current) {
loadFile(replacement.path);
loadFileRef.current?.(replacement.path);
}
} else {
setCurrentFile(null);
Expand All @@ -489,14 +491,14 @@ export default function EditorView() {
[openTabs, activeTabId, setUnsavedChanges]
);

const handleCloseTab = (tabId) => {
const handleCloseTab = useCallback((tabId) => {
const tab = openTabs.find((t) => t.id === tabId);
if (tab && unsavedChangesRef.current[tab.path] !== undefined) {
setPendingCloseTabId(tabId);
return;
}
performCloseTab(tabId);
};
}, [openTabs, performCloseTab]);

const handleConfirmCloseSave = useCallback(async () => {
if (pendingCloseTabId == null) return;
Expand Down Expand Up @@ -639,7 +641,7 @@ export default function EditorView() {

window.addEventListener("keydown", onKeyDown, { capture: true });
return () => window.removeEventListener("keydown", onKeyDown, { capture: true });
}, [activeTabId, openTabs, handleCloseTab]);
}, [activeTabId, openTabs, handleCloseTab, handleSelectTab]);

if (!project) return null;

Expand Down
8 changes: 4 additions & 4 deletions src/components/GitPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from "react";
import React, { useCallback, useState, useEffect, useRef } from "react";
import {
GitBranch,
Upload,
Expand Down Expand Up @@ -75,7 +75,7 @@ export default function GitPanel({
const workingFiles =
status?.files?.filter((f) => !f.index || f.index === " " || f.workingDir === "U") || [];

const loadGitStatus = async () => {
const loadGitStatus = useCallback(async () => {
if (!projectPath) return;
setError(null);
setIsRefreshing(true);
Expand Down Expand Up @@ -105,13 +105,13 @@ export default function GitPanel({
} finally {
setIsRefreshing(false);
}
};
}, [onStatusChange, projectPath]);

useEffect(() => {
if (isOpen && projectPath) {
loadGitStatus();
}
}, [isOpen, projectPath]);
}, [isOpen, loadGitStatus, projectPath]);

const handleAddAll = async () => {
if (!projectPath) return;
Expand Down
4 changes: 2 additions & 2 deletions src/components/LogViewer.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useOutletContext } from "react-router-dom";
import {
Terminal as TerminalIcon,
Expand Down Expand Up @@ -33,7 +33,7 @@ export default function LogViewer(props) {
const status = context?.project?.status;
const onSendInput = context?.handleSendInput;
const allLogs = useAtomValue(logsAtom);
const logs = allLogs[projectId] || [];
const logs = useMemo(() => allLogs[projectId] || [], [allLogs, projectId]);
const [input, setInput] = useState("");
const [history, setHistory] = useState([]);
const [historyIndex, setHistoryIndex] = useState(-1);
Expand Down
2 changes: 1 addition & 1 deletion src/components/OverviewView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ function useProjectLayout({ projectId, defaultLayout }) {

function ConsoleMiniTile({ projectId, status, onSendInput }) {
const allLogs = useAtomValue(logsAtom);
const logs = allLogs?.[projectId] || [];
const logs = useMemo(() => allLogs?.[projectId] || [], [allLogs, projectId]);

const terminalContainerRef = useRef(null);
const xtermRef = useRef(null);
Expand Down
40 changes: 21 additions & 19 deletions src/components/ProjectLayout.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react";
import { useCallback, useEffect, useRef } from "react";
import { useNavigate, Outlet } from "react-router-dom";
import { useAtom, useSetAtom } from "jotai";
import { toast } from "react-toastify";
Expand Down Expand Up @@ -27,6 +27,8 @@ export default function ProjectLayout() {
const setTunnelState = useSetAtom(atoms.tunnelStateAtom);
const projectPathRef = useRef(null);
projectPathRef.current = project?.path ?? null;
const projectId = project?.id;
const projectPath = project?.path;

// Redirect if project ID in URL doesn't exist (e.g. deleted)
useEffect(() => {
Expand All @@ -35,60 +37,60 @@ export default function ProjectLayout() {
}
}, [projects.length, project, navigate]);

const loadData = async () => {
const loadData = useCallback(async () => {
const [projectList, categoryList] = await Promise.all([API.getProjects(), API.getCategories()]);
setProjects(normalizeProjectList(projectList));
setCategories(normalizeCategoryList(categoryList));
};
}, [setCategories, setProjects]);

const loadFileTree = async (path) => {
const loadFileTree = useCallback(async (path) => {
setIsFileTreeLoading(true);
const tree = await API.readDirectory(path);
setFileTree(tree);
setIsFileTreeLoading(false);
};
}, [setFileTree, setIsFileTreeLoading]);

// Per-project: file tree, watcher, log history, tunnel state
useEffect(() => {
if (!project) return;
if (!projectId) return;
let cancelled = false;
setStats(null);

(async () => {
const list = normalizeProjectList(await API.getProjects());
if (cancelled) return;
setProjects(list);
const currentProject = list.find((p) => p.id === project.id);
const currentProject = list.find((p) => p.id === projectId);
if (currentProject) {
loadFileTree(currentProject.path);
API.watchFolder(currentProject.path);
}
})();

API.getLogHistory(project.id).then((history) => {
API.getLogHistory(projectId).then((history) => {
if (history?.length > 0) {
setLogs((prev) => ({
...prev,
[project.id]: history,
[projectId]: history,
}));
}
});

API.getTunnelStatus(project.id).then((status) => {
API.getTunnelStatus(projectId).then((status) => {
if (status) {
setTunnelState((prev) => ({
...prev,
[project.id]: { ...(prev[project.id] || { logs: [] }), ...status },
[projectId]: { ...(prev[projectId] || { logs: [] }), ...status },
}));
}
});

API.getTunnelLogs(project.id).then((logs) => {
API.getTunnelLogs(projectId).then((logs) => {
if (logs?.length > 0) {
setTunnelState((prev) => ({
...prev,
[project.id]: {
...(prev[project.id] || { status: "stopped", url: null }),
[projectId]: {
...(prev[projectId] || { status: "stopped", url: null }),
logs,
},
}));
Expand All @@ -97,15 +99,15 @@ export default function ProjectLayout() {

return () => {
cancelled = true;
if (project?.path) {
API.stopWatchingFolder(project.path).catch(() => {});
if (projectPath) {
API.stopWatchingFolder(projectPath).catch(() => {});
}
};
}, [project?.id]);
}, [loadFileTree, projectId, projectPath, setLogs, setProjects, setStats, setTunnelState]);

// File change listener (reload tree when structure changes under this project)
useEffect(() => {
if (!project?.path) return;
if (!projectPath) return;
let fileChangeDebounceTimer = null;
const cleanupFileChange = API.onFileChange(({ event, filePath }) => {
const projectPath = projectPathRef.current;
Expand All @@ -127,7 +129,7 @@ export default function ProjectLayout() {
if (fileChangeDebounceTimer) clearTimeout(fileChangeDebounceTimer);
cleanupFileChange();
};
}, [project?.path]);
}, [loadFileTree, projectPath]);

// Subscribe to native push-based stats events (no polling)
useEffect(() => {
Expand Down
39 changes: 27 additions & 12 deletions src/components/TunnelView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ export default function TunnelView(props) {
const projectTunnelState = selectedProject
? tunnelState[selectedProject.id] || { status: "stopped", url: null, logs: [] }
: { status: "stopped", url: null, logs: [] };
const selectedProjectId = selectedProject?.id;
const selectedTunnelMode = selectedProject?.tunnelMode;
const selectedTunnelPort = selectedProject?.tunnelPort;
const selectedTunnelToken = selectedProject?.encryptedTunnelToken;
const selectedAutoStartTunnel = selectedProject?.autoStartTunnel;
const selectedTunnelConfig = selectedProject?.tunnelConfig;
const currentTunnelStatusRef = useRef(projectTunnelState.status);
currentTunnelStatusRef.current = projectTunnelState.status;

const [mode, setMode] = useState(selectedProject?.tunnelMode || "quick");
const [port, setPort] = useState(
Expand All @@ -78,22 +86,29 @@ export default function TunnelView(props) {

// Reset state on project change
useEffect(() => {
if (!selectedProject) return;
setMode(selectedProject.tunnelMode || "quick");
setPort(selectedProject?.tunnelPort != null ? String(selectedProject.tunnelPort) : "3000");
setToken(selectedProject.encryptedTunnelToken || "");
if (!selectedProjectId) return;
setMode(selectedTunnelMode || "quick");
setPort(selectedTunnelPort != null ? String(selectedTunnelPort) : "3000");
setToken(selectedTunnelToken || "");
setShowAdvanced(false);
setShowHelp(false);
setAutoStart(selectedProject.autoStartTunnel || false);
setAutoStart(selectedAutoStartTunnel || false);
setConfig({
protocol: selectedProject.tunnelConfig?.protocol || "http2",
loglevel: selectedProject.tunnelConfig?.loglevel || "info",
noTLSVerify: selectedProject.tunnelConfig?.noTLSVerify || false,
connectTimeout: selectedProject.tunnelConfig?.connectTimeout || "30s",
httpHostHeader: selectedProject.tunnelConfig?.httpHostHeader || "",
protocol: selectedTunnelConfig?.protocol || "http2",
loglevel: selectedTunnelConfig?.loglevel || "info",
noTLSVerify: selectedTunnelConfig?.noTLSVerify || false,
connectTimeout: selectedTunnelConfig?.connectTimeout || "30s",
httpHostHeader: selectedTunnelConfig?.httpHostHeader || "",
});
prevStatusRef.current = projectTunnelState.status;
}, [selectedProject?.id]);
prevStatusRef.current = currentTunnelStatusRef.current;
}, [
selectedProjectId,
selectedTunnelMode,
selectedTunnelPort,
selectedTunnelToken,
selectedAutoStartTunnel,
selectedTunnelConfig,
]);
useEffect(() => {
if (prevStatusRef.current !== "running" && projectTunnelState.status === "running") {
toast.success("Tunnel established successfully!");
Expand Down
4 changes: 2 additions & 2 deletions src/components/overview/tiles/ConsoleMiniTile.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useAtomValue } from "jotai";
import { Terminal as TerminalIcon, Send } from "lucide-react";
import { toast } from "react-toastify";
Expand All @@ -14,7 +14,7 @@ const API = window.api;

export default function ConsoleMiniTile({ projectId, status, onSendInput }) {
const allLogs = useAtomValue(logsAtom);
const logs = allLogs?.[projectId] || [];
const logs = useMemo(() => allLogs?.[projectId] || [], [allLogs, projectId]);

const terminalContainerRef = useRef(null);
const xtermRef = useRef(null);
Expand Down
8 changes: 4 additions & 4 deletions src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react";
import { useCallback, useEffect, useRef } from "react";
import { Outlet } from "react-router-dom";
import { useAtom, useSetAtom } from "jotai";
import * as atoms from "@/store/atoms";
Expand All @@ -16,11 +16,11 @@ export default function Dashboard() {
const setTunnelState = useSetAtom(atoms.tunnelStateAtom);
const setResourceHistory = useSetAtom(atoms.resourceHistoryAtom);

const loadData = async () => {
const loadData = useCallback(async () => {
const [projectList, categoryList] = await Promise.all([API.getProjects(), API.getCategories()]);
setProjects(normalizeProjectList(projectList));
setCategories(normalizeCategoryList(categoryList));
};
}, [setCategories, setProjects]);

useEffect(() => {
loadData();
Expand Down Expand Up @@ -124,7 +124,7 @@ export default function Dashboard() {
cleanupTunnelStatus();
cleanupTunnelLog();
};
}, []);
}, [loadData, setLogs, setProjects, setResourceHistory, setTunnelState]);

return (
<div className="flex h-screen bg-background text-foreground overflow-hidden font-sans">
Expand Down
Loading