summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/components/WalletActivity.tsx')
-rw-r--r--packages/taler-wallet-webextension/src/components/WalletActivity.tsx1539
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>
+ );
+}