feat(gui): Feedback quick fixes (#331)

* refactor(gui): Simplify FeedbackDialog component and enhance feedback submission process

- Consolidated state management for feedback input using a custom hook.
- Improved user interface for feedback submission by including clearer instructions
- Removed redundant code and improved overall component structure for better maintainability.

* refactor(gui): Enhance FeedbackDialog layout and add mail link

* feat(gui): Add error handling in feedback submission

* feat(docs): Add brand identity to docs

* feat(docs): Add Send Feedback page

* feat(tauri): build base for log export feature

* feat(tauri): update save_txt_files to use HashMap for file content

* feat(gui): Implement log export functionality

* fix(gui): adjust feedback dialog link to show docs page with instructions for mail feedback

* fix(gui): minor style adjustments to export logs button

* feat(gui, tauri): enhance log export functionality to include zip file naming

* fix(docs): clarify docs section about exporting logs

* feat(gui): initialize selected swap in SwapSelectDropDown with most recent swap

* fix(gui): parse logs correctly for saving to log file

* fix(gui): ensure to use the most recent swap info by using a specialized hook

* fmr

---------

Co-authored-by: Binarybaron <binarybaron@protonmail.com>
This commit is contained in:
b-enedict 2025-05-27 12:55:20 +02:00 committed by GitHub
parent 60d2ee9f7e
commit 854b14939e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1043 additions and 355 deletions

View file

@ -1,356 +1,247 @@
import {
Box,
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
FormControlLabel,
IconButton,
MenuItem,
Paper,
Select,
Switch,
TextField,
Tooltip,
Typography,
} from "@material-ui/core";
import { useSnackbar } from "notistack";
import { useEffect, useState } from "react";
import TruncatedText from "renderer/components/other/TruncatedText";
import { store } from "renderer/store/storeRenderer";
import { useActiveSwapInfo, useAppSelector } from "store/hooks";
import { logsToRawString, parseDateString } from "utils/parseUtils";
import { submitFeedbackViaHttp, AttachmentInput } from "../../../api";
import LoadingButton from "../../other/LoadingButton";
import { PiconeroAmount } from "../../other/Units";
import { getLogsOfSwap, redactLogs } from "renderer/rpc";
import logger from "utils/logger";
import { Label, Visibility } from "@material-ui/icons";
import CliLogsBox from "renderer/components/other/RenderedCliLog";
import { CliLog, parseCliLogString } from "models/cliModel";
import { addFeedbackId } from "store/features/conversationsSlice";
async function submitFeedback(body: string, swapId: string | null, swapLogs: string | null, daemonLogs: string | null) {
const attachments: AttachmentInput[] = [];
if (swapId !== null) {
const swapInfo = store.getState().rpc.state.swapInfos[swapId];
if (swapInfo) {
// Add swap info as an attachment
attachments.push({
key: `swap_info_${swapId}.json`,
content: JSON.stringify(swapInfo, null, 2), // Pretty print JSON
});
// Retrieve and add logs for the specific swap
try {
const logs = await getLogsOfSwap(swapId, false);
attachments.push({
key: `swap_logs_${swapId}.txt`,
content: logs.logs.map((l) => JSON.stringify(l)).join("\n"),
});
} catch (logError) {
logger.error(logError, "Failed to get logs for swap", { swapId });
// Optionally add an attachment indicating log retrieval failure
attachments.push({ key: `swap_logs_${swapId}.error`, content: "Failed to retrieve swap logs." });
}
} else {
logger.warn("Selected swap info not found in state", { swapId });
attachments.push({ key: `swap_info_${swapId}.error`, content: "Swap info not found." });
}
// Add swap logs as an attachment
if (swapLogs) {
attachments.push({
key: `swap_logs_${swapId}.txt`,
content: swapLogs,
});
}
}
// Handle daemon logs
if (daemonLogs !== null) {
attachments.push({
key: "daemon_logs.txt",
content: daemonLogs,
});
}
// Call the updated API function
const feedbackId = await submitFeedbackViaHttp(body, attachments);
// Dispatch only the ID
store.dispatch(addFeedbackId(feedbackId));
}
/*
* This component is a dialog that allows the user to submit feedback to the
* developers. The user can enter a message and optionally attach logs from a
* specific swap.
* selectedSwap = null means no swap is attached
*/
function SwapSelectDropDown({
selectedSwap,
setSelectedSwap,
}: {
selectedSwap: string | null;
setSelectedSwap: (swapId: string | null) => void;
}) {
const swaps = useAppSelector((state) =>
Object.values(state.rpc.state.swapInfos),
);
return (
<Select
value={selectedSwap ?? ""}
variant="outlined"
onChange={(e) => setSelectedSwap(e.target.value as string || null)}
style={{ width: "100%" }}
displayEmpty
>
<MenuItem value="">Do not attach a swap</MenuItem>
{swaps.map((swap) => (
<MenuItem value={swap.swap_id} key={swap.swap_id}>
Swap{" "}<TruncatedText>{swap.swap_id}</TruncatedText>{" "}from{" "}
{new Date(parseDateString(swap.start_date)).toDateString()} (
<PiconeroAmount amount={swap.xmr_amount} />)
</MenuItem>
))}
</Select>
);
}
const MAX_FEEDBACK_LENGTH = 4000;
Box,
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControlLabel,
IconButton,
Paper,
TextField,
Tooltip,
Typography,
} from '@material-ui/core'
import { ErrorOutline, Visibility } from '@material-ui/icons'
import ExternalLink from 'renderer/components/other/ExternalLink'
import SwapSelectDropDown from './SwapSelectDropDown'
import LogViewer from './LogViewer'
import { useFeedback, MAX_FEEDBACK_LENGTH } from './useFeedback'
import { useState } from 'react'
import PromiseInvokeButton from 'renderer/components/PromiseInvokeButton'
export default function FeedbackDialog({
open,
onClose,
open,
onClose,
}: {
open: boolean;
onClose: () => void;
open: boolean
onClose: () => void
}) {
const [pending, setPending] = useState(false);
const [bodyText, setBodyText] = useState("");
const currentSwapId = useActiveSwapInfo();
const [swapLogsEditorOpen, setSwapLogsEditorOpen] = useState(false)
const [daemonLogsEditorOpen, setDaemonLogsEditorOpen] = useState(false)
const { enqueueSnackbar } = useSnackbar();
const { input, setInputState, logs, error, clearState, submitFeedback } =
useFeedback()
const [selectedSwap, setSelectedSwap] = useState<
string | null
>(currentSwapId?.swap_id || null);
const [swapLogs, setSwapLogs] = useState<(string | CliLog)[] | null>(null);
const [attachDaemonLogs, setAttachDaemonLogs] = useState(true);
const [daemonLogs, setDaemonLogs] = useState<(string | CliLog)[] | null>(null);
useEffect(() => {
// Reset logs if no swap is selected
if (selectedSwap === null) {
setSwapLogs(null);
return;
const handleClose = () => {
clearState()
onClose()
}
// Fetch the logs from the rust backend and update the state
getLogsOfSwap(selectedSwap, false).then((response) => setSwapLogs(response.logs.map(parseCliLogString)))
}, [selectedSwap]);
const bodyTooLong = input.bodyText.length > MAX_FEEDBACK_LENGTH
useEffect(() => {
if (attachDaemonLogs === false) {
setDaemonLogs(null);
return;
}
setDaemonLogs(store.getState().rpc?.logs)
}, [attachDaemonLogs]);
// Whether to display the log editor
const [swapLogsEditorOpen, setSwapLogsEditorOpen] = useState(false);
const [daemonLogsEditorOpen, setDaemonLogsEditorOpen] = useState(false);
const bodyTooLong = bodyText.length > MAX_FEEDBACK_LENGTH;
const clearState = () => {
setBodyText("");
setAttachDaemonLogs(false);
setSelectedSwap(null);
}
const sendFeedback = async () => {
if (pending) {
return;
}
try {
setPending(true);
await submitFeedback(
bodyText,
selectedSwap,
logsToRawString(swapLogs ?? []),
logsToRawString(daemonLogs ?? [])
);
enqueueSnackbar("Feedback submitted successfully!", {
variant: "success",
});
clearState()
} catch (e) {
logger.error(`Failed to submit feedback: ${e}`);
enqueueSnackbar(`Failed to submit feedback (${e})`, {
variant: "error",
});
} finally {
setPending(false);
}
onClose();
}
const setSwapLogsRedacted = async (redact: boolean) => {
setSwapLogs((await getLogsOfSwap(selectedSwap, redact)).logs.map(parseCliLogString))
}
const setDaemonLogsRedacted = async (redact: boolean) => {
if (!redact)
return setDaemonLogs(store.getState().rpc?.logs)
const redactedLogs = await redactLogs(daemonLogs);
setDaemonLogs(redactedLogs)
}
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Submit Feedback</DialogTitle>
<DialogContent>
<ul>
<li>Got something to say? Drop us a message below. </li>
<li>If you had an issue with a specific swap, select it from the dropdown to attach the logs.
It will help us figure out what went wrong.
</li>
<li>We appreciate you taking the time to share your thoughts! Every message is read by a core developer!</li>
</ul>
<Box
style={{
display: "flex",
flexDirection: "column",
gap: "1rem",
}}
>
<TextField
variant="outlined"
value={bodyText}
onChange={(e) => setBodyText(e.target.value)}
label={
bodyTooLong
? `Text is too long (${bodyText.length}/${MAX_FEEDBACK_LENGTH})`
: "Message"
}
multiline
minRows={4}
maxRows={4}
fullWidth
error={bodyTooLong}
/>
<Box style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
gap: "1rem",
}}>
<SwapSelectDropDown
selectedSwap={selectedSwap}
setSelectedSwap={setSelectedSwap}
/>
<Tooltip title="View the logs">
<Box style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<IconButton onClick={() => setSwapLogsEditorOpen(true)} disabled={selectedSwap === null}>
<Visibility />
</IconButton>
</Box>
</Tooltip>
</Box>
<LogViewer open={swapLogsEditorOpen} setOpen={setSwapLogsEditorOpen} logs={swapLogs} redact={setSwapLogsRedacted} />
<Box style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
gap: "1rem",
}}>
<Paper variant="outlined" style={{ padding: "0.5rem", width: "100%" }} >
<FormControlLabel
control={
<Checkbox
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitle style={{ paddingBottom: '0.5rem' }}>
Submit Feedback
</DialogTitle>
<DialogContent>
<Box
style={{
display: 'flex',
flexDirection: 'column',
gap: '1.5rem',
}}
>
{error && (
<Box
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
gap: '0.5rem',
width: '100%',
backgroundColor: 'hsla(0, 45%, 17%, 1)',
padding: '0.5rem',
borderRadius: '0.5rem',
border: '1px solid hsla(0, 61%, 32%, 1)',
}}
>
<ErrorOutline style={{ color: 'hsla(0, 77%, 75%, 1)' }} />
<Typography style={{ color: 'hsla(0, 83%, 91%, 1)' }} noWrap>
{error}
</Typography>
</Box>
)}
<Box>
<Typography style={{ marginBottom: '0.5rem' }}>
Have a question or need assistance? Message us below
or{' '}
<ExternalLink href="https://docs.unstoppableswap.net/send_feedback#email-support">
email us
</ExternalLink>
!
</Typography>
<TextField
variant="outlined"
value={input.bodyText}
onChange={(e) =>
setInputState((prev) => ({
...prev,
bodyText: e.target.value,
}))
}
label={
bodyTooLong
? `Text is too long (${input.bodyText.length}/${MAX_FEEDBACK_LENGTH})`
: 'Message'
}
multiline
minRows={4}
maxRows={4}
fullWidth
error={bodyTooLong}
/>
</Box>
<Box>
<Typography style={{ marginBottom: '0.5rem' }}>
Attach logs with your feedback for better support.
</Typography>
<Box
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
gap: '1rem',
paddingBottom: '0.5rem',
}}
>
<SwapSelectDropDown
selectedSwap={input.selectedSwap}
setSelectedSwap={(swapId) =>
setInputState((prev) => ({
...prev,
selectedSwap: swapId,
}))
}
/>
<Tooltip title="View the logs">
<Box
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<IconButton
onClick={() =>
setSwapLogsEditorOpen(true)
}
disabled={input.selectedSwap === null}
>
<Visibility />
</IconButton>
</Box>
</Tooltip>
</Box>
<LogViewer
open={swapLogsEditorOpen}
setOpen={setSwapLogsEditorOpen}
logs={logs.swapLogs}
setIsRedacted={(redact) =>
setInputState((prev) => ({
...prev,
isSwapLogsRedacted: redact,
}))
}
isRedacted={input.isSwapLogsRedacted}
/>
<Box
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
gap: '1rem',
}}
>
<Paper
variant="outlined"
style={{ padding: '0.5rem', width: '100%' }}
>
<FormControlLabel
control={
<Checkbox
color="primary"
checked={input.attachDaemonLogs}
onChange={(e) =>
setInputState((prev) => ({
...prev,
attachDaemonLogs:
e.target.checked,
}))
}
/>
}
label="Attach logs from the current session"
/>
</Paper>
<Tooltip title="View the logs">
<Box
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<IconButton
onClick={() =>
setDaemonLogsEditorOpen(true)
}
disabled={
input.attachDaemonLogs === false
}
>
<Visibility />
</IconButton>
</Box>
</Tooltip>
</Box>
</Box>
<Typography
variant="caption"
color="textSecondary"
style={{ marginBottom: '0.5rem' }}
>
Your feedback will be answered in the app and can be
found in the Feedback tab
</Typography>
<LogViewer
open={daemonLogsEditorOpen}
setOpen={setDaemonLogsEditorOpen}
logs={logs.daemonLogs}
setIsRedacted={(redact) =>
setInputState((prev) => ({
...prev,
isDaemonLogsRedacted: redact,
}))
}
isRedacted={input.isDaemonLogsRedacted}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<PromiseInvokeButton
requiresContext={false}
color="primary"
checked={attachDaemonLogs}
onChange={(e) => setAttachDaemonLogs(e.target.checked)}
/>
}
label="Attach logs from the current session"
/>
</Paper>
<Tooltip title="View the logs">
<Box style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<IconButton onClick={() => setDaemonLogsEditorOpen(true)} disabled={attachDaemonLogs === false}>
<Visibility />
</IconButton>
</Box>
</Tooltip>
</Box>
<LogViewer open={daemonLogsEditorOpen} setOpen={setDaemonLogsEditorOpen} logs={daemonLogs} redact={setDaemonLogsRedacted} />
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => { clearState(); onClose() }}>Cancel</Button>
<LoadingButton
color="primary"
variant="contained"
onClick={sendFeedback}
loading={pending}
>
Submit
</LoadingButton>
</DialogActions>
</Dialog>
);
variant="contained"
onInvoke={submitFeedback}
onSuccess={handleClose}
>
Submit
</PromiseInvokeButton>
</DialogActions>
</Dialog>
)
}
function LogViewer(
{ open,
setOpen,
logs,
redact
}: {
open: boolean,
setOpen: (_: boolean) => void,
logs: (string | CliLog)[] | null,
redact: (_: boolean) => void
}) {
return (
<Dialog open={open} onClose={() => setOpen(false)} fullWidth>
<DialogContent>
<Box>
<DialogContentText>
<Box style={{ display: "flex", flexDirection: "row", alignItems: "center" }}>
<Typography>
These are the logs that would be attached to your feedback message and provided to us developers.
They help us narrow down the problem you encountered.
</Typography>
</Box>
</DialogContentText>
<CliLogsBox label="Logs" logs={logs} topRightButton={<Paper style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', paddingLeft: "0.5rem" }} variant="outlined">
Redact
<Switch color="primary" onChange={(_, checked: boolean) => redact(checked)} />
</Paper>} />
</Box>
</DialogContent>
<DialogActions>
<Button variant="contained" color="primary" onClick={() => setOpen(false)}>
Close
</Button>
</DialogActions>
</Dialog >
)
}

