/*
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
*/
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 (
{
updateSettings("showWalletActivity", false);
}}
>
close
{
setTable("tasks");
}}
>
Tasks
{
setTable("events");
}}
>
Events
{(function (): VNode {
switch (table) {
case "events": {
return
;
}
case "tasks": {
return
;
}
default: {
assertUnreachable(table);
}
}
})()}
);
}
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 ;
const not = events[0];
if (not.type !== NotificationType.BalanceChange) return ;
return (
Transaction
{not.hintTransactionId.substring(0, 10)}
);
}
function ShowBackupOperationError({ events, onClick }: MoreInfoPRops): VNode {
if (!events.length) return ;
const not = events[0];
if (not.type !== NotificationType.BackupOperationError) return ;
return (
Error
{
e.preventDefault();
const error = not.error;
onClick(
Code
{TalerErrorCode[error.code]} ({error.code})
Hint
{error.hint ?? "--"}
Time
{JSON.stringify(error, undefined, 2)}
,
);
}}
>
{TalerErrorCode[not.error.code]}
);
}
function ShowTransactionStateTransition({
events,
onClick,
}: MoreInfoPRops): VNode {
if (!events.length) return ;
const not = events[0];
if (not.type !== NotificationType.TransactionStateTransition)
return ;
return (
Old state
{not.oldTxState.major} - {not.oldTxState.minor ?? ""}
New state
{not.newTxState.major} - {not.newTxState.minor ?? ""}
Transaction
{not.transactionId.substring(0, 10)}
{not.errorInfo ? (
Error
{
if (!not.errorInfo) return;
e.preventDefault();
const error = not.errorInfo;
onClick(
Code
{TalerErrorCode[error.code]} ({error.code})
Hint
{error.hint ?? "--"}
Message
{error.message ?? "--"}
,
);
}}
>
{TalerErrorCode[not.errorInfo.code]}
) : undefined}
Experimental
{JSON.stringify(not.experimentalUserData, undefined, 2)}
);
}
function ShowExchangeStateTransition({
events,
onClick,
}: MoreInfoPRops): VNode {
if (!events.length) return ;
const not = events[0];
if (not.type !== NotificationType.ExchangeStateTransition)
return ;
return (
Exchange
{not.exchangeBaseUrl}
{not.oldExchangeState &&
not.newExchangeState.exchangeEntryStatus !==
not.oldExchangeState?.exchangeEntryStatus && (
Entry status
from {not.oldExchangeState.exchangeEntryStatus} to{" "}
{not.newExchangeState.exchangeEntryStatus}
)}
{not.oldExchangeState &&
not.newExchangeState.exchangeUpdateStatus !==
not.oldExchangeState?.exchangeUpdateStatus && (
Update status
from {not.oldExchangeState.exchangeUpdateStatus} to{" "}
{not.newExchangeState.exchangeUpdateStatus}
)}
{not.oldExchangeState &&
not.newExchangeState.tosStatus !== not.oldExchangeState?.tosStatus && (
Tos status
from {not.oldExchangeState.tosStatus} to{" "}
{not.newExchangeState.tosStatus}
)}
);
}
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 ;
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 (
);
});
return (
Event
Info
Start
End
{asd}
);
}
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 (
{
e.preventDefault();
onClick(
{JSON.stringify({ event: notif, prev }, undefined, 2)}
,
);
}}
>
{title}
{notif.event.url}{" "}
{prev?.event.type ===
ObservabilityEventType.HttpFetchFinishSuccess ? (
`(${prev.event.status})`
) : prev?.event.type ===
ObservabilityEventType.HttpFetchFinishError ? (
{
e.preventDefault();
if (
prev.event.type !==
ObservabilityEventType.HttpFetchFinishError
)
return;
const error = prev.event.error;
onClick(
Code
{TalerErrorCode[error.code]} ({error.code})
Hint
{error.hint ?? "--"}
Time
{JSON.stringify(error, undefined, 2)}
,
);
}}
>
fail
) : undefined}
{" "}
{" "}
);
}
case ObservabilityEventType.DbQueryStart:
case ObservabilityEventType.DbQueryFinishSuccess:
case ObservabilityEventType.DbQueryFinishError: {
return (
{
e.preventDefault();
onClick(
{JSON.stringify({ event: notif, prev }, undefined, 2)}
,
);
}}
>
{title}
{notif.event.location} {notif.event.name}
);
}
case ObservabilityEventType.TaskStart:
case ObservabilityEventType.TaskStop:
case ObservabilityEventType.DeclareTaskDependency:
case ObservabilityEventType.TaskReset: {
return (
{
e.preventDefault();
onClick(
{JSON.stringify({ event: notif, prev }, undefined, 2)}
,
);
}}
>
{title}
{notif.event.taskId}
);
}
case ObservabilityEventType.ShepherdTaskResult: {
return (
{
e.preventDefault();
onClick(
{JSON.stringify({ event: notif, prev }, undefined, 2)}
,
);
}}
>
{title}
{notif.event.resultType}
);
}
case ObservabilityEventType.CryptoStart:
case ObservabilityEventType.CryptoFinishSuccess:
case ObservabilityEventType.CryptoFinishError: {
return (
{
e.preventDefault();
onClick(
{JSON.stringify({ event: notif, prev }, undefined, 2)}
,
);
}}
>
{title}
{notif.event.operation}
);
}
case ObservabilityEventType.RequestStart:
case ObservabilityEventType.RequestFinishSuccess:
case ObservabilityEventType.RequestFinishError: {
return (
{
e.preventDefault();
onClick(
{JSON.stringify({ event: notif, prev }, undefined, 2)}
,
);
}}
>
{title}
{notif.event.type}
);
}
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([]);
const [showDetails, setShowDetails] = useState();
useEffect(() => {
let lastTimeout: ReturnType;
function periodicRefresh() {
refresh(api, setNotifications);
lastTimeout = setTimeout(() => {
periodicRefresh();
}, 1000);
//clear on unload
return () => {
clearTimeout(lastTimeout);
};
}
return periodicRefresh();
}, [1]);
return (
{
api.background.call("clearNotifications", undefined).then((d) => {
refresh(api, setNotifications);
});
}}
>
clear
{showDetails && (
{
setShowDetails(undefined);
}) as any,
}}
>
{showDetails}
)}
{notifications.map((not) => {
return (
{
setShowDetails(details);
}}
/>
);
})}
);
}
function ErroDetailModal({
error,
onClose,
}: {
error: TalerErrorDetail;
onClose: () => void;
}): VNode {
return (
Code
{TalerErrorCode[error.code]} ({error.code})
Hint
{error.hint ?? "--"}
Time
{JSON.stringify(error, undefined, 2)}
);
}
export function ActiveTasksTable({}: {}): VNode {
const { i18n } = useTranslationContext();
const api = useBackendContext();
const state = useAsyncAsHook(() => {
return api.wallet.call(WalletApiOperation.GetActiveTasks, {});
});
const [showError, setShowError] = useState();
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({ length: 1 });
// listenAllEvents.includes = () => true
// useEffect(() => {
// return api.listener.onUpdateNotification(listenAllEvents, (notif) => {
// state?.retry()
// });
// });
return (
{showError && (
{
setShowError(undefined);
}}
/>
)}
);
}