Skip to content

Commit 96a518e

Browse files
authored
Add log download modal (#968)
* Start adding log download modal * Remove unused var * Fix mypy issue * Address copilot review comments
1 parent 3390939 commit 96a518e

8 files changed

Lines changed: 448 additions & 63 deletions

File tree

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import {
2+
Button,
3+
Group,
4+
LoadingOverlay,
5+
Modal,
6+
Progress,
7+
ScrollArea,
8+
Text,
9+
} from "@mantine/core"
10+
import { useEffect, useState } from "react"
11+
import { useDispatch, useSelector } from "react-redux"
12+
import { showErrorNotification } from "../../helpers/notification"
13+
import {
14+
emitListLogFiles,
15+
emitReadFile,
16+
resetFiles,
17+
selectFiles,
18+
selectIsReadingFile,
19+
selectLoadingListFiles,
20+
selectLogPath,
21+
selectReadFileProgress,
22+
} from "../../redux/slices/ftpSlice"
23+
import { readableBytes } from "./utils"
24+
25+
export default function DownloadLogModal({ opened, onClose }) {
26+
const dispatch = useDispatch()
27+
const files = useSelector(selectFiles)
28+
const loadingListFiles = useSelector(selectLoadingListFiles)
29+
const isReadingFile = useSelector(selectIsReadingFile)
30+
const readFileProgress = useSelector(selectReadFileProgress)
31+
const logPath = useSelector(selectLogPath)
32+
33+
const [selectedLog, setSelectedLog] = useState(null)
34+
const [hasFetched, setHasFetched] = useState(false)
35+
36+
useEffect(() => {
37+
// Fetch log files when modal opens (only once per opening)
38+
if (opened && !hasFetched) {
39+
dispatch(emitListLogFiles())
40+
setHasFetched(true)
41+
}
42+
}, [opened, hasFetched, dispatch])
43+
44+
useEffect(() => {
45+
// Reset files and fetch state when modal closes
46+
if (!opened) {
47+
dispatch(resetFiles())
48+
setSelectedLog(null)
49+
setHasFetched(false)
50+
}
51+
}, [opened, dispatch])
52+
53+
async function handleLogClick(log) {
54+
// First, ask user where to save the file
55+
try {
56+
const options = {
57+
title: "Save log file",
58+
defaultPath: log.name,
59+
filters: [
60+
{ name: "Log Files", extensions: ["bin", "log"] },
61+
{ name: "All Files", extensions: ["*"] },
62+
],
63+
}
64+
65+
const result = await window.ipcRenderer.invoke(
66+
"app:get-save-file-path",
67+
options,
68+
)
69+
70+
if (!result.canceled && result.filePath) {
71+
// Now download the file with the save path
72+
setSelectedLog(log)
73+
dispatch(emitReadFile({ path: log.path, savePath: result.filePath }))
74+
}
75+
} catch (error) {
76+
showErrorNotification(`Error selecting save location: ${error.message}`)
77+
}
78+
}
79+
80+
function handleRefresh() {
81+
dispatch(resetFiles())
82+
setHasFetched(false)
83+
dispatch(emitListLogFiles())
84+
setHasFetched(true)
85+
}
86+
87+
return (
88+
<Modal
89+
opened={opened}
90+
onClose={onClose}
91+
title="Download Log from Drone"
92+
size="lg"
93+
centered
94+
closeOnEscape={!isReadingFile}
95+
closeOnClickOutside={!isReadingFile}
96+
withCloseButton={!isReadingFile}
97+
>
98+
<div className="flex flex-col gap-4">
99+
<div className="flex justify-between items-center">
100+
<div className="flex flex-col">
101+
<Text size="sm" c="dimmed">
102+
{files.length > 0
103+
? `Found ${files.length} log file${files.length !== 1 ? "s" : ""}`
104+
: loadingListFiles
105+
? "Searching for logs..."
106+
: "No log files found"}
107+
</Text>
108+
{logPath && files.length > 0 && (
109+
<Text size="xs" c="dimmed">
110+
Location: {logPath}
111+
</Text>
112+
)}
113+
</div>
114+
<Button
115+
size="xs"
116+
onClick={handleRefresh}
117+
disabled={loadingListFiles || isReadingFile}
118+
loading={loadingListFiles}
119+
>
120+
Refresh
121+
</Button>
122+
</div>
123+
124+
{isReadingFile && readFileProgress && (
125+
<div className="flex flex-col gap-2 p-3 bg-falcongrey-900/20 rounded">
126+
<div className="flex justify-between items-center">
127+
<Text size="sm">Downloading: {selectedLog?.name}</Text>
128+
</div>
129+
<Text size="xs" c="dimmed">
130+
{readFileProgress.bytes_downloaded.toLocaleString()} /{" "}
131+
{readFileProgress.total_bytes.toLocaleString()} bytes
132+
</Text>
133+
<Progress
134+
value={readFileProgress.percentage}
135+
size="lg"
136+
animated
137+
color="blue"
138+
/>
139+
<Text size="xs" c="dimmed" ta="center">
140+
{readFileProgress.percentage}% complete
141+
</Text>
142+
</div>
143+
)}
144+
145+
<div className="relative">
146+
<LoadingOverlay
147+
visible={loadingListFiles}
148+
zIndex={1000}
149+
overlayProps={{ blur: 2 }}
150+
/>
151+
152+
<ScrollArea.Autosize mah={400} offsetScrollbars>
153+
{files.length > 0 ? (
154+
<div className="flex flex-col gap-1">
155+
{files.map((log, idx) => (
156+
<div
157+
key={idx}
158+
className={`flex items-center gap-3 p-1 rounded cursor-pointer transition-colors ${
159+
selectedLog?.path === log.path
160+
? "bg-falcongrey-700/30"
161+
: "hover:bg-falcongrey-600"
162+
} ${isReadingFile ? "opacity-50 pointer-events-none" : ""}`}
163+
onClick={() => handleLogClick(log)}
164+
>
165+
<div className="flex-1">
166+
<Text size="sm">{log.name}</Text>
167+
<Group gap={8}>
168+
<Text size="xs" c="dimmed">
169+
{readableBytes(log.size_b)}
170+
</Text>
171+
</Group>
172+
</div>
173+
</div>
174+
))}
175+
</div>
176+
) : (
177+
!loadingListFiles && (
178+
<div className="text-center py-8 text-gray-400">
179+
<Text size="sm">No log files found</Text>
180+
</div>
181+
)
182+
)}
183+
</ScrollArea.Autosize>
184+
</div>
185+
</div>
186+
</Modal>
187+
)
188+
}

gcs/src/components/fla/SelectFlightLog.jsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,32 @@ import {
55
Progress,
66
ScrollArea,
77
} from "@mantine/core"
8+
import { useDisclosure } from "@mantine/hooks"
89
import moment from "moment"
910
import { useCallback, useEffect, useMemo, useState } from "react"
10-
import { useDispatch } from "react-redux"
11+
import { useDispatch, useSelector } from "react-redux"
1112
import {
1213
showErrorNotification,
1314
showSuccessNotification,
1415
} from "../../helpers/notification.js"
16+
import { selectConnectedToDrone } from "../../redux/slices/droneConnectionSlice.js"
1517
import { setFile } from "../../redux/slices/logAnalyserSlice.js"
18+
import DownloadLogModal from "./DownloadLogModal.jsx"
1619
import { readableBytes } from "./utils"
1720

1821
/**
1922
* Initial FLA screen for selecting or uploading a flight log file.
2023
*/
2124
export default function SelectFlightLog({ getLogSummary }) {
2225
const dispatch = useDispatch()
26+
const connected = useSelector(selectConnectedToDrone)
2327
const [recentFgcsLogs, setRecentFgcsLogs] = useState(null)
2428
const [loadingFile, setLoadingFile] = useState(false)
2529
const [loadingFileProgress, setLoadingFileProgress] = useState(0)
30+
const [
31+
downloadModalOpened,
32+
{ open: openDownloadModal, close: closeDownloadModal },
33+
] = useDisclosure(false)
2634

2735
async function getFgcsLogs() {
2836
setRecentFgcsLogs(await window.ipcRenderer.invoke("fla:get-recent-logs"))
@@ -133,6 +141,11 @@ export default function SelectFlightLog({ getLogSummary }) {
133141
<Button onClick={selectFile} loading={loadingFile}>
134142
Analyse a log
135143
</Button>
144+
{connected && (
145+
<Button onClick={openDownloadModal} color="blue" variant="filled">
146+
Download from Drone
147+
</Button>
148+
)}
136149
<Button
137150
disabled={logsExist}
138151
color="red"
@@ -172,6 +185,10 @@ export default function SelectFlightLog({ getLogSummary }) {
172185
color="green"
173186
/>
174187
)}
188+
<DownloadLogModal
189+
opened={downloadModalOpened}
190+
onClose={closeDownloadModal}
191+
/>
175192
</div>
176193
)
177194
}

gcs/src/redux/middleware/emitters.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
} from "../slices/droneConnectionSlice"
3838
import {
3939
emitListFiles,
40+
emitListLogFiles,
4041
emitReadFile,
4142
setIsReadingFile,
4243
setLoadingListFiles,
@@ -389,11 +390,19 @@ export function handleEmitters(socket, store, action) {
389390
store.dispatch(setLoadingListFiles(true))
390391
},
391392
},
393+
{
394+
emitter: emitListLogFiles,
395+
callback: () => {
396+
socket.socket.emit("list_log_files")
397+
store.dispatch(setLoadingListFiles(true))
398+
},
399+
},
392400
{
393401
emitter: emitReadFile,
394402
callback: () => {
395403
socket.socket.emit("read_file", {
396404
path: action.payload.path,
405+
save_path: action.payload.savePath,
397406
})
398407
store.dispatch(setIsReadingFile(true))
399408
store.dispatch(setReadFileProgress(null)) // Reset progress when starting new download

gcs/src/redux/middleware/socketMiddleware.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import {
8080
resetFiles,
8181
setIsReadingFile,
8282
setLoadingListFiles,
83+
setLogPath,
8384
setReadFileData,
8485
setReadFileProgress,
8586
} from "../slices/ftpSlice.js"
@@ -181,6 +182,7 @@ const ConfigSpecificSocketEvents = Object.freeze({
181182

182183
const FtpSpecificSocketEvents = Object.freeze({
183184
onListFilesResult: "list_files_result",
185+
onListLogFilesResult: "list_log_files_result",
184186
onReadFileResult: "read_file_result",
185187
onReadFileProgress: "read_file_progress",
186188
})
@@ -1099,6 +1101,29 @@ const socketMiddleware = (store) => {
10991101
}
11001102
})
11011103

1104+
socket.socket.on(
1105+
FtpSpecificSocketEvents.onListLogFilesResult,
1106+
(msg) => {
1107+
store.dispatch(setLoadingListFiles(false))
1108+
if (msg.success) {
1109+
const data = msg.data || {}
1110+
const files = data.files || []
1111+
const logPath = data.log_path || null
1112+
1113+
store.dispatch(addFiles(files))
1114+
store.dispatch(setLogPath(logPath))
1115+
1116+
if (files.length === 0) {
1117+
showErrorNotification(
1118+
msg.message || "No log files found on drone",
1119+
)
1120+
}
1121+
} else {
1122+
showErrorNotification(msg.message)
1123+
}
1124+
},
1125+
)
1126+
11021127
socket.socket.on(FtpSpecificSocketEvents.onReadFileResult, (msg) => {
11031128
if (msg.success) {
11041129
showSuccessNotification(msg.message)

0 commit comments

Comments
 (0)