/*
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"
}
})();
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}
}
}
}
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: 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) })} />}
}