View file

@ -0,0 +1,66 @@
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
Paper,
Switch,
Typography,
} from "@material-ui/core";
import { CliLog } from "models/cliModel";
import CliLogsBox from "renderer/components/other/RenderedCliLog";
interface LogViewerProps {
open: boolean;
setOpen: (_: boolean) => void;
logs: (string | CliLog)[] | null;
setIsRedacted: (_: boolean) => void;
isRedacted: boolean;
}
export default function LogViewer({
open,
setOpen,
logs,
setIsRedacted,
isRedacted
}: LogViewerProps) {
return (
<Dialog open={open} onClose={() => setOpen(false)} fullWidth>
<DialogContent>
<Box>
<DialogContentText>
<Box style={{ display: "flex", flexDirection: "row", alignItems: "center" }}>
<Typography>
These are the logs that would be attached to your feedback message and provided to us developers.
They help us narrow down the problem you encountered.
</Typography>
</Box>
</DialogContentText>
<CliLogsBox
label="Logs"
logs={logs}
topRightButton={
<Paper style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', paddingLeft: "0.5rem" }} variant="outlined">
Redact
<Switch
color="primary"
checked={isRedacted}
onChange={(_, checked: boolean) => setIsRedacted(checked)}
/>
</Paper>
}
/>
</Box>
</DialogContent>
<DialogActions>
<Button variant="contained" color="primary" onClick={() => setOpen(false)}>
Close
</Button>
</DialogActions>
</Dialog>
);
}

