From 49caa1f72f8fc7d460a4667b0b67b9e875ee8966 Mon Sep 17 00:00:00 2001 From: Kush Makkapati Date: Sun, 1 Feb 2026 15:51:29 +0000 Subject: [PATCH 1/4] Start adding log download modal --- gcs/src/components/fla/DownloadLogModal.jsx | 192 +++++++++++++++++++ gcs/src/components/fla/SelectFlightLog.jsx | 19 +- gcs/src/redux/middleware/emitters.js | 9 + gcs/src/redux/middleware/socketMiddleware.js | 26 +++ gcs/src/redux/slices/ftpSlice.js | 22 ++- radio/app/controllers/ftpController.py | 160 ++++++++++++++-- radio/app/endpoints/ftp.py | 49 +++-- radio/tests/test_ftp.py | 40 ++-- 8 files changed, 451 insertions(+), 66 deletions(-) create mode 100644 gcs/src/components/fla/DownloadLogModal.jsx diff --git a/gcs/src/components/fla/DownloadLogModal.jsx b/gcs/src/components/fla/DownloadLogModal.jsx new file mode 100644 index 000000000..460f249e7 --- /dev/null +++ b/gcs/src/components/fla/DownloadLogModal.jsx @@ -0,0 +1,192 @@ +import { + Button, + Group, + LoadingOverlay, + Modal, + Progress, + ScrollArea, + Text, +} from "@mantine/core" +import { useEffect, useMemo, useState } from "react" +import { useDispatch, useSelector } from "react-redux" +import { showErrorNotification } from "../../helpers/notification" +import { + emitListLogFiles, + emitReadFile, + resetFiles, + selectFiles, + selectIsReadingFile, + selectLoadingListFiles, + selectLogPath, + selectReadFileProgress, +} from "../../redux/slices/ftpSlice" +import { readableBytes } from "./utils" + +export default function DownloadLogModal({ opened, onClose }) { + const dispatch = useDispatch() + const files = useSelector(selectFiles) + const loadingListFiles = useSelector(selectLoadingListFiles) + const isReadingFile = useSelector(selectIsReadingFile) + const readFileProgress = useSelector(selectReadFileProgress) + const logPath = useSelector(selectLogPath) + + const [selectedLog, setSelectedLog] = useState(null) + const [hasFetched, setHasFetched] = useState(false) + + const logFiles = useMemo(() => { + return files + }, [files]) + + useEffect(() => { + // Fetch log files when modal opens (only once per opening) + if (opened && !hasFetched) { + dispatch(emitListLogFiles()) + setHasFetched(true) + } + }, [opened, hasFetched, dispatch]) + + useEffect(() => { + // Reset files and fetch state when modal closes + if (!opened) { + dispatch(resetFiles()) + setSelectedLog(null) + setHasFetched(false) + } + }, [opened, dispatch]) + + async function handleLogClick(log) { + // First, ask user where to save the file + try { + const options = { + title: "Save log file", + defaultPath: log.name, + filters: [ + { name: "Log Files", extensions: ["bin", "log"] }, + { name: "All Files", extensions: ["*"] }, + ], + } + + const result = await window.ipcRenderer.invoke( + "app:get-save-file-path", + options, + ) + + if (!result.canceled && result.filePath) { + // Now download the file with the save path + setSelectedLog(log) + dispatch(emitReadFile({ path: log.path, savePath: result.filePath })) + } + } catch (error) { + showErrorNotification(`Error selecting save location: ${error.message}`) + } + } + + function handleRefresh() { + dispatch(resetFiles()) + setHasFetched(false) + dispatch(emitListLogFiles()) + setHasFetched(true) + } + + return ( + +
+
+
+ + {logFiles.length > 0 + ? `Found ${logFiles.length} log file${logFiles.length !== 1 ? "s" : ""}` + : loadingListFiles + ? "Searching for logs..." + : "No log files found"} + + {logPath && logFiles.length > 0 && ( + + Location: {logPath} + + )} +
+ +
+ + {isReadingFile && readFileProgress && ( +
+
+ Downloading: {selectedLog?.name} +
+ + {readFileProgress.bytes_downloaded.toLocaleString()} /{" "} + {readFileProgress.total_bytes.toLocaleString()} bytes + + + + {readFileProgress.percentage}% complete + +
+ )} + +
+ + + + {logFiles.length > 0 ? ( +
+ {logFiles.map((log, idx) => ( +
handleLogClick(log)} + > +
+ {log.name} + + + {readableBytes(log.size_b)} + + +
+
+ ))} +
+ ) : ( + !loadingListFiles && ( +
+ No log files found +
+ ) + )} +
+
+
+
+ ) +} diff --git a/gcs/src/components/fla/SelectFlightLog.jsx b/gcs/src/components/fla/SelectFlightLog.jsx index 2b35458c6..45d853435 100644 --- a/gcs/src/components/fla/SelectFlightLog.jsx +++ b/gcs/src/components/fla/SelectFlightLog.jsx @@ -5,14 +5,17 @@ import { Progress, ScrollArea, } from "@mantine/core" +import { useDisclosure } from "@mantine/hooks" import moment from "moment" import { useCallback, useEffect, useMemo, useState } from "react" -import { useDispatch } from "react-redux" +import { useDispatch, useSelector } from "react-redux" import { showErrorNotification, showSuccessNotification, } from "../../helpers/notification.js" +import { selectConnectedToDrone } from "../../redux/slices/droneConnectionSlice.js" import { setFile } from "../../redux/slices/logAnalyserSlice.js" +import DownloadLogModal from "./DownloadLogModal.jsx" import { readableBytes } from "./utils" /** @@ -20,9 +23,14 @@ import { readableBytes } from "./utils" */ export default function SelectFlightLog({ getLogSummary }) { const dispatch = useDispatch() + const connected = useSelector(selectConnectedToDrone) const [recentFgcsLogs, setRecentFgcsLogs] = useState(null) const [loadingFile, setLoadingFile] = useState(false) const [loadingFileProgress, setLoadingFileProgress] = useState(0) + const [ + downloadModalOpened, + { open: openDownloadModal, close: closeDownloadModal }, + ] = useDisclosure(false) async function getFgcsLogs() { setRecentFgcsLogs(await window.ipcRenderer.invoke("fla:get-recent-logs")) @@ -133,6 +141,11 @@ export default function SelectFlightLog({ getLogSummary }) { + {connected && ( + + )}