Merge pull request #73 from UnstoppableSwap/gui/allow-logs-of-swap-button

feat(gui): Display logs of specific swap on press of button on history page
This commit is contained in:
binarybaron 2024-09-09 21:49:45 +02:00 committed by GitHub
commit 6a3a0a5458
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 115 additions and 38 deletions

View file

@ -18,3 +18,26 @@ export interface CliLog {
[index: string]: unknown; [index: string]: unknown;
}[]; }[];
} }
function isCliLog(log: unknown): log is CliLog {
return (
typeof log === "object" &&
log !== null &&
"timestamp" in log &&
"level" in log &&
"fields" in log
);
}
export function parseCliLogString(log: string): CliLog | string {
try {
const parsed = JSON.parse(log);
if (isCliLog(parsed)) {
return parsed;
} else {
return log;
}
} catch (err) {
return log;
}
}

View file

@ -11,6 +11,7 @@ import {
import { ReactNode } from "react"; import { ReactNode } from "react";
import { exhaustiveGuard } from "utils/typescriptUtils"; import { exhaustiveGuard } from "utils/typescriptUtils";
import HumanizedBitcoinBlockDuration from "../other/HumanizedBitcoinBlockDuration"; import HumanizedBitcoinBlockDuration from "../other/HumanizedBitcoinBlockDuration";
import TruncatedText from "../other/TruncatedText";
import { import {
SwapCancelRefundButton, SwapCancelRefundButton,
SwapResumeButton, SwapResumeButton,
@ -219,7 +220,7 @@ export default function SwapStatusAlert({
variant="filled" variant="filled"
> >
<AlertTitle> <AlertTitle>
Swap {swap.swap_id.substring(0, 5)}... is unfinished Swap <TruncatedText>{swap.swap_id}</TruncatedText> is unfinished
</AlertTitle> </AlertTitle>
<SwapAlertStatusText swap={swap} /> <SwapAlertStatusText swap={swap} />
</Alert> </Alert>

View file

@ -1,4 +1,5 @@
import { DialogTitle, makeStyles, Typography } from "@material-ui/core"; import { DialogTitle, makeStyles, Typography } from "@material-ui/core";
import { ReactNode } from "react";
const useStyles = makeStyles({ const useStyles = makeStyles({
root: { root: {
@ -8,7 +9,7 @@ const useStyles = makeStyles({
}); });
type DialogTitleProps = { type DialogTitleProps = {
title: string; title: ReactNode;
}; };
export default function DialogHeader({ title }: DialogTitleProps) { export default function DialogHeader({ title }: DialogTitleProps) {

View file

@ -13,6 +13,7 @@ import {
import { CliLog } from "models/cliModel"; import { CliLog } from "models/cliModel";
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { useState } from "react"; import { useState } from "react";
import TruncatedText from "renderer/components/other/TruncatedText";
import { store } from "renderer/store/storeRenderer"; import { store } from "renderer/store/storeRenderer";
import { useActiveSwapInfo, useAppSelector } from "store/hooks"; import { useActiveSwapInfo, useAppSelector } from "store/hooks";
import { parseDateString } from "utils/parseUtils"; import { parseDateString } from "utils/parseUtils";
@ -68,7 +69,7 @@ function SwapSelectDropDown({
<MenuItem value={0}>Do not attach logs</MenuItem> <MenuItem value={0}>Do not attach logs</MenuItem>
{swaps.map((swap) => ( {swaps.map((swap) => (
<MenuItem value={swap.swap_id} key={swap.swap_id}> <MenuItem value={swap.swap_id} key={swap.swap_id}>
Swap {swap.swap_id.substring(0, 5)}... from{" "} Swap <TruncatedText>{swap.swap_id}</TruncatedText> from{" "}
{new Date(parseDateString(swap.start_date)).toDateString()} ( {new Date(parseDateString(swap.start_date)).toDateString()} (
<PiconeroAmount amount={swap.xmr_amount} />) <PiconeroAmount amount={swap.xmr_amount} />)
</MenuItem> </MenuItem>

View file

@ -14,6 +14,7 @@ import {
import { Multiaddr } from "multiaddr"; import { Multiaddr } from "multiaddr";
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { ChangeEvent, useState } from "react"; import { ChangeEvent, useState } from "react";
import TruncatedText from "renderer/components/other/TruncatedText";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
const PRESET_RENDEZVOUS_POINTS = [ const PRESET_RENDEZVOUS_POINTS = [
@ -108,10 +109,7 @@ export default function ListSellersDialog({
<Chip <Chip
key={rAddress} key={rAddress}
clickable clickable
label={`${rAddress.substring( label={<TruncatedText limit={30}>{rAddress}</TruncatedText>}
0,
Math.min(rAddress.length - 1, 20),
)}...`}
onClick={() => setRendezvousAddress(rAddress)} onClick={() => setRendezvousAddress(rAddress)}
/> />
))} ))}

View file

@ -1,6 +1,7 @@
import { Box, Chip, makeStyles, Tooltip, Typography } from "@material-ui/core"; import { Box, Chip, makeStyles, Tooltip, Typography } from "@material-ui/core";
import { VerifiedUser } from "@material-ui/icons"; import { VerifiedUser } from "@material-ui/icons";
import { ExtendedProviderStatus } from "models/apiModel"; import { ExtendedProviderStatus } from "models/apiModel";
import TruncatedText from "renderer/components/other/TruncatedText";
import { import {
MoneroBitcoinExchangeRate, MoneroBitcoinExchangeRate,
SatsAmount, SatsAmount,
@ -38,7 +39,7 @@ export default function ProviderInfo({
{provider.multiAddr} {provider.multiAddr}
</Typography> </Typography>
<Typography color="textSecondary" gutterBottom> <Typography color="textSecondary" gutterBottom>
{provider.peerId.substring(0, 8)}...{provider.peerId.slice(-8)} <TruncatedText limit={12}>{provider.peerId}</TruncatedText>
</Typography> </Typography>
<Typography variant="caption"> <Typography variant="caption">
Exchange rate:{" "} Exchange rate:{" "}

View file

@ -22,7 +22,7 @@ const useStyles = makeStyles((theme) => ({
fieldsOuter: { fieldsOuter: {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: theme.spacing(2), gap: theme.spacing(1.5),
}, },
})); }));
@ -51,8 +51,8 @@ export default function InitPage() {
return ( return (
<Box> <Box>
<RemainingFundsWillBeUsedAlert />
<Box className={classes.fieldsOuter}> <Box className={classes.fieldsOuter}>
<RemainingFundsWillBeUsedAlert />
<MoneroAddressTextField <MoneroAddressTextField
label="Monero redeem address" label="Monero redeem address"
address={redeemAddress} address={redeemAddress}

View file

@ -19,6 +19,8 @@ export default function WithdrawDialog({
const [withdrawAddressValid, setWithdrawAddressValid] = useState(false); const [withdrawAddressValid, setWithdrawAddressValid] = useState(false);
const [withdrawAddress, setWithdrawAddress] = useState<string>(""); const [withdrawAddress, setWithdrawAddress] = useState<string>("");
const haveFundsBeenWithdrawn = withdrawTxId !== null;
function onCancel() { function onCancel() {
if (!pending) { if (!pending) {
setWithdrawTxId(null); setWithdrawTxId(null);
@ -31,34 +33,34 @@ export default function WithdrawDialog({
<Dialog open={open} onClose={onCancel} maxWidth="sm" fullWidth> <Dialog open={open} onClose={onCancel} maxWidth="sm" fullWidth>
<DialogHeader title="Withdraw Bitcoin" /> <DialogHeader title="Withdraw Bitcoin" />
<WithdrawDialogContent isPending={pending} withdrawTxId={withdrawTxId}> <WithdrawDialogContent isPending={pending} withdrawTxId={withdrawTxId}>
{withdrawTxId === null ? ( {haveFundsBeenWithdrawn ? (
<BtcTxInMempoolPageContent withdrawTxId={withdrawTxId} />
) : (
<AddressInputPage <AddressInputPage
setWithdrawAddress={setWithdrawAddress} setWithdrawAddress={setWithdrawAddress}
withdrawAddress={withdrawAddress} withdrawAddress={withdrawAddress}
setWithdrawAddressValid={setWithdrawAddressValid} setWithdrawAddressValid={setWithdrawAddressValid}
/> />
) : (
<BtcTxInMempoolPageContent withdrawTxId={withdrawTxId} />
)} )}
</WithdrawDialogContent> </WithdrawDialogContent>
<DialogActions> <DialogActions>
<Button onClick={onCancel} color="primary" disabled={pending}> <Button
{withdrawTxId === null ? "Cancel" : "Done"} onClick={onCancel}
color="primary"
disabled={pending}
variant={haveFundsBeenWithdrawn ? "contained" : "text"}
>
{haveFundsBeenWithdrawn ? "Done" : "Close"}
</Button> </Button>
{withdrawTxId === null && ( {!haveFundsBeenWithdrawn && (
<PromiseInvokeButton <PromiseInvokeButton
displayErrorSnackbar displayErrorSnackbar
variant="contained" variant="contained"
color="primary" color="primary"
disabled={!withdrawAddressValid} disabled={!withdrawAddressValid}
onInvoke={() => withdrawBtc(withdrawAddress)} onInvoke={() => withdrawBtc(withdrawAddress)}
onPendingChange={(pending) => { onPendingChange={setPending}
console.log("pending", pending); onSuccess={setWithdrawTxId}
setPending(pending);
}}
onSuccess={(txId) => {
setWithdrawTxId(txId);
}}
> >
Withdraw Withdraw
</PromiseInvokeButton> </PromiseInvokeButton>

View file

@ -0,0 +1,14 @@
export default function TruncatedText({
children,
limit = 6,
ellipsis = "...",
}: {
children: string;
limit?: number;
ellipsis?: string;
}) {
const truncatedText =
children.length > limit ? children.slice(0, limit) + ellipsis : children;
return truncatedText;
}

View file

@ -11,6 +11,7 @@ import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp"; import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp";
import { GetSwapInfoResponse } from "models/tauriModel"; import { GetSwapInfoResponse } from "models/tauriModel";
import { useState } from "react"; import { useState } from "react";
import TruncatedText from "renderer/components/other/TruncatedText";
import { PiconeroAmount, SatsAmount } from "../../../other/Units"; import { PiconeroAmount, SatsAmount } from "../../../other/Units";
import HistoryRowActions from "./HistoryRowActions"; import HistoryRowActions from "./HistoryRowActions";
import HistoryRowExpanded from "./HistoryRowExpanded"; import HistoryRowExpanded from "./HistoryRowExpanded";
@ -52,7 +53,9 @@ export default function HistoryRow(swap: GetSwapInfoResponse) {
{expanded ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />} {expanded ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton> </IconButton>
</TableCell> </TableCell>
<TableCell>{swap.swap_id}</TableCell> <TableCell>
<TruncatedText>{swap.swap_id}</TruncatedText>
</TableCell>
<TableCell> <TableCell>
<AmountTransfer <AmountTransfer
xmrAmount={swap.xmr_amount} xmrAmount={swap.xmr_amount}

View file

@ -6,29 +6,38 @@ import {
DialogTitle, DialogTitle,
} from "@material-ui/core"; } from "@material-ui/core";
import { ButtonProps } from "@material-ui/core/Button/Button"; import { ButtonProps } from "@material-ui/core/Button/Button";
import { CliLog } from "models/cliModel"; import { CliLog, parseCliLogString } from "models/cliModel";
import { GetLogsResponse } from "models/tauriModel";
import { useState } from "react"; import { useState } from "react";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { getLogsOfSwap } from "renderer/rpc";
import CliLogsBox from "../../../other/RenderedCliLog"; import CliLogsBox from "../../../other/RenderedCliLog";
export default function SwapLogFileOpenButton({ export default function SwapLogFileOpenButton({
swapId, swapId,
...props ...props
}: { swapId: string } & ButtonProps) { }: { swapId: string } & ButtonProps) {
const [logs, setLogs] = useState<CliLog[] | null>(null); const [logs, setLogs] = useState<(CliLog | string)[] | null>(null);
function onLogsReceived(response: GetLogsResponse) {
setLogs(response.logs.map(parseCliLogString));
}
return ( return (
<> <>
<PromiseInvokeButton <PromiseInvokeButton
onSuccess={(data) => { onSuccess={onLogsReceived}
setLogs(data as CliLog[]); onInvoke={() => getLogsOfSwap(swapId, false)}
}}
onInvoke={async () => {
throw new Error("Not implemented");
}}
{...props} {...props}
> >
View log View full logs
</PromiseInvokeButton>
<PromiseInvokeButton
onSuccess={onLogsReceived}
onInvoke={() => getLogsOfSwap(swapId, true)}
{...props}
>
View redacted logs
</PromiseInvokeButton> </PromiseInvokeButton>
{logs && ( {logs && (
<Dialog open onClose={() => setLogs(null)} fullWidth maxWidth="lg"> <Dialog open onClose={() => setLogs(null)} fullWidth maxWidth="lg">
@ -37,7 +46,13 @@ export default function SwapLogFileOpenButton({
<CliLogsBox logs={logs} label="Logs relevant to the swap" /> <CliLogsBox logs={logs} label="Logs relevant to the swap" />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setLogs(null)}>Close</Button> <Button
onClick={() => setLogs(null)}
variant="contained"
color="primary"
>
Close
</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
)} )}

View file

@ -9,6 +9,7 @@ import {
} from "@material-ui/core"; } from "@material-ui/core";
import { ButtonProps } from "@material-ui/core/Button/Button"; import { ButtonProps } from "@material-ui/core/Button/Button";
import { BobStateName, GetSwapInfoResponseExt } from "models/tauriModelExt"; import { BobStateName, GetSwapInfoResponseExt } from "models/tauriModelExt";
import TruncatedText from "renderer/components/other/TruncatedText";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { getMoneroRecoveryKeys } from "renderer/rpc"; import { getMoneroRecoveryKeys } from "renderer/rpc";
import { store } from "renderer/store/storeRenderer"; import { store } from "renderer/store/storeRenderer";
@ -38,7 +39,9 @@ function MoneroRecoveryKeysDialog({
return ( return (
<Dialog open onClose={onClose} maxWidth="sm" fullWidth> <Dialog open onClose={onClose} maxWidth="sm" fullWidth>
<DialogHeader <DialogHeader
title={`Recovery Keys for swap ${swap_id.substring(0, 5)}...`} title=<>
Recovery Keys for swap <TruncatedText>{swap_id}</TruncatedText>
</>
/> />
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>

View file

@ -5,6 +5,8 @@ import {
BalanceResponse, BalanceResponse,
BuyXmrArgs, BuyXmrArgs,
BuyXmrResponse, BuyXmrResponse,
GetLogsArgs,
GetLogsResponse,
GetSwapInfoResponse, GetSwapInfoResponse,
MoneroRecoveryArgs, MoneroRecoveryArgs,
ResumeSwapArgs, ResumeSwapArgs,
@ -132,3 +134,13 @@ export async function checkContextAvailability(): Promise<boolean> {
const available = await invokeNoArgs<boolean>("is_context_available"); const available = await invokeNoArgs<boolean>("is_context_available");
return available; return available;
} }
export async function getLogsOfSwap(
swapId: string,
redact: boolean,
): Promise<GetLogsResponse> {
return await invoke<GetLogsArgs, GetLogsResponse>("get_logs", {
swap_id: swapId,
redact,
});
}

View file

@ -3,8 +3,8 @@ use std::sync::Arc;
use swap::cli::{ use swap::cli::{
api::{ api::{
request::{ request::{
BalanceArgs, BuyXmrArgs, GetHistoryArgs, GetSwapInfosAllArgs, MoneroRecoveryArgs, BalanceArgs, BuyXmrArgs, GetHistoryArgs, GetLogsArgs, GetSwapInfosAllArgs,
ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs, MoneroRecoveryArgs, ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
}, },
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle}, tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle},
Context, ContextBuilder, Context, ContextBuilder,
@ -168,6 +168,7 @@ pub fn run() {
resume_swap, resume_swap,
get_history, get_history,
monero_recovery, monero_recovery,
get_logs,
suspend_current_swap, suspend_current_swap,
is_context_available, is_context_available,
]) ])
@ -206,6 +207,7 @@ tauri_command!(buy_xmr, BuyXmrArgs);
tauri_command!(resume_swap, ResumeSwapArgs); tauri_command!(resume_swap, ResumeSwapArgs);
tauri_command!(withdraw_btc, WithdrawBtcArgs); tauri_command!(withdraw_btc, WithdrawBtcArgs);
tauri_command!(monero_recovery, MoneroRecoveryArgs); tauri_command!(monero_recovery, MoneroRecoveryArgs);
tauri_command!(get_logs, GetLogsArgs);
// These commands require no arguments // These commands require no arguments
tauri_command!(suspend_current_swap, SuspendCurrentSwapArgs, no_args); tauri_command!(suspend_current_swap, SuspendCurrentSwapArgs, no_args);

View file

@ -359,9 +359,10 @@ impl Request for GetSwapInfosAllArgs {
#[typeshare] #[typeshare]
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct GetLogsArgs { pub struct GetLogsArgs {
#[typeshare(serialized_as = "Option<string>")]
pub swap_id: Option<Uuid>, pub swap_id: Option<Uuid>,
pub redact: bool, pub redact: bool,
#[typeshare(serialized_as = "string")] #[typeshare(serialized_as = "Option<string>")]
pub logs_dir: Option<PathBuf>, pub logs_dir: Option<PathBuf>,
} }