/*
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, 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 { TextField } from "../mui/TextField.js";
import { SafeHandler } from "../mui/handlers.js";
import { WxApiType } from "../wxApi.js";
import { WalletActivityTrack } from "../wxBackend.js";
import { Modal } from "./Modal.js";
import { Time } from "./Time.js";
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);
useEffect(() => {
document.body.style.marginBottom = `${
collapsed ? CLOSE_ACTIVITY_HEIGHT_PX : OPEN_ACTIVITY_HEIGHT_PX
}px`;
return () => {
document.body.style.marginBottom = "0px";
};
}, [collapsed]);
const [table, setTable] = useState<"tasks" | "events">("events");
if (collapsed) {
return (
{
setCollcapsed(!collapsed);
}}
>
Click here to open the wallet activity tab.
);
}
return (
{
setCollcapsed(!collapsed);
}}
>
{
setTable("events");
}}
>
Events
{
setTable("tasks");
}}
>
Active tasks
{
updateSettings("showWalletActivity", false);
}}
>
Close
{(function (): VNode {
switch (table) {
case "events": {
return
;
}
case "tasks": {
return
;
}
default: {
assertUnreachable(table);
}
}
})()}
);
}
interface MoreInfoPRops {
events: (WalletNotification & { when: AbsoluteTime })[];
onClick: (content: VNode) => void;
}
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 }: 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, idx) => {
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 refresh(
api: WxApiType,
onUpdate: (list: WalletActivityTrack[]) => void,
filter: string,
) {
api.background
.call("getNotifications", { filter })
.then((notif) => {
onUpdate(notif);
})
.catch((error) => {
console.log(error);
});
}
export function ObservabilityEventsTable(): VNode {
const { i18n } = useTranslationContext();
const api = useBackendContext();
const [notifications, setNotifications] = useState([]);
const [showDetails, setShowDetails] = useState();
const [filter, onChangeFilter] = useState("");
useEffect(() => {
let lastTimeout: ReturnType;
function periodicRefresh() {
refresh(api, setNotifications, filter);
lastTimeout = setTimeout(() => {
periodicRefresh();
}, 1000);
return () => {
clearTimeout(lastTimeout);
};
}
return periodicRefresh();
}, [filter]);
return (
{
api.background.call("clearNotifications", undefined).then(() => {
refresh(api, setNotifications, filter);
});
}}
>
clear
{showDetails && (
{
setShowDetails(undefined);
}) as SafeHandler,
}}
>
{showDetails}
)}
{notifications.map((not) => {
return (
{(() => {
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);
}
}
})()}
{(() => {
switch (not.type) {
case NotificationType.BalanceChange: {
return (
{
setShowDetails(details);
}}
/>
);
}
case NotificationType.BackupOperationError: {
return (
{
setShowDetails(details);
}}
/>
);
}
case NotificationType.TransactionStateTransition: {
return (
{
setShowDetails(details);
}}
/>
);
}
case NotificationType.ExchangeStateTransition: {
return (
{
setShowDetails(details);
}}
/>
);
}
case NotificationType.Idle: {
return not implemented
;
}
case NotificationType.TaskObservabilityEvent: {
return (
{
setShowDetails(details);
}}
/>
);
}
case NotificationType.RequestObservabilityEvent: {
return (
{
setShowDetails(details);
}}
/>
);
}
case NotificationType.WithdrawalOperationTransition: {
return not implemented
;
}
}
})()}
);
})}
);
}
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]);
return (
{showError && (
{
setShowError(undefined);
}}
/>
)}
);
}