View file

@ -0,0 +1,45 @@
import { MenuItem, Select, Box } from "@material-ui/core";
import TruncatedText from "renderer/components/other/TruncatedText";
import { PiconeroAmount } from "../../other/Units";
import { parseDateString } from "utils/parseUtils";
import { useEffect } from "react";
import { useSwapInfosSortedByDate } from "store/hooks";
interface SwapSelectDropDownProps {
selectedSwap: string | null;
setSelectedSwap: (swapId: string | null) => void;
}
export default function SwapSelectDropDown({
selectedSwap,
setSelectedSwap,
}: SwapSelectDropDownProps) {
const swaps = useSwapInfosSortedByDate();
useEffect(() => {
if (swaps.length > 0) {
setSelectedSwap(swaps[0].swap_id);
}
}, []);
return (
<Select
value={selectedSwap ?? ""}
variant="outlined"
onChange={(e) => setSelectedSwap(e.target.value as string || null)}
style={{ width: "100%" }}
displayEmpty
>
{swaps.map((swap) => (
<MenuItem value={swap.swap_id} key={swap.swap_id}>
<Box component="span" style={{ whiteSpace: 'pre' }}>
Swap <TruncatedText>{swap.swap_id}</TruncatedText> from{' '}
{new Date(parseDateString(swap.start_date)).toDateString()} (
<PiconeroAmount amount={swap.xmr_amount} />)
</Box>
</MenuItem>
))}
<MenuItem value="">Do not attach a swap</MenuItem>
</Select>
);
}

View file

@ -0,0 +1,163 @@
import { useState, useEffect } from 'react'
import { store } from 'renderer/store/storeRenderer'
import { useActiveSwapInfo } from 'store/hooks'
import { logsToRawString } from 'utils/parseUtils'
import { getLogsOfSwap, redactLogs } from 'renderer/rpc'
import { CliLog, parseCliLogString } from 'models/cliModel'
import logger from 'utils/logger'
import { submitFeedbackViaHttp } from 'renderer/api'
import { addFeedbackId } from 'store/features/conversationsSlice'
import { AttachmentInput } from 'models/apiModel'
import { useSnackbar } from 'notistack'
export const MAX_FEEDBACK_LENGTH = 4000
interface FeedbackInputState {
bodyText: string
selectedSwap: string | null
attachDaemonLogs: boolean
isSwapLogsRedacted: boolean
isDaemonLogsRedacted: boolean
}
interface FeedbackLogsState {
swapLogs: (string | CliLog)[] | null
daemonLogs: (string | CliLog)[] | null
}
const initialInputState: FeedbackInputState = {
bodyText: '',
selectedSwap: null,
attachDaemonLogs: true,
isSwapLogsRedacted: false,
isDaemonLogsRedacted: false,
}
const initialLogsState: FeedbackLogsState = {
swapLogs: null,
daemonLogs: null,
}
export function useFeedback() {
const currentSwapId = useActiveSwapInfo()
const { enqueueSnackbar } = useSnackbar()
const [inputState, setInputState] = useState<FeedbackInputState>({
...initialInputState,
selectedSwap: currentSwapId?.swap_id || null,
})
const [logsState, setLogsState] =
useState<FeedbackLogsState>(initialLogsState)
const [isPending, setIsPending] = useState(false)
const [error, setError] = useState<string | null>(null)
const bodyTooLong = inputState.bodyText.length > MAX_FEEDBACK_LENGTH
useEffect(() => {
if (inputState.selectedSwap === null) {
setLogsState((prev) => ({ ...prev, swapLogs: null }))
return
}
getLogsOfSwap(inputState.selectedSwap, inputState.isSwapLogsRedacted)
.then((response) => {
setLogsState((prev) => ({
...prev,
swapLogs: response.logs.map(parseCliLogString),
}))
setError(null)
})
.catch((e) => {
logger.error(`Failed to fetch swap logs: ${e}`)
setLogsState((prev) => ({ ...prev, swapLogs: null }))
setError(`Failed to fetch swap logs: ${e}`)
})
}, [inputState.selectedSwap, inputState.isSwapLogsRedacted])
useEffect(() => {
if (!inputState.attachDaemonLogs) {
setLogsState((prev) => ({ ...prev, daemonLogs: null }))
return
}
try {
if (inputState.isDaemonLogsRedacted) {
redactLogs(store.getState().rpc?.logs)
.then((redactedLogs) => {
setLogsState((prev) => ({
...prev,
daemonLogs: redactedLogs,
}))
setError(null)
})
.catch((e) => {
logger.error(`Failed to redact daemon logs: ${e}`)
setLogsState((prev) => ({ ...prev, daemonLogs: null }))
setError(`Failed to redact daemon logs: ${e}`)
})
} else {
setLogsState((prev) => ({
...prev,
daemonLogs: store.getState().rpc?.logs,
}))
setError(null)
}
} catch (e) {
logger.error(`Failed to fetch daemon logs: ${e}`)
setLogsState((prev) => ({ ...prev, daemonLogs: null }))
setError(`Failed to fetch daemon logs: ${e}`)
}
}, [inputState.attachDaemonLogs, inputState.isDaemonLogsRedacted])
const clearState = () => {
setInputState(initialInputState)
setLogsState(initialLogsState)
setError(null)
}
const submitFeedback = async () => {
if (inputState.bodyText.length === 0) {
setError('Please enter a message')
throw new Error('User did not enter a message')
}
const attachments: AttachmentInput[] = []
// Add swap logs as an attachment
if (logsState.swapLogs) {
attachments.push({
key: `swap_logs_${inputState.selectedSwap}.txt`,
content: logsToRawString(logsState.swapLogs),
})
}
// Handle daemon logs
if (logsState.daemonLogs) {
attachments.push({
key: 'daemon_logs.txt',
content: logsToRawString(logsState.daemonLogs),
})
}
// Call the updated API function
const feedbackId = await submitFeedbackViaHttp(
inputState.bodyText,
attachments
)
enqueueSnackbar('Feedback submitted successfully', {
variant: 'success',
})
// Dispatch only the ID
store.dispatch(addFeedbackId(feedbackId))
}
return {
input: inputState,
setInputState,
logs: logsState,
error,
clearState,
submitFeedback,
}
}

View file

@ -0,0 +1,35 @@
import { getLogsOfSwap, saveLogFiles } from 'renderer/rpc'
import PromiseInvokeButton from 'renderer/components/PromiseInvokeButton'
import { store } from 'renderer/store/storeRenderer'
import { ButtonProps } from '@material-ui/core'
import { logsToRawString } from 'utils/parseUtils'
interface ExportLogsButtonProps extends ButtonProps {
swap_id: string
}
export default function ExportLogsButton({ swap_id, ...buttonProps }: ExportLogsButtonProps) {
async function handleExportLogs() {
const swapLogs = await getLogsOfSwap(swap_id, false)
const daemonLogs = store.getState().rpc?.logs
const logContent = {
swap_logs: logsToRawString(swapLogs.logs),
daemon_logs: logsToRawString(daemonLogs),
}
await saveLogFiles(
`swap_${swap_id}_logs.zip`,
logContent
)
}
return (
<PromiseInvokeButton
onInvoke={handleExportLogs}
{...buttonProps}
>
Export Logs
</PromiseInvokeButton>
)
}

View file

@ -21,6 +21,7 @@ import {
import { isTestnet } from "store/config";
import { getBitcoinTxExplorerUrl } from "utils/conversionUtils";
import SwapLogFileOpenButton from "./SwapLogFileOpenButton";
import ExportLogsButton from "./ExportLogsButton";
const useStyles = makeStyles((theme) => ({
outer: {
@ -128,6 +129,8 @@ export default function HistoryRowExpanded({
variant="outlined"
size="small"
/>
<ExportLogsButton swap_id={swap.swap_id} variant="outlined"
size="small"/>
</Box>
</Box>
);

View file

@ -313,3 +313,7 @@ export async function getDataDir(): Promise<string> {
export async function resolveApproval(requestId: string, accept: boolean): Promise<void> {
await invoke<ResolveApprovalArgs, ResolveApprovalResponse>("resolve_approval_request", { request_id: requestId, accept });
}
export async function saveLogFiles(zipFileName: string, content: Record<string, string>): Promise<void> {
await invokeUnsafe<void>("save_txt_files", { zipFileName, content });
}