diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/components/WalletActivity.tsx')
-rw-r--r-- | packages/taler-wallet-webextension/src/components/WalletActivity.tsx | 1539 |
1 files changed, 880 insertions, 659 deletions
diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx index 60839e1f0..41b0c5c76 100644 --- a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx +++ b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx @@ -15,6 +15,7 @@ */ import { AbsoluteTime, + ExchangeStateTransitionNotification, NotificationType, ObservabilityEventType, RequestProgressNotification, @@ -22,809 +23,1029 @@ import { TalerErrorDetail, TaskProgressNotification, WalletNotification, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, JSX, VNode, h } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; import { Pages } from "../NavigationBar.js"; import { useBackendContext } from "../context/backend.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { useSettings } from "../hooks/useSettings.js"; import { Button } from "../mui/Button.js"; +import { SafeHandler } from "../mui/handlers.js"; import { WxApiType } from "../wxApi.js"; import { Modal } from "./Modal.js"; import { Time } from "./Time.js"; +import { TextField } from "../mui/TextField.js"; +import { WalletActivityTrack } from "../wxBackend.js"; -interface Props extends JSX.HTMLAttributes { -} +const OPEN_ACTIVITY_HEIGHT_PX = 250; +const CLOSE_ACTIVITY_HEIGHT_PX = 40; + +export function WalletActivity(): VNode { + const { i18n } = useTranslationContext(); + const [, updateSettings] = useSettings(); + + const [collapsed, setCollcapsed] = useState(true); -export function WalletActivity({ }: Props): VNode { - const { i18n } = useTranslationContext() - const [settings, updateSettings] = useSettings() - const api = useBackendContext(); useEffect(() => { - document.body.style.marginBottom = "250px" + document.body.style.marginBottom = `${ + collapsed ? CLOSE_ACTIVITY_HEIGHT_PX : OPEN_ACTIVITY_HEIGHT_PX + }px`; return () => { - document.body.style.marginBottom = "0px" - } - }) - const [table, setTable] = useState<"tasks" | "events">("tasks") - return ( - <div style={{ position: "fixed", bottom: 0, background: "white", zIndex: 1, height: 250, overflowY: "scroll", width: "100%" }}> - <div style={{ display: "flex", justifyContent: "space-between", float: "right" }}> - <div /> - <div> - <div style={{ padding: 4, margin: 2, border: "solid 1px black" }} onClick={() => { - updateSettings("showWalletActivity", false) - }}> - close - </div> + document.body.style.marginBottom = "0px"; + }; + }, [collapsed]); + + const [table, setTable] = useState<"tasks" | "events">("events"); + if (collapsed) { + return ( + <div + style={{ + position: "fixed", + bottom: 0, + background: "lightgrey", + zIndex: 1, + height: CLOSE_ACTIVITY_HEIGHT_PX, + overflowY: "scroll", + width: "100%", + }} + onClick={() => { + setCollcapsed(!collapsed); + }} + > + <div + style={{ + display: "flex", + justifyContent: "space-around", + marginTop: 10, + cursor: "pointer", + }} + > + click here to open </div> </div> - <div style={{ display: "flex", justifyContent: "space-around" }}> - <Button variant={table === "tasks" ? "contained" : "outlined"} + ); + } + return ( + <div + style={{ + position: "fixed", + bottom: 0, + background: "lightgrey", + zIndex: 1, + height: OPEN_ACTIVITY_HEIGHT_PX, + overflowY: "scroll", + width: "100%", + }} + > + <div + style={{ + display: "flex", + justifyContent: "space-around", + cursor: "pointer", + }} + onClick={() => { + setCollcapsed(!collapsed); + }} + > + <Button + variant={table === "events" ? "contained" : "outlined"} style={{ margin: 4 }} onClick={async () => { - setTable("tasks") + setTable("events"); }} > - <i18n.Translate>Tasks</i18n.Translate> + <i18n.Translate>Events</i18n.Translate> </Button> - <Button variant={table === "events" ? "contained" : "outlined"} + <Button + variant={table === "tasks" ? "contained" : "outlined"} style={{ margin: 4 }} onClick={async () => { - setTable("events") + setTable("tasks"); }} > - <i18n.Translate>Events</i18n.Translate> + <i18n.Translate>Active tasks</i18n.Translate> </Button> + <Button + variant="outlined" + style={{ margin: 4 }} + onClick={async () => { + updateSettings("showWalletActivity", false); + }} + > + <i18n.Translate>Close</i18n.Translate> + </Button> </div> - {(function (): VNode { - switch (table) { - case "events": { - return <ObservabilityEventsTable /> - } - case "tasks": { - return <ActiveTasksTable /> + <div + style={{ + backgroundColor: "white", + }} + > + {(function (): VNode { + switch (table) { + case "events": { + return <ObservabilityEventsTable />; + } + case "tasks": { + return <ActiveTasksTable />; + } + default: { + assertUnreachable(table); + } } - default: { - assertUnreachable(table) - } - } - })()} + })()} + </div> </div> ); } -interface MoreInfoPRops { events: (WalletNotification & { when: AbsoluteTime })[], onClick: (content: VNode) => void } -type Notif = { - id: string; +interface MoreInfoPRops { events: (WalletNotification & { when: AbsoluteTime })[]; - description: string; - start: AbsoluteTime; - end: AbsoluteTime; - reference: { - eventType: NotificationType, - referenceType: "task" | "transaction" | "operation" | "exchange", - id: string; - } | undefined, - MoreInfo: (p: MoreInfoPRops) => VNode; + onClick: (content: VNode) => void; } function ShowBalanceChange({ events }: MoreInfoPRops): VNode { if (!events.length) return <Fragment />; const not = events[0]; if (not.type !== NotificationType.BalanceChange) return <Fragment />; - return <Fragment> - <dt>Transaction</dt> - <dd> - <a title={not.hintTransactionId} href={Pages.balanceTransaction({ tid: not.hintTransactionId })}>{not.hintTransactionId.substring(0, 10)}</a> - </dd> - </Fragment> + return ( + <Fragment> + <dt>Transaction</dt> + <dd> + <a + title={not.hintTransactionId} + href={Pages.balanceTransaction({ tid: not.hintTransactionId })} + > + {not.hintTransactionId.substring(0, 10)} + </a> + </dd> + </Fragment> + ); } function ShowBackupOperationError({ events, onClick }: MoreInfoPRops): VNode { if (!events.length) return <Fragment />; const not = events[0]; if (not.type !== NotificationType.BackupOperationError) return <Fragment />; - return <Fragment> - <dt>Error</dt> - <dd> - <a href="#" onClick={(e) => { - e.preventDefault(); - const error = not.error - onClick(<Fragment> - <dl> - <dt>Code</dt> - <dd>{TalerErrorCode[error.code]} ({error.code})</dd> - <dt>Hint</dt> - <dd>{error.hint ?? "--"}</dd> - <dt>Time</dt> - <dd><Time - timestamp={error.when} - format="yyyy/MM/dd HH:mm:ss" - /></dd> - </dl> - <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}> - {JSON.stringify(error, undefined, 2)} - </pre> - </Fragment>) - }}>{TalerErrorCode[not.error.code]}</a> - </dd> - </Fragment> -} - -function ShowTransactionStateTransition({ events, onClick }: MoreInfoPRops): VNode { - if (!events.length) return <Fragment />; - const not = events[0]; - if (not.type !== NotificationType.TransactionStateTransition) return <Fragment />; - return <Fragment> - <dt>Old state</dt> - <dd> - {not.oldTxState.major} - {not.oldTxState.minor ?? ""} - </dd> - <dt>New state</dt> - <dd> - {not.newTxState.major} - {not.newTxState.minor ?? ""} - </dd> - <dt>Transaction</dt> - <dd> - <a title={not.transactionId} href={Pages.balanceTransaction({ tid: not.transactionId })}>{not.transactionId.substring(0, 10)}</a> - </dd> - {not.errorInfo ? <Fragment> + return ( + <Fragment> <dt>Error</dt> <dd> - <a href="#" onClick={(e) => { - if (!not.errorInfo) return; - e.preventDefault(); - const error = not.errorInfo; - onClick(<Fragment> - <dl> - <dt>Code</dt> - <dd>{TalerErrorCode[error.code]} ({error.code})</dd> - <dt>Hint</dt> - <dd>{error.hint ?? "--"}</dd> - <dt>Message</dt> - <dd>{error.message ?? "--"}</dd> - </dl> - </Fragment>) - - }}>{TalerErrorCode[not.errorInfo.code]}</a> + <a + href="#" + onClick={(e) => { + e.preventDefault(); + const error = not.error; + onClick( + <Fragment> + <dl> + <dt>Code</dt> + <dd> + {TalerErrorCode[error.code]} ({error.code}) + </dd> + <dt>Hint</dt> + <dd>{error.hint ?? "--"}</dd> + <dt>Time</dt> + <dd> + <Time timestamp={error.when} format="yyyy/MM/dd HH:mm:ss" /> + </dd> + </dl> + <pre + style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }} + > + {JSON.stringify(error, undefined, 2)} + </pre> + </Fragment>, + ); + }} + > + {TalerErrorCode[not.error.code]} + </a> </dd> - </Fragment> : undefined} - <dt>Experimental</dt> - <dd> - <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}> - {JSON.stringify(not.experimentalUserData, undefined, 2)} - </pre> - </dd> - - - </Fragment> + </Fragment> + ); } -function ShowExchangeStateTransition({ events, onClick }: MoreInfoPRops): VNode { + +function ShowTransactionStateTransition({ + events, + onClick, +}: MoreInfoPRops): VNode { if (!events.length) return <Fragment />; const not = events[0]; - if (not.type !== NotificationType.ExchangeStateTransition) return <Fragment />; - return <Fragment> - <dt>Exchange</dt> - <dd> - {not.exchangeBaseUrl} - </dd> - {not.oldExchangeState && not.newExchangeState.exchangeEntryStatus !== not.oldExchangeState?.exchangeEntryStatus && <Fragment> - <dt>Entry status</dt> + if (not.type !== NotificationType.TransactionStateTransition) + return <Fragment />; + return ( + <Fragment> + <dt>Old state</dt> <dd> - from {not.oldExchangeState.exchangeEntryStatus} to {not.newExchangeState.exchangeEntryStatus} + {not.oldTxState.major} - {not.oldTxState.minor ?? ""} </dd> - </Fragment>} - {not.oldExchangeState && not.newExchangeState.exchangeUpdateStatus !== not.oldExchangeState?.exchangeUpdateStatus && <Fragment> - <dt>Update status</dt> + <dt>New state</dt> <dd> - from {not.oldExchangeState.exchangeUpdateStatus} to {not.newExchangeState.exchangeUpdateStatus} + {not.newTxState.major} - {not.newTxState.minor ?? ""} </dd> - </Fragment>} - {not.oldExchangeState && not.newExchangeState.tosStatus !== not.oldExchangeState?.tosStatus && <Fragment> - <dt>Tos status</dt> + <dt>Transaction</dt> + <dd> + <a + title={not.transactionId} + href={Pages.balanceTransaction({ tid: not.transactionId })} + > + {not.transactionId.substring(0, 10)} + </a> + </dd> + {not.errorInfo ? ( + <Fragment> + <dt>Error</dt> + <dd> + <a + href="#" + onClick={(e) => { + if (!not.errorInfo) return; + e.preventDefault(); + const error = not.errorInfo; + onClick( + <Fragment> + <dl> + <dt>Code</dt> + <dd> + {TalerErrorCode[error.code]} ({error.code}) + </dd> + <dt>Hint</dt> + <dd>{error.hint ?? "--"}</dd> + <dt>Message</dt> + <dd>{error.message ?? "--"}</dd> + </dl> + </Fragment>, + ); + }} + > + {TalerErrorCode[not.errorInfo.code]} + </a> + </dd> + </Fragment> + ) : undefined} + <dt>Experimental</dt> <dd> - from {not.oldExchangeState.tosStatus} to {not.newExchangeState.tosStatus} + <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}> + {JSON.stringify(not.experimentalUserData, undefined, 2)} + </pre> </dd> - </Fragment>} - </Fragment> + </Fragment> + ); +} +function ShowExchangeStateTransition({ events }: MoreInfoPRops): VNode { + if (!events.length) return <Fragment />; + const not = events[0]; + if (not.type !== NotificationType.ExchangeStateTransition) + return <Fragment />; + return ( + <Fragment> + <dt>Exchange</dt> + <dd>{not.exchangeBaseUrl}</dd> + {not.oldExchangeState && + not.newExchangeState.exchangeEntryStatus !== + not.oldExchangeState?.exchangeEntryStatus && ( + <Fragment> + <dt>Entry status</dt> + <dd> + from {not.oldExchangeState.exchangeEntryStatus} to{" "} + {not.newExchangeState.exchangeEntryStatus} + </dd> + </Fragment> + )} + {not.oldExchangeState && + not.newExchangeState.exchangeUpdateStatus !== + not.oldExchangeState?.exchangeUpdateStatus && ( + <Fragment> + <dt>Update status</dt> + <dd> + from {not.oldExchangeState.exchangeUpdateStatus} to{" "} + {not.newExchangeState.exchangeUpdateStatus} + </dd> + </Fragment> + )} + {not.oldExchangeState && + not.newExchangeState.tosStatus !== not.oldExchangeState?.tosStatus && ( + <Fragment> + <dt>Tos status</dt> + <dd> + from {not.oldExchangeState.tosStatus} to{" "} + {not.newExchangeState.tosStatus} + </dd> + </Fragment> + )} + </Fragment> + ); } -type ObservaNotifWithTime = ((TaskProgressNotification | RequestProgressNotification) & { +type ObservaNotifWithTime = ( + | TaskProgressNotification + | RequestProgressNotification +) & { when: AbsoluteTime; -}) +}; function ShowObservabilityEvent({ events, onClick }: MoreInfoPRops): VNode { // let prev: ObservaNotifWithTime; - const asd = events.map(not => { - if (not.type !== NotificationType.RequestObservabilityEvent && not.type !== NotificationType.TaskObservabilityEvent) return <Fragment />; + const asd = events.map((not, idx) => { + if ( + not.type !== NotificationType.RequestObservabilityEvent && + not.type !== NotificationType.TaskObservabilityEvent + ) + return <Fragment />; const title = (function () { switch (not.event.type) { case ObservabilityEventType.HttpFetchFinishError: case ObservabilityEventType.HttpFetchFinishSuccess: - case ObservabilityEventType.HttpFetchStart: return "HTTP Request" + case ObservabilityEventType.HttpFetchStart: + return "HTTP Request"; case ObservabilityEventType.DbQueryFinishSuccess: case ObservabilityEventType.DbQueryFinishError: - case ObservabilityEventType.DbQueryStart: return "Database" + case ObservabilityEventType.DbQueryStart: + return "Database"; case ObservabilityEventType.RequestFinishSuccess: case ObservabilityEventType.RequestFinishError: - case ObservabilityEventType.RequestStart: return "Wallet" + case ObservabilityEventType.RequestStart: + return "Wallet"; case ObservabilityEventType.CryptoFinishSuccess: case ObservabilityEventType.CryptoFinishError: - case ObservabilityEventType.CryptoStart: return "Crypto" - case ObservabilityEventType.TaskStart: return "Task start" - case ObservabilityEventType.TaskStop: return "Task stop" - case ObservabilityEventType.TaskReset: return "Task reset" - case ObservabilityEventType.ShepherdTaskResult: return "Schedule" - case ObservabilityEventType.DeclareTaskDependency: return "Task dependency" + case ObservabilityEventType.CryptoStart: + return "Crypto"; + case ObservabilityEventType.TaskStart: + return "Task start"; + case ObservabilityEventType.TaskStop: + return "Task stop"; + case ObservabilityEventType.TaskReset: + return "Task reset"; + case ObservabilityEventType.ShepherdTaskResult: + return "Schedule"; + case ObservabilityEventType.DeclareTaskDependency: + return "Task dependency"; + case ObservabilityEventType.Message: + return "Message"; } })(); - return <ShowObervavilityDetails title={title} notif={not} onClick={onClick} /> - - }) - return <table> - <thead> - <td>Event</td> - <td>Info</td> - <td>Start</td> - <td>End</td> - </thead> - <tbody> - {asd} - </tbody> - </table> + return ( + <ShowObervavilityDetails + key={idx} + title={title} + notif={not} + onClick={onClick} + /> + ); + }); + return ( + <table> + <thead> + <td>Event</td> + <td>Info</td> + <td>Start</td> + <td>End</td> + </thead> + <tbody>{asd}</tbody> + </table> + ); } -function ShowObervavilityDetails({ title, notif, onClick, prev }: { title: string, notif: ObservaNotifWithTime, prev?: ObservaNotifWithTime, onClick: (content: VNode) => void }): VNode { +function ShowObervavilityDetails({ + title, + notif, + onClick, + prev, +}: { + title: string; + notif: ObservaNotifWithTime; + prev?: ObservaNotifWithTime; + onClick: (content: VNode) => void; +}): VNode { switch (notif.event.type) { case ObservabilityEventType.HttpFetchStart: case ObservabilityEventType.HttpFetchFinishError: case ObservabilityEventType.HttpFetchFinishSuccess: { - return <tr> - <td><a href="#" onClick={(e) => { - e.preventDefault(); - onClick(<Fragment> - <pre - style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }} - > - {JSON.stringify({ event: notif, prev }, undefined, 2)} - </pre> - </Fragment>); - }}>{title}</a></td> - <td> - {notif.event.url} { - prev?.event.type === ObservabilityEventType.HttpFetchFinishSuccess ? `(${prev.event.status})` - : prev?.event.type === ObservabilityEventType.HttpFetchFinishError ? <a href="#" onClick={(e) => { + return ( + <tr> + <td> + <a + href="#" + onClick={(e) => { e.preventDefault(); - if (prev.event.type !== ObservabilityEventType.HttpFetchFinishError) return; - const error = prev.event.error - onClick(<Fragment> - <dl> - <dt>Code</dt> - <dd>{TalerErrorCode[error.code]} ({error.code})</dd> - <dt>Hint</dt> - <dd>{error.hint ?? "--"}</dd> - <dt>Time</dt> - <dd><Time - timestamp={error.when} - format="yyyy/MM/dd HH:mm:ss" - /></dd> - </dl> - <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}> - {JSON.stringify(error, undefined, 2)} - </pre> - - </Fragment>) - }}>fail</a> : undefined - } - </td> - <td> <Time - timestamp={notif.when} - format="yyyy/MM/dd HH:mm:ss" - /></td> - <td> <Time - timestamp={prev?.when} - format="yyyy/MM/dd HH:mm:ss" - /></td> - </tr> - + onClick( + <Fragment> + <pre + style={{ + whiteSpace: "pre-wrap", + wordBreak: "break-word", + }} + > + {JSON.stringify({ event: notif, prev }, undefined, 2)} + </pre> + </Fragment>, + ); + }} + > + {title} + </a> + </td> + <td> + {notif.event.url}{" "} + {prev?.event.type === + ObservabilityEventType.HttpFetchFinishSuccess ? ( + `(${prev.event.status})` + ) : prev?.event.type === + ObservabilityEventType.HttpFetchFinishError ? ( + <a + href="#" + onClick={(e) => { + e.preventDefault(); + if ( + prev.event.type !== + ObservabilityEventType.HttpFetchFinishError + ) + return; + const error = prev.event.error; + onClick( + <Fragment> + <dl> + <dt>Code</dt> + <dd> + {TalerErrorCode[error.code]} ({error.code}) + </dd> + <dt>Hint</dt> + <dd>{error.hint ?? "--"}</dd> + <dt>Time</dt> + <dd> + <Time + timestamp={error.when} + format="yyyy/MM/dd HH:mm:ss" + /> + </dd> + </dl> + <pre + style={{ + whiteSpace: "pre-wrap", + wordBreak: "break-word", + }} + > + {JSON.stringify(error, undefined, 2)} + </pre> + </Fragment>, + ); + }} + > + fail + </a> + ) : undefined} + </td> + <td> + {" "} + <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" /> + </td> + <td> + {" "} + <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" /> + </td> + </tr> + ); } case ObservabilityEventType.DbQueryStart: case ObservabilityEventType.DbQueryFinishSuccess: case ObservabilityEventType.DbQueryFinishError: { - return <tr> - <td><a href="#" onClick={(e) => { - e.preventDefault(); - onClick(<Fragment> - <pre - style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }} + return ( + <tr> + <td> + <a + href="#" + onClick={(e) => { + e.preventDefault(); + onClick( + <Fragment> + <pre + style={{ + whiteSpace: "pre-wrap", + wordBreak: "break-word", + }} + > + {JSON.stringify({ event: notif, prev }, undefined, 2)} + </pre> + </Fragment>, + ); + }} > - {JSON.stringify({ event: notif, prev }, undefined, 2)} - </pre> - </Fragment>); - }}>{title}</a></td> - <td> - {notif.event.location} {notif.event.name} - </td> - <td> - <Time - timestamp={notif.when} - format="yyyy/MM/dd HH:mm:ss" - /> - </td> - <td> - <Time - timestamp={prev?.when} - format="yyyy/MM/dd HH:mm:ss" - /> - </td> - </tr> + {title} + </a> + </td> + <td> + {notif.event.location} {notif.event.name} + </td> + <td> + <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" /> + </td> + <td> + <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" /> + </td> + </tr> + ); } case ObservabilityEventType.TaskStart: case ObservabilityEventType.TaskStop: case ObservabilityEventType.DeclareTaskDependency: case ObservabilityEventType.TaskReset: { - return <tr> - <td><a href="#" onClick={(e) => { - e.preventDefault(); - onClick(<Fragment> - <pre - style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }} + return ( + <tr> + <td> + <a + href="#" + onClick={(e) => { + e.preventDefault(); + onClick( + <Fragment> + <pre + style={{ + whiteSpace: "pre-wrap", + wordBreak: "break-word", + }} + > + {JSON.stringify({ event: notif, prev }, undefined, 2)} + </pre> + </Fragment>, + ); + }} > - {JSON.stringify({ event: notif, prev }, undefined, 2)} - </pre> - </Fragment>); - }}>{title}</a></td> - <td> - {notif.event.taskId} - </td> - <td> - <Time - timestamp={notif.when} - format="yyyy/MM/dd HH:mm:ss" - /> - </td> - <td> - <Time - timestamp={prev?.when} - format="yyyy/MM/dd HH:mm:ss" - /> - </td> - </tr> + {title} + </a> + </td> + <td>{notif.event.taskId}</td> + <td> + <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" /> + </td> + <td> + <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" /> + </td> + </tr> + ); } case ObservabilityEventType.ShepherdTaskResult: { - return <tr> - <td><a href="#" onClick={(e) => { - e.preventDefault(); - onClick(<Fragment> - <pre - style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }} + return ( + <tr> + <td> + <a + href="#" + onClick={(e) => { + e.preventDefault(); + onClick( + <Fragment> + <pre + style={{ + whiteSpace: "pre-wrap", + wordBreak: "break-word", + }} + > + {JSON.stringify({ event: notif, prev }, undefined, 2)} + </pre> + </Fragment>, + ); + }} > - {JSON.stringify({ event: notif, prev }, undefined, 2)} - </pre> - </Fragment>); - }}>{title}</a></td> - <td> - {notif.event.resultType} - </td> - <td> - <Time - timestamp={notif.when} - format="yyyy/MM/dd HH:mm:ss" - /> - </td> - <td> - <Time - timestamp={prev?.when} - format="yyyy/MM/dd HH:mm:ss" - /> - </td> - </tr> - + {title} + </a> + </td> + <td>{notif.event.resultType}</td> + <td> + <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" /> + </td> + <td> + <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" /> + </td> + </tr> + ); } case ObservabilityEventType.CryptoStart: case ObservabilityEventType.CryptoFinishSuccess: case ObservabilityEventType.CryptoFinishError: { - return <tr> - <td><a href="#" onClick={(e) => { - e.preventDefault(); - onClick(<Fragment> - <pre - style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }} + return ( + <tr> + <td> + <a + href="#" + onClick={(e) => { + e.preventDefault(); + onClick( + <Fragment> + <pre + style={{ + whiteSpace: "pre-wrap", + wordBreak: "break-word", + }} + > + {JSON.stringify({ event: notif, prev }, undefined, 2)} + </pre> + </Fragment>, + ); + }} > - {JSON.stringify({ event: notif, prev }, undefined, 2)} - </pre> - </Fragment>); - }}>{title}</a></td> - <td> - {notif.event.operation} - </td> - <td> - <Time - timestamp={notif.when} - format="yyyy/MM/dd HH:mm:ss" - /> - </td> - <td> - <Time - timestamp={prev?.when} - format="yyyy/MM/dd HH:mm:ss" - /> - </td> - </tr> + {title} + </a> + </td> + <td>{notif.event.operation}</td> + <td> + <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" /> + </td> + <td> + <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" /> + </td> + </tr> + ); } case ObservabilityEventType.RequestStart: case ObservabilityEventType.RequestFinishSuccess: case ObservabilityEventType.RequestFinishError: { - return <tr > - <td><a href="#" onClick={(e) => { - e.preventDefault(); - onClick(<Fragment> - <pre - style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }} + return ( + <tr> + <td> + <a + href="#" + onClick={(e) => { + e.preventDefault(); + onClick( + <Fragment> + <pre + style={{ + whiteSpace: "pre-wrap", + wordBreak: "break-word", + }} + > + {JSON.stringify({ event: notif, prev }, undefined, 2)} + </pre> + </Fragment>, + ); + }} > - {JSON.stringify({ event: notif, prev }, undefined, 2)} - </pre> - </Fragment>); - }}>{title}</a></td> - <td> - {notif.event.type} - </td> - <td> - <Time - timestamp={notif.when} - format="yyyy/MM/dd HH:mm:ss" - /> - </td> - <td> - <Time - timestamp={prev?.when} - format="yyyy/MM/dd HH:mm:ss" - /> - </td> - </tr> + {title} + </a> + </td> + <td>{notif.event.type}</td> + <td> + <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" /> + </td> + <td> + <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" /> + </td> + </tr> + ); } + case ObservabilityEventType.Message: + // FIXME + return <></>; } } -function getNotificationFor(id: string, event: WalletNotification, start: AbsoluteTime, list: Notif[]): Notif | undefined { - const eventWithTime = { ...event, when: start } - switch (event.type) { - case NotificationType.BalanceChange: { - return ({ - id, - events: [eventWithTime], - reference: { - eventType: event.type, - referenceType: "transaction", - id: event.hintTransactionId, - }, - description: "Balance change", - start, - end: AbsoluteTime.never(), - MoreInfo: ShowBalanceChange - }) - } - case NotificationType.BackupOperationError: { - return ({ - id, - events: [eventWithTime], - reference: undefined, - description: "Backup error", - start, - end: AbsoluteTime.never(), - MoreInfo: ShowBackupOperationError - }) - } - case NotificationType.TransactionStateTransition: { - const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.transactionId) - if (found) { - found.end = start; - found.events.unshift(eventWithTime) - return undefined - } - return ({ - id, - events: [eventWithTime], - reference: { - eventType: event.type, - referenceType: "transaction", - id: event.transactionId, - }, - description: event.type, - start, - end: AbsoluteTime.never(), - MoreInfo: ShowTransactionStateTransition - }) - } - case NotificationType.ExchangeStateTransition: { - const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.exchangeBaseUrl) - if (found) { - found.end = start; - found.events.unshift(eventWithTime) - return undefined - } - return ({ - id, - events: [eventWithTime], - description: "Exchange update", - reference: { - eventType: event.type, - referenceType: "exchange", - id: event.exchangeBaseUrl, - }, - start, - end: AbsoluteTime.never(), - MoreInfo: ShowExchangeStateTransition - }) - } - case NotificationType.TaskObservabilityEvent: { - const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.taskId) - if (found) { - found.end = start; - found.events.unshift(eventWithTime) - return undefined - } - return ({ - id, - events: [eventWithTime], - reference: { - eventType: event.type, - referenceType: "task", - id: event.taskId, - }, - description: `Task update ${event.taskId}`, - start, - end: AbsoluteTime.never(), - MoreInfo: ShowObservabilityEvent - }) - } - case NotificationType.WithdrawalOperationTransition: { - const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.uri) - if (found) { - found.end = start; - found.events.unshift(eventWithTime) - return undefined - } - return ({ - id, - events: [eventWithTime], - reference: { - eventType: event.type, - referenceType: "task", - id: event.uri, - }, - description: `Withdrawal operation updated`, - start, - end: AbsoluteTime.never(), - MoreInfo: ShowObservabilityEvent - }) - } - case NotificationType.RequestObservabilityEvent: { - const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.requestId) - if (found) { - found.end = start; - found.events.unshift(eventWithTime) - return undefined - } - return ({ - id, - events: [eventWithTime], - reference: { - eventType: event.type, - referenceType: "operation", - id: event.requestId, - }, - description: `wallet.${event.operation}(${event.requestId})`, - start, - end: AbsoluteTime.never(), - MoreInfo: ShowObservabilityEvent - }) - } - default: { - assertUnreachable(event) - } - } +function refresh( + api: WxApiType, + onUpdate: (list: WalletActivityTrack[]) => void, + filter: string, +) { + api.background + .call("getNotifications", { filter }) + .then((notif) => { + onUpdate(notif); + }) + .catch((error) => { + console.log(error); + }); } - -function refresh(api: WxApiType, onUpdate: (list: Notif[]) => void) { - api.background.call("getNotifications", undefined).then(notif => { - - const list: Notif[] = [] - for (const n of notif) { - if (n.notification.type === NotificationType.RequestObservabilityEvent && - n.notification.operation === "getActiveTasks") { - //ignore monitor request - continue; - } - const event = getNotificationFor(String(list.length), n.notification, n.when, list) - // pepe. - if (event) { - list.unshift(event) - } - } - onUpdate(list); - }).catch(error => { - console.log(error) - }) -} - -export function ObservabilityEventsTable({ }: {}): VNode { - const { i18n } = useTranslationContext() +export function ObservabilityEventsTable(): VNode { + const { i18n } = useTranslationContext(); const api = useBackendContext(); - const [notifications, setNotifications] = useState<Notif[]>([]) - const [showDetails, setShowDetails] = useState<VNode>() + const [notifications, setNotifications] = useState<WalletActivityTrack[]>([]); + const [showDetails, setShowDetails] = useState<VNode>(); + const [filter, onChangeFilter] = useState(""); useEffect(() => { let lastTimeout: ReturnType<typeof setTimeout>; function periodicRefresh() { - - refresh(api, setNotifications) + refresh(api, setNotifications, filter); lastTimeout = setTimeout(() => { periodicRefresh(); - }, 1000) + }, 1000); - //clear on unload - return () => { clearTimeout(lastTimeout) } + return () => { + clearTimeout(lastTimeout); + }; } - return periodicRefresh() - }, [1]); - - return <div> - <div style={{ display: "flex", justifyContent: "space-between" }}> + return periodicRefresh(); + }, [filter]); - <div style={{ padding: 4, margin: 2, border: "solid 1px black" }} onClick={() => { - api.background.call("clearNotifications", undefined).then(d => { - refresh(api, setNotifications) - }) - }}> - clear + return ( + <div> + <div style={{ display: "flex", justifyContent: "space-between" }}> + <TextField + label="Filter" + variant="outlined" + value={filter} + onChange={onChangeFilter} + /> + <div + style={{ + padding: 4, + margin: 2, + border: "solid 1px black", + alignSelf: "center", + }} + onClick={() => { + api.background.call("clearNotifications", undefined).then(() => { + refresh(api, setNotifications, filter); + }); + }} + > + clear + </div> </div> - - - </div> - {showDetails && <Modal title="event details" onClose={{ onClick: (async () => { setShowDetails(undefined) }) as any }} > - {showDetails} - </Modal>} - {notifications.map((not) => { - return ( - <details key={not.id}> - <summary> - <div style={{ width: "90%", display: "inline-flex", justifyContent: "space-between", padding: 4 }}> - <div style={{ padding: 4 }}> - {not.description} - </div> - <div style={{ padding: 4 }}> - <Time - timestamp={not.start} - format="yyyy/MM/dd HH:mm:ss" - /> + {showDetails && ( + <Modal + title="event details" + onClose={{ + onClick: (async () => { + setShowDetails(undefined); + }) as SafeHandler<void>, + }} + > + {showDetails} + </Modal> + )} + {notifications.map((not) => { + return ( + <details key={not.id}> + <summary> + <div + style={{ + width: "90%", + display: "inline-flex", + justifyContent: "space-between", + padding: 4, + }} + > + <div style={{ padding: 4 }}> + {(() => { + switch (not.type) { + case NotificationType.BalanceChange: + return i18n.str`Balance change`; + case NotificationType.BackupOperationError: + return i18n.str`Backup failed`; + case NotificationType.TransactionStateTransition: + return i18n.str`Transaction updated`; + case NotificationType.ExchangeStateTransition: + return i18n.str`Exchange updated`; + case NotificationType.Idle: + return i18n.str`Idle`; + case NotificationType.TaskObservabilityEvent: + return i18n.str`task.${ + (not.events[0] as TaskProgressNotification).taskId + }`; + case NotificationType.RequestObservabilityEvent: + return i18n.str`wallet.${ + (not.events[0] as RequestProgressNotification) + .operation + }(${ + (not.events[0] as RequestProgressNotification) + .requestId + })`; + case NotificationType.WithdrawalOperationTransition: { + return `---`; + } + default: { + assertUnreachable(not.type); + } + } + })()} + </div> + <div style={{ padding: 4 }}> + <Time timestamp={not.start} format="yyyy/MM/dd HH:mm:ss" /> + </div> + <div style={{ padding: 4 }}> + <Time timestamp={not.end} format="yyyy/MM/dd HH:mm:ss" /> + </div> </div> - <div style={{ padding: 4 }}><Time - timestamp={not.end} - format="yyyy/MM/dd HH:mm:ss" - /></div> - </div> - </summary> - <not.MoreInfo events={not.events} onClick={(details) => { - setShowDetails(details) - }} /> - </details> - ); - })} - </div > + </summary> + {(() => { + switch (not.type) { + case NotificationType.BalanceChange: { + return ( + <ShowBalanceChange + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.BackupOperationError: { + return ( + <ShowBackupOperationError + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.TransactionStateTransition: { + return ( + <ShowTransactionStateTransition + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.ExchangeStateTransition: { + return ( + <ShowExchangeStateTransition + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.Idle: { + return <div>not implemented</div>; + } + case NotificationType.TaskObservabilityEvent: { + return ( + <ShowObservabilityEvent + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.RequestObservabilityEvent: { + return ( + <ShowObservabilityEvent + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.WithdrawalOperationTransition: { + return <div>not implemented</div>; + } + } + })()} + </details> + ); + })} + </div> + ); } -function ErroDetailModal({ error, onClose }: { error: TalerErrorDetail, onClose: () => void }): VNode { - return <Modal title="Full detail" onClose={{ - onClick: onClose as any - }}> - <dl> - <dt>Code</dt> - <dd>{TalerErrorCode[error.code]} ({error.code})</dd> - <dt>Hint</dt> - <dd>{error.hint ?? "--"}</dd> - <dt>Time</dt> - <dd><Time - timestamp={error.when} - format="yyyy/MM/dd HH:mm:ss" - /></dd> - </dl> - <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}> - {JSON.stringify(error, undefined, 2)} - </pre> - </Modal> +function ErroDetailModal({ + error, + onClose, +}: { + error: TalerErrorDetail; + onClose: () => void; +}): VNode { + return ( + <Modal + title="Full detail" + onClose={{ + onClick: onClose as SafeHandler<void>, + }} + > + <dl> + <dt>Code</dt> + <dd> + {TalerErrorCode[error.code]} ({error.code}) + </dd> + <dt>Hint</dt> + <dd>{error.hint ?? "--"}</dd> + <dt>Time</dt> + <dd> + <Time timestamp={error.when} format="yyyy/MM/dd HH:mm:ss" /> + </dd> + </dl> + <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}> + {JSON.stringify(error, undefined, 2)} + </pre> + </Modal> + ); } -export function ActiveTasksTable({ }: {}): VNode { - const { i18n } = useTranslationContext() +export function ActiveTasksTable(): VNode { + const { i18n } = useTranslationContext(); const api = useBackendContext(); const state = useAsyncAsHook(() => { return api.wallet.call(WalletApiOperation.GetActiveTasks, {}); }); - const [showError, setShowError] = useState<TalerErrorDetail>() + const [showError, setShowError] = useState<TalerErrorDetail>(); const tasks = state && !state.hasError ? state.response.tasks : []; useEffect(() => { - if (!state || state.hasError) return + if (!state || state.hasError) return; const lastTimeout = setTimeout(() => { state.retry(); - }, 1000) + }, 1000); return () => { - clearTimeout(lastTimeout) - } - }, [tasks]) + clearTimeout(lastTimeout); + }; + }, [tasks]); - // const listenAllEvents = Array.from<NotificationType>({ length: 1 }); - // listenAllEvents.includes = () => true - // useEffect(() => { - // return api.listener.onUpdateNotification(listenAllEvents, (notif) => { - // state?.retry() - // }); - // }); - return <Fragment> - {showError && <ErroDetailModal error={showError} onClose={(async () => { setShowError(undefined) })} />} + return ( + <Fragment> + {showError && ( + <ErroDetailModal + error={showError} + onClose={async () => { + setShowError(undefined); + }} + /> + )} - <table style={{ width: "100%" }}> - <thead> - <tr> - <th> - <i18n.Translate>Type</i18n.Translate> - </th> - <th> - <i18n.Translate>Id</i18n.Translate> - </th> - <th> - <i18n.Translate>Since</i18n.Translate> - </th> - <th> - <i18n.Translate>Next try</i18n.Translate> - </th> - <th> - <i18n.Translate>Error</i18n.Translate> - </th> - <th> - <i18n.Translate>Transaction</i18n.Translate> - </th> - </tr> - </thead> - <tbody> - {tasks.map((task) => { - const [type, id] = task.id.split(":") - return ( - <tr> - <td>{type}</td> - <td title={id}>{id.substring(0, 10)}</td> - <td> - <Time - timestamp={task.firstTry} - format="yyyy/MM/dd HH:mm:ss" - /> - </td> - <td> - <Time - timestamp={task.nextTry} - format="yyyy/MM/dd HH:mm:ss" - /> - </td> - <td>{!task.lastError?.code ? "" : <a href="#" onClick={(e) => { e.preventDefault(); setShowError(task.lastError) }}>{TalerErrorCode[task.lastError.code]}</a>}</td> - <td> - {task.transaction ? <a title={task.transaction} href={Pages.balanceTransaction({ tid: task.transaction })}>{task.transaction.substring(0, 10)}</a> : "--"} - </td> - </tr> - ); - })} - </tbody> - </table> - </Fragment> -}
\ No newline at end of file + <table style={{ width: "100%" }}> + <thead> + <tr> + <th> + <i18n.Translate>Type</i18n.Translate> + </th> + <th> + <i18n.Translate>Id</i18n.Translate> + </th> + <th> + <i18n.Translate>Since</i18n.Translate> + </th> + <th> + <i18n.Translate>Next try</i18n.Translate> + </th> + <th> + <i18n.Translate>Error</i18n.Translate> + </th> + <th> + <i18n.Translate>Transaction</i18n.Translate> + </th> + </tr> + </thead> + <tbody> + {tasks.map((task) => { + const [type, id] = task.taskId.split(":"); + return ( + <tr key={id}> + <td>{type}</td> + <td title={id}>{id.substring(0, 10)}</td> + <td> + <Time + timestamp={task.firstTry} + format="yyyy/MM/dd HH:mm:ss" + /> + </td> + <td> + <Time timestamp={task.nextTry} format="yyyy/MM/dd HH:mm:ss" /> + </td> + <td> + {!task.lastError?.code ? ( + "" + ) : ( + <a + href="#" + onClick={(e) => { + e.preventDefault(); + setShowError(task.lastError); + }} + > + {TalerErrorCode[task.lastError.code]} + </a> + )} + </td> + <td> + {task.transaction ? ( + <a + title={task.transaction} + href={Pages.balanceTransaction({ tid: task.transaction })} + > + {task.transaction.substring(0, 10)} + </a> + ) : ( + "--" + )} + </td> + </tr> + ); + })} + </tbody> + </table> + </Fragment> + ); +} |