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.tsx1100
1 files changed, 1100 insertions, 0 deletions
diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
new file mode 100644
index 000000000..69a2c0675
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
@@ -0,0 +1,1100 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ NotificationType,
+ ObservabilityEventType,
+ RequestProgressNotification,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TaskProgressNotification,
+ WalletNotification,
+ 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 { 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 { WxApiType } from "../wxApi.js";
+import { Modal } from "./Modal.js";
+import { Time } from "./Time.js";
+
+interface Props extends JSX.HTMLAttributes {}
+
+export function WalletActivity({}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings();
+ const api = useBackendContext();
+ useEffect(() => {
+ document.body.style.marginBottom = "250px";
+ 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>
+ </div>
+ </div>
+ <div style={{ display: "flex", justifyContent: "space-around" }}>
+ <Button
+ variant={table === "tasks" ? "contained" : "outlined"}
+ style={{ margin: 4 }}
+ onClick={async () => {
+ setTable("tasks");
+ }}
+ >
+ <i18n.Translate>Tasks</i18n.Translate>
+ </Button>
+ <Button
+ variant={table === "events" ? "contained" : "outlined"}
+ style={{ margin: 4 }}
+ onClick={async () => {
+ setTable("events");
+ }}
+ >
+ <i18n.Translate>Events</i18n.Translate>
+ </Button>
+ </div>
+ {(function (): VNode {
+ switch (table) {
+ case "events": {
+ return <ObservabilityEventsTable />;
+ }
+ case "tasks": {
+ return <ActiveTasksTable />;
+ }
+ default: {
+ assertUnreachable(table);
+ }
+ }
+ })()}
+ </div>
+ );
+}
+
+interface MoreInfoPRops {
+ events: (WalletNotification & { when: AbsoluteTime })[];
+ onClick: (content: VNode) => void;
+}
+type Notif = {
+ id: string;
+ 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;
+};
+
+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>
+ );
+}
+
+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>
+ <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>
+ <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {JSON.stringify(not.experimentalUserData, undefined, 2)}
+ </pre>
+ </dd>
+ </Fragment>
+ );
+}
+function ShowExchangeStateTransition({
+ 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>
+ <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
+) & {
+ 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 title = (function () {
+ switch (not.event.type) {
+ case ObservabilityEventType.HttpFetchFinishError:
+ case ObservabilityEventType.HttpFetchFinishSuccess:
+ case ObservabilityEventType.HttpFetchStart:
+ return "HTTP Request";
+ case ObservabilityEventType.DbQueryFinishSuccess:
+ case ObservabilityEventType.DbQueryFinishError:
+ case ObservabilityEventType.DbQueryStart:
+ return "Database";
+ case ObservabilityEventType.RequestFinishSuccess:
+ case ObservabilityEventType.RequestFinishError:
+ 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.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>
+ );
+}
+
+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) => {
+ 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",
+ }}
+ >
+ {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>
+ );
+ }
+
+ 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",
+ }}
+ >
+ {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>
+ );
+ }
+ case ObservabilityEventType.ShepherdTaskResult: {
+ 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.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",
+ }}
+ >
+ {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>
+ );
+ }
+ 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",
+ }}
+ >
+ {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>
+ );
+ }
+ 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,
+ };
+ }
+ case NotificationType.Idle:
+ return undefined;
+ default: {
+ assertUnreachable(event);
+ }
+ }
+}
+
+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();
+ const api = useBackendContext();
+
+ const [notifications, setNotifications] = useState<Notif[]>([]);
+ const [showDetails, setShowDetails] = useState<VNode>();
+
+ useEffect(() => {
+ let lastTimeout: ReturnType<typeof setTimeout>;
+ function periodicRefresh() {
+ refresh(api, setNotifications);
+
+ lastTimeout = setTimeout(() => {
+ periodicRefresh();
+ }, 1000);
+
+ //clear on unload
+ return () => {
+ clearTimeout(lastTimeout);
+ };
+ }
+ return periodicRefresh();
+ }, [1]);
+
+ return (
+ <div>
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <div
+ style={{ padding: 4, margin: 2, border: "solid 1px black" }}
+ onClick={() => {
+ api.background.call("clearNotifications", undefined).then((d) => {
+ refresh(api, setNotifications);
+ });
+ }}
+ >
+ clear
+ </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" />
+ </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>
+ );
+}
+
+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>
+ );
+}
+
+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 tasks = state && !state.hasError ? state.response.tasks : [];
+
+ useEffect(() => {
+ if (!state || state.hasError) return;
+ const lastTimeout = setTimeout(() => {
+ state.retry();
+ }, 1000);
+ return () => {
+ 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);
+ }}
+ />
+ )}
+
+ <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>
+ <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>
+ );
+}