diff options
6 files changed, 458 insertions, 116 deletions
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index e5ff6cc26..553155ece 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -1310,10 +1310,10 @@ type Primitives = string | number | boolean; type RecursivePartial<T extends object> = { [P in keyof T]?: T[P] extends Array<infer U extends object> - ? Array<RecursivePartial<U>> - : T[P] extends Array<infer J extends Primitives> - ? Array<J> - : T[P] extends object - ? RecursivePartial<T[P]> - : T[P]; + ? Array<RecursivePartial<U>> + : T[P] extends Array<infer J extends Primitives> + ? Array<J> + : T[P] extends object + ? RecursivePartial<T[P]> + : T[P]; } & object; diff --git a/packages/taler-wallet-webextension/src/components/Modal.tsx b/packages/taler-wallet-webextension/src/components/Modal.tsx index 11fa72181..5553c72df 100644 --- a/packages/taler-wallet-webextension/src/components/Modal.tsx +++ b/packages/taler-wallet-webextension/src/components/Modal.tsx @@ -18,7 +18,7 @@ import { styled } from "@linaria/react"; import { ComponentChildren, h, VNode } from "preact"; import { ButtonHandler } from "../mui/handlers.js"; import closeIcon from "../svg/close_24px.inline.svg"; -import { Link, LinkPrimary, LinkWarning } from "./styled/index.js"; +import { Link } from "./styled/index.js"; interface Props { children: ComponentChildren; @@ -52,40 +52,43 @@ const Body = styled.div` export function Modal({ title, children, onClose }: Props): VNode { return ( - <FullSize onClick={onClose?.onClick}> - <div - onClick={(e) => e.stopPropagation()} - style={{ - background: "white", - width: 600, - height: "80%", - margin: "auto", - borderRadius: 8, - padding: 8, - // overflow: "scroll", - }} - > - <Header> - <div> - <h2>{title}</h2> - </div> - <Link onClick={onClose?.onClick}> - <div - style={{ - height: 24, - width: 24, - marginLeft: 4, - marginRight: 4, - // fill: "white", - }} - dangerouslySetInnerHTML={{ __html: closeIcon }} - /> - </Link> - </Header> - <hr /> + <div style={{ position: "fixed", top: 0, width: "100%", height: "100%" }}> - <Body onClick={(e: any) => e.stopPropagation()}>{children}</Body> - </div> - </FullSize> + <FullSize onClick={onClose?.onClick}> + <div + onClick={(e) => e.stopPropagation()} + style={{ + background: "white", + width: 600, + height: "80%", + margin: "auto", + borderRadius: 8, + padding: 8, + // overflow: "scroll", + }} + > + <Header> + <div> + <h2>{title}</h2> + </div> + <Link onClick={onClose?.onClick}> + <div + style={{ + height: 24, + width: 24, + marginLeft: 4, + marginRight: 4, + // fill: "white", + }} + dangerouslySetInnerHTML={{ __html: closeIcon }} + /> + </Link> + </Header> + <hr /> + + <Body onClick={(e: any) => e.stopPropagation()}>{children}</Body> + </div> + </FullSize> + </div> ); } diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx index a63ee97cb..8c55d1fc9 100644 --- a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx +++ b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx @@ -14,61 +14,420 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - AbsoluteTime, - Amounts, NotificationType, - Transaction, + ObservabilityEventType, + TalerErrorCode, + TalerErrorDetail, TransactionMajorState, + WalletNotification, + assertUnreachable } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { Fragment, h, JSX, VNode } from "preact"; -import { useEffect } from "preact/hooks"; -import { useBackendContext } from "../context/backend.js"; 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 { Avatar } from "../mui/Avatar.js"; -import { Grid } from "../mui/Grid.js"; -import { Typography } from "../mui/Typography.js"; -import Banner from "./Banner.js"; +import { Button } from "../mui/Button.js"; +import { Modal } from "./Modal.js"; import { Time } from "./Time.js"; +import { useSettings } from "../hooks/useSettings.js"; interface Props extends JSX.HTMLAttributes { } -/** - * this cache will save the tx from the previous render - */ -const cache = { tx: [] as Transaction[] }; -export function WalletActivity({ }: Props): VNode { - const api = useBackendContext(); - const state = useAsyncAsHook(() => - api.wallet.call(WalletApiOperation.GetTransactions, {}), +export function WalletActivity({ }: Props): VNode { + const { i18n } = useTranslationContext() + const [settings, updateSettings] = useSettings() + useEffect(() => { + document.body.style.marginBottom = "250px" + return () => { + document.body.style.marginBottom = "0px" + } + }) + const [table, setTable] = useState<"tasks" | "events" | "calls">("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 onClick={() => { + updateSettings("showWalletActivity", false) + }}> + close + </div> + </div> + <div> + <Button variant={table === "tasks" ? "contained" : "outlined"} + onClick={async () => { + setTable("tasks") + }} + > + <i18n.Translate>Tasks</i18n.Translate> + </Button> + <Button variant={table === "events" ? "contained" : "outlined"} + onClick={async () => { + setTable("events") + }} + > + <i18n.Translate>Events</i18n.Translate> + </Button> + + <Button variant={table === "calls" ? "contained" : "outlined"} + onClick={async () => { + setTable("calls") + }} + > + <i18n.Translate>Calls</i18n.Translate> + </Button> + </div> + {(function (): VNode { + switch (table) { + case "events": { + return <ObservavilityEventsTable /> + } + case "calls": { + return <WalletCallsTable /> + } + case "tasks": { + return <ActiveTasksTable /> + } + default: { + assertUnreachable(table) + } + } + })()} + </div> ); - const listenAllEvents = Array.from<NotificationType>({ length: 1 }); +} +export function WalletCallsTable({ }: {}): VNode { + return <div /> +} +const notifications: WalletNotification[] = [] +export function ObservavilityEventsTable({ }: {}): VNode { + const { i18n } = useTranslationContext() + const listenAllEvents = Array.from<NotificationType>({ length: 1 }); + listenAllEvents.includes = () => true + const api = useBackendContext(); + const [lastEvent, setLastEvent] = useState<Date>(new Date()) useEffect(() => { - return api.listener.onUpdateNotification( listenAllEvents, (notif) => { - console.log(notif) + return api.listener.onUpdateNotification(listenAllEvents, (notif) => { + notifications.unshift(notif) + setLastEvent(new Date()) }); }); + const [showError, setShowError] = useState<TalerErrorDetail>() + return <div> + {showError && <ErroDetailModal error={showError} onClose={(async () => { setShowError(undefined) })} />} + {notifications.map((not) => { + return ( + <details> + <summary>{not.type}</summary> + {(function () { + switch (not.type) { + case NotificationType.BalanceChange: { + return <Fragment> + <dt>Transaction</dt> + <dd> + <a title={not.hintTransactionId} href={Pages.balanceTransaction({ tid: not.hintTransactionId })}>{not.hintTransactionId.substring(0, 10)}</a> + </dd> + </Fragment> + } + case NotificationType.BackupOperationError: { + return <Fragment> + <dt>Error</dt> + <dd> + <a href="#" onClick={(e) => { e.preventDefault(); setShowError(not.error) }}>{TalerErrorCode[not.error.code]}</a> + </dd> + </Fragment> + } + case NotificationType.TransactionStateTransition: { + 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) => { + e.preventDefault(); setShowError({ + code: not.errorInfo!.code, + hint: not.errorInfo!.hint, + message: not.errorInfo!.message, + }) + }}>{TalerErrorCode[not.errorInfo!.code]}</a> + </dd> + </Fragment> : undefined} + <dt>Experimental</dt> + <dd> + <pre> + {JSON.stringify(not.experimentalUserData, undefined, 2)} + </pre> + </dd> + + </Fragment> + } + case NotificationType.ExchangeStateTransition: { + return <Fragment> + <dt>Exchange</dt> + <dd> + {not.exchangeBaseUrl} + </dd> + <dt>Entry status</dt> + <dd> + {not.newExchangeState.exchangeEntryStatus} + </dd> + <dt>Update status</dt> + <dd> + {not.newExchangeState.exchangeUpdateStatus} + </dd> + <dt>Tos status</dt> + <dd> + {not.newExchangeState.tosStatus} + </dd> + </Fragment> + } + case NotificationType.TaskObservabilityEvent: { + return <Fragment> + <dt>Task</dt> + <dd> + {not.taskId} + </dd> + <dt>Event</dt> + <dd> + {not.event.type} + </dd> + {(function () { + switch (not.event.type) { + case ObservabilityEventType.HttpFetchStart: + case ObservabilityEventType.HttpFetchFinishError: + case ObservabilityEventType.HttpFetchFinishSuccess: { + return <Fragment> + <dt>Request</dt> + <dd>{not.event.url}</dd> + </Fragment> + } + case ObservabilityEventType.DbQueryStart: + case ObservabilityEventType.DbQueryFinishSuccess: + case ObservabilityEventType.DbQueryFinishError: { + return <Fragment> + <dt>Location</dt> + <dd>{not.event.location}</dd> + <dt>Name</dt> + <dd>{not.event.name}</dd> + </Fragment> + } + + case ObservabilityEventType.TaskStart: + case ObservabilityEventType.TaskStop: + case ObservabilityEventType.DeclareTaskDependency: + case ObservabilityEventType.TaskReset: { + return <Fragment> + <dt>Task</dt> + <dd>{not.event.taskId}</dd> + </Fragment> + } + case ObservabilityEventType.ShepherdTaskResult: { + return <Fragment> + <dt>result</dt> + <dd>{not.event.resultType}</dd> + </Fragment> - const transactions = - !state || state.hasError - ? cache.tx - : state.response.transactions.filter( - (t) => t.txState.major === TransactionMajorState.Pending, + } + case ObservabilityEventType.CryptoStart: + case ObservabilityEventType.CryptoFinishSuccess: + case ObservabilityEventType.CryptoFinishError: { + return <Fragment> + <dt>operation</dt> + <dd>{not.event.operation}</dd> + </Fragment> + } + case ObservabilityEventType.RequestStart: + case ObservabilityEventType.RequestFinishSuccess: + case ObservabilityEventType.RequestFinishError: { + return <Fragment /> + } + } + })()} + </Fragment> + } + case NotificationType.RequestObservabilityEvent: { + return <Fragment> + <dt>Operation</dt> + <dd> + {not.operation} + </dd> + <dt>Request</dt> + <dd> + {not.requestId} + </dd> + <dt>Event type</dt> + <dd> + {not.event.type} + </dd> + {(function () { + switch (not.event.type) { + case ObservabilityEventType.HttpFetchStart: + case ObservabilityEventType.HttpFetchFinishError: + case ObservabilityEventType.HttpFetchFinishSuccess: { + return <Fragment> + <dt>Request</dt> + <dd>{not.event.url}</dd> + </Fragment> + } + case ObservabilityEventType.DbQueryStart: + case ObservabilityEventType.DbQueryFinishSuccess: + case ObservabilityEventType.DbQueryFinishError: { + return <Fragment> + <dt>Location</dt> + <dd>{not.event.location}</dd> + <dt>Name</dt> + <dd>{not.event.name}</dd> + </Fragment> + } + + case ObservabilityEventType.TaskStart: + case ObservabilityEventType.TaskStop: + case ObservabilityEventType.DeclareTaskDependency: + case ObservabilityEventType.TaskReset: { + return <Fragment> + <dt>Task</dt> + <dd>{not.event.taskId}</dd> + </Fragment> + } + case ObservabilityEventType.ShepherdTaskResult: { + return <Fragment> + <dt>result</dt> + <dd>{not.event.resultType}</dd> + </Fragment> + + } + case ObservabilityEventType.CryptoStart: + case ObservabilityEventType.CryptoFinishSuccess: + case ObservabilityEventType.CryptoFinishError: { + return <Fragment> + <dt>operation</dt> + <dd>{not.event.operation}</dd> + </Fragment> + } + case ObservabilityEventType.RequestStart: + case ObservabilityEventType.RequestFinishSuccess: + case ObservabilityEventType.RequestFinishError: { + return <Fragment /> + } + } + })()} + + </Fragment> + } + } + })()} + + </details> ); + })} + </div > +} - if (state && !state.hasError) { - cache.tx = transactions; - } - if (!transactions.length) { - return <Fragment />; - } - return ( - <div> - this is shown below - </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> + {JSON.stringify(error, undefined, 2)} + </pre> + </Modal> } + +export function ActiveTasksTable({ }: {}): VNode { + const { i18n } = useTranslationContext() + const listenAllEvents = Array.from<NotificationType>({ length: 1 }); + listenAllEvents.includes = () => true + const api = useBackendContext(); + const state = useAsyncAsHook(() => + api.wallet.call(WalletApiOperation.GetActiveTasks, {}), + ); + const [showError, setShowError] = useState<TalerErrorDetail>() + const tasks = state && !state.hasError ? state.response.tasks : []; + useEffect(() => { + return api.listener.onUpdateNotification(listenAllEvents, (notif) => { + state?.retry() + }); + }); + return <Fragment> + {showError && <ErroDetailModal error={showError} onClose={(async () => { setShowError(undefined) })} />} + <table> + <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 diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx index 4fafc73ad..62a519f06 100644 --- a/packages/taler-wallet-webextension/src/wallet/Application.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx @@ -82,6 +82,7 @@ import { SettingsPage } from "./Settings.js"; import { TransactionPage } from "./Transaction.js"; import { WelcomePage } from "./Welcome.js"; import { WalletActivity } from "../components/WalletActivity.js"; +import { EnabledBySettings } from "../components/EnabledBySettings.js"; export function Application(): VNode { const { i18n } = useTranslationContext(); @@ -511,6 +512,9 @@ export function Application(): VNode { component={() => <Redirect to={Pages.balanceHistory({})} />} /> </Router> + <EnabledBySettings name="showWalletActivity"> + <WalletActivity /> + </EnabledBySettings> </IoCProviderForRuntime> </TranslationProvider> ); @@ -610,7 +614,6 @@ function WalletTemplate({ {children} </AlertProvider> </WalletBox> - <WalletActivity /> </Fragment> ); } diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx index adb114862..cdd3994d7 100644 --- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx @@ -46,6 +46,7 @@ import { Paper } from "../mui/Paper.js"; import { TextField } from "../mui/TextField.js"; import { Pages } from "../NavigationBar.js"; import { CoinInfo } from "@gnu-taler/taler-wallet-core/dbless"; +import { ActiveTasksTable } from "../components/WalletActivity.js"; type CoinsInfo = CoinDumpJson["coins"]; type CalculatedCoinfInfo = { @@ -95,27 +96,23 @@ export function DeveloperPage({ }: Props): VNode { const [settings, updateSettings] = useSettings(); const { safely } = useAlertContext(); - // const hook = useAsyncAsHook(() => - // api.wallet.call(WalletApiOperation.ListExchanges, {}), - // ); const listenAllEvents = Array.from<NotificationType>({ length: 1 }); + listenAllEvents.includes = () => true const hook = useAsyncAsHook(async () => { const list = await api.wallet.call(WalletApiOperation.ListExchanges, {}); const version = await api.wallet.call(WalletApiOperation.GetVersion, {}); - const tasks = await api.wallet.call( - WalletApiOperation.GetActiveTasks, - {}, - ); const coins = await api.wallet.call(WalletApiOperation.DumpCoins, {}); - return { exchanges: list.exchanges, version, coins, tasks:tasks.tasks }; + return { exchanges: list.exchanges, version, coins }; }); const exchangeList = hook && !hook.hasError ? hook.response.exchanges : []; const coins = hook && !hook.hasError ? hook.response.coins.coins : []; - const tasks = hook && !hook.hasError ? hook.response.tasks : []; useEffect(() => { - return api.listener.onUpdateNotification(listenAllEvents, hook?.retry); + return api.listener.onUpdateNotification(listenAllEvents, (ev) => { + console.log("event", ev) + return hook?.retry() + }); }); const currencies: { [ex: string]: string } = {}; @@ -145,7 +142,6 @@ export function DeveloperPage({ }: Props): VNode { ); const [tagName, setTagName] = useState(""); - const [exchangeURL, setExchangeURL] = useState(""); const [logLevel, setLogLevel] = useState("info"); return ( <div> @@ -322,7 +318,7 @@ export function DeveloperPage({ }: Props): VNode { return ( <tr key={idx}> <td> - <a href={!uri? undefined: Pages.defaultCta({ uri })}> + <a href={!uri ? undefined : Pages.defaultCta({ uri })}> {e.scopeInfo ? `${e.scopeInfo.currency} (${e.scopeInfo.type === ScopeType.Global ? "global" : "regional"})` : e.currency} </a> </td> @@ -545,31 +541,9 @@ export function DeveloperPage({ }: Props): VNode { ); })} <br /> - {tasks && tasks.length > 0 && ( - <Fragment> - <p> - <i18n.Translate>Pending operations</i18n.Translate> - </p> - <dl> - {tasks.map((o) => { - return ( - <NotifyUpdateFadeOut key={hashObjectId(o)}> - <dt> - {o.id}{" "} - <Time - timestamp={o.nextTry} - format="yy/MM/dd HH:mm:ss" - /> - </dt> - <dd> - <pre>{JSON.stringify(o, undefined, 2)}</pre> - </dd> - </NotifyUpdateFadeOut> - ); - })} - </dl> - </Fragment> - )} + <NotifyUpdateFadeOut> + <ActiveTasksTable /> + </NotifyUpdateFadeOut> </div> ); } diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index 051048e81..2753d57b4 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -277,6 +277,9 @@ async function reinitWallet(): Promise<void> { timer, cryptoWorker, { + testing: { + emitObservabilityEvents: true, + }, features: { allowHttp: settings.walletAllowHttp, }, |