wallet logs: less visual clutter, refactor (#2369)

* Remove unnecessary initial state for template logs

* Rename skip to noFetch

* Remove outdated TODO

* Cleaner wallet template logs + refactor
This commit is contained in:
ekzyis 2025-07-31 16:58:34 +02:00 committed by GitHub
parent 1aeb206842
commit 7857601c36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 111 additions and 85 deletions

View File

@ -5,6 +5,7 @@ import { useCallback, useEffect, useState, Fragment } from 'react'
import { timeSince } from '@/lib/time' import { timeSince } from '@/lib/time'
import classNames from 'classnames' import classNames from 'classnames'
import { ModalClosedError } from '@/components/modal' import { ModalClosedError } from '@/components/modal'
import { isTemplate } from '@/wallets/lib/util'
// TODO(wallet-v2): // TODO(wallet-v2):
// when we delete logs for a protocol, the cache is not updated // when we delete logs for a protocol, the cache is not updated
@ -28,40 +29,105 @@ export function WalletLogs ({ protocol, className, debug }) {
const embedded = !!protocol const embedded = !!protocol
// avoid unnecessary clutter when attaching new wallet
const hideLogs = logs.length === 0 && protocol && isTemplate(protocol)
if (hideLogs) return null
// showing delete button and logs footer for temporary template logs is unnecessary clutter
const template = protocol && isTemplate(protocol)
return ( return (
<> <div className={className}>
<div className={classNames('d-flex w-100 align-items-center mb-3', className)}> {!template && (
<div className='d-flex w-100 align-items-center mb-3'>
<span <span
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
className='text-muted fw-bold nav-link ms-auto' onClick={onDelete} className='text-muted fw-bold nav-link ms-auto' onClick={onDelete}
>clear logs >clear logs
</span> </span>
</div> </div>
)}
<div className={classNames(styles.container, embedded && styles.embedded)}> <div className={classNames(styles.container, embedded && styles.embedded)}>
{logs.map((log, i) => ( {logs.map((log, i) => (
<LogMessage <LogMessage
key={i} key={i}
tag={log.wallet?.name} tag={protocol ? null : log.wallet?.name}
level={log.level} level={log.level}
message={log.message} message={log.message}
context={log.context} context={log.context}
ts={log.createdAt} ts={log.createdAt}
/> />
))} ))}
{!template && <WalletLogsFooter empty={logs.length === 0} loading={loading} hasMore={hasMore} loadMore={loadMore} />}
</div>
</div>
)
}
function WalletLogsFooter ({ empty, loading, hasMore, loadMore }) {
return (
<>
{loading {loading
? <div className='w-100 text-center'>loading...</div> ? <div className='w-100 text-center'>loading...</div>
: logs.length === 0 && <div className='w-100 text-center'>empty</div>} : empty && <div className='w-100 text-center'>empty</div>}
{hasMore {hasMore
? <div className='w-100 text-center'><Button onClick={loadMore} size='sm' className='mt-3'>more</Button></div> ? <div className='w-100 text-center'><Button onClick={loadMore} size='sm' className='mt-3'>more</Button></div>
: <div className='w-100 text-center'>------ start of logs ------</div>} : <div className='w-100 text-center'>------ start of logs ------</div>}
</div>
</> </>
) )
} }
export function LogMessage ({ tag, level, message, context, ts }) { export function LogMessage ({ tag, level, message, context, ts }) {
const [show, setShow] = useState(false) const [showContext, setShowContext] = useState(false)
const filtered = context
? Object.keys(context)
.filter(key => !['send', 'recv', 'status'].includes(key))
.reduce((obj, key) => {
obj[key] = context[key]
return obj
}, {})
: {}
const hasContext = context && Object.keys(filtered).length > 0
const handleClick = () => {
if (hasContext) { setShowContext(show => !show) }
}
const style = hasContext ? { cursor: 'pointer' } : { cursor: 'inherit' }
return (
<>
<div className={styles.row} onClick={handleClick} style={style}>
<TimeSince timestamp={ts} />
{tag !== null && <Tag tag={tag?.toLowerCase() ?? 'system'} />}
<Level level={level} />
<Message message={message} />
{hasContext && <Indicator show={showContext} />}
</div>
{hasContext && showContext && <Context context={filtered} />}
</>
)
}
function TimeSince ({ timestamp }) {
const [time, setTime] = useState(timeSince(new Date(timestamp)))
useEffect(() => {
const timer = setInterval(() => {
setTime(timeSince(new Date(timestamp)))
}, 1000)
return () => clearInterval(timer)
}, [timestamp])
return <div className={styles.timestamp}>{time}</div>
}
function Tag ({ tag }) {
return <div className={styles.tag}>{`[${tag}]`}</div>
}
function Level ({ level }) {
let className let className
switch (level.toLowerCase()) { switch (level.toLowerCase()) {
case 'ok': case 'ok':
@ -80,38 +146,21 @@ export function LogMessage ({ tag, level, message, context, ts }) {
className = 'text-muted'; break className = 'text-muted'; break
} }
const filtered = context return <div className={classNames(styles.level, className)}>{level}</div>
? Object.keys(context)
.filter(key => !['send', 'recv', 'status'].includes(key))
.reduce((obj, key) => {
obj[key] = context[key]
return obj
}, {})
: {}
const hasContext = context && Object.keys(filtered).length > 0
const handleClick = () => {
if (hasContext) { setShow(show => !show) }
} }
const style = hasContext ? { cursor: 'pointer' } : { cursor: 'inherit' } function Message ({ message }) {
const indicator = hasContext ? (show ? '-' : '+') : <></> return <div className={styles.message}>{message}</div>
}
// TODO(wallet-v2): show invoice context function Indicator ({ show }) {
return <div className={styles.indicator}>{show ? '-' : '+'}</div>
}
function Context ({ context }) {
return ( return (
<>
<div className={styles.row} onClick={handleClick} style={style}>
<TimeSince timestamp={ts} />
<div className={styles.tag}>{`[${nameToTag(tag)}]`}</div>
<div className={`${styles.level} ${className}`}>{level}</div>
<div className={styles.message}>{message}</div>
<div className={styles.indicator}>{indicator}</div>
</div>
{show && hasContext && (
<div className={styles.context}> <div className={styles.context}>
{Object.entries(filtered) {Object.entries(context)
.map(([key, value], i) => { .map(([key, value], i) => {
return ( return (
<Fragment key={i}> <Fragment key={i}>
@ -121,28 +170,5 @@ export function LogMessage ({ tag, level, message, context, ts }) {
) )
})} })}
</div> </div>
)}
</>
) )
} }
function nameToTag (name) {
switch (name) {
case undefined: return 'system'
default: return name.toLowerCase()
}
}
function TimeSince ({ timestamp }) {
const [time, setTime] = useState(timeSince(new Date(timestamp)))
useEffect(() => {
const timer = setInterval(() => {
setTime(timeSince(new Date(timestamp)))
}, 1000)
return () => clearInterval(timer)
}, [timestamp])
return <div className={styles.timestamp}>{time}</div>
}

View File

@ -88,21 +88,21 @@ export function useWalletLogs (protocol, debug) {
const { templateLogs, clearTemplateLogs } = useContext(TemplateLogsContext) const { templateLogs, clearTemplateLogs } = useContext(TemplateLogsContext)
const [cursor, setCursor] = useState(null) const [cursor, setCursor] = useState(null)
// if we're configuring a protocol template, there are no logs to fetch const [logs, setLogs] = useState([])
const skip = protocol && isTemplate(protocol)
const [logs, setLogs] = useState(skip ? templateLogs : [])
// if no protocol was given, we want to fetch all logs // if no protocol was given, we want to fetch all logs
const protocolId = protocol ? Number(protocol.id) : undefined const protocolId = protocol ? Number(protocol.id) : undefined
// if we're configuring a protocol template, there are no logs to fetch
const noFetch = protocol && isTemplate(protocol)
const [fetchLogs, { called, loading, error }] = useLazyQuery(WALLET_LOGS, { const [fetchLogs, { called, loading, error }] = useLazyQuery(WALLET_LOGS, {
variables: { protocolId, debug }, variables: { protocolId, debug },
skip, skip: noFetch,
fetchPolicy: 'network-only' fetchPolicy: 'network-only'
}) })
useEffect(() => { useEffect(() => {
if (skip) return if (noFetch) return
const interval = setInterval(async () => { const interval = setInterval(async () => {
const { data, error } = await fetchLogs({ variables: { protocolId, debug } }) const { data, error } = await fetchLogs({ variables: { protocolId, debug } })
@ -118,7 +118,7 @@ export function useWalletLogs (protocol, debug) {
}, FAST_POLL_INTERVAL) }, FAST_POLL_INTERVAL)
return () => clearInterval(interval) return () => clearInterval(interval)
}, [fetchLogs, called, skip, debug]) }, [fetchLogs, called, noFetch, debug])
const loadMore = useCallback(async () => { const loadMore = useCallback(async () => {
const { data } = await fetchLogs({ variables: { protocolId, cursor, debug } }) const { data } = await fetchLogs({ variables: { protocolId, cursor, debug } })
@ -135,14 +135,14 @@ export function useWalletLogs (protocol, debug) {
return useMemo(() => { return useMemo(() => {
return { return {
loading: skip ? false : (!called ? true : loading), loading: noFetch ? false : (!called ? true : loading),
logs: skip ? templateLogs : logs, logs: noFetch ? templateLogs : logs,
error, error,
loadMore, loadMore,
hasMore: cursor !== null, hasMore: cursor !== null,
clearLogs clearLogs
} }
}, [loading, skip, called, templateLogs, logs, error, loadMore, clearLogs]) }, [loading, noFetch, called, templateLogs, logs, error, loadMore, clearLogs])
} }
function mapLevelToConsole (level) { function mapLevelToConsole (level) {