diff options
4 files changed, 395 insertions, 294 deletions
diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx index 69a2c0675..41b0c5c76 100644 --- a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx +++ b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx @@ -15,6 +15,7 @@ */ import { AbsoluteTime, + ExchangeStateTransitionNotification, NotificationType, ObservabilityEventType, RequestProgressNotification, @@ -26,38 +27,76 @@ import { } 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 { 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 { SafeHandler } from "../mui/handlers.js"; import { WxApiType } from "../wxApi.js"; import { Modal } from "./Modal.js"; import { Time } from "./Time.js"; +import { TextField } from "../mui/TextField.js"; +import { WalletActivityTrack } from "../wxBackend.js"; -interface Props extends JSX.HTMLAttributes {} +const OPEN_ACTIVITY_HEIGHT_PX = 250; +const CLOSE_ACTIVITY_HEIGHT_PX = 40; -export function WalletActivity({}: Props): VNode { +export function WalletActivity(): VNode { const { i18n } = useTranslationContext(); - const [settings, updateSettings] = useSettings(); - const api = useBackendContext(); + const [, updateSettings] = useSettings(); + + const [collapsed, setCollcapsed] = useState(true); + useEffect(() => { - document.body.style.marginBottom = "250px"; + document.body.style.marginBottom = `${ + collapsed ? CLOSE_ACTIVITY_HEIGHT_PX : OPEN_ACTIVITY_HEIGHT_PX + }px`; return () => { document.body.style.marginBottom = "0px"; }; - }); - const [table, setTable] = useState<"tasks" | "events">("tasks"); + }, [collapsed]); + + const [table, setTable] = useState<"tasks" | "events">("events"); + if (collapsed) { + return ( + <div + style={{ + position: "fixed", + bottom: 0, + background: "lightgrey", + zIndex: 1, + height: CLOSE_ACTIVITY_HEIGHT_PX, + overflowY: "scroll", + width: "100%", + }} + onClick={() => { + setCollcapsed(!collapsed); + }} + > + <div + style={{ + display: "flex", + justifyContent: "space-around", + marginTop: 10, + cursor: "pointer", + }} + > + click here to open + </div> + </div> + ); + } return ( <div style={{ position: "fixed", bottom: 0, - background: "white", + background: "lightgrey", zIndex: 1, - height: 250, + height: OPEN_ACTIVITY_HEIGHT_PX, overflowY: "scroll", width: "100%", }} @@ -65,23 +104,22 @@ export function WalletActivity({}: Props): VNode { <div style={{ display: "flex", - justifyContent: "space-between", - float: "right", + justifyContent: "space-around", + cursor: "pointer", + }} + onClick={() => { + setCollcapsed(!collapsed); }} > - <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 === "events" ? "contained" : "outlined"} + style={{ margin: 4 }} + onClick={async () => { + setTable("events"); + }} + > + <i18n.Translate>Events</i18n.Translate> + </Button> <Button variant={table === "tasks" ? "contained" : "outlined"} style={{ margin: 4 }} @@ -89,31 +127,38 @@ export function WalletActivity({}: Props): VNode { setTable("tasks"); }} > - <i18n.Translate>Tasks</i18n.Translate> + <i18n.Translate>Active tasks</i18n.Translate> </Button> + <Button - variant={table === "events" ? "contained" : "outlined"} + variant="outlined" style={{ margin: 4 }} onClick={async () => { - setTable("events"); + updateSettings("showWalletActivity", false); }} > - <i18n.Translate>Events</i18n.Translate> + <i18n.Translate>Close</i18n.Translate> </Button> </div> - {(function (): VNode { - switch (table) { - case "events": { - return <ObservabilityEventsTable />; - } - case "tasks": { - return <ActiveTasksTable />; - } - default: { - assertUnreachable(table); + <div + style={{ + backgroundColor: "white", + }} + > + {(function (): VNode { + switch (table) { + case "events": { + return <ObservabilityEventsTable />; + } + case "tasks": { + return <ActiveTasksTable />; + } + default: { + assertUnreachable(table); + } } - } - })()} + })()} + </div> </div> ); } @@ -122,21 +167,6 @@ 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 />; @@ -267,10 +297,7 @@ function ShowTransactionStateTransition({ </Fragment> ); } -function ShowExchangeStateTransition({ - events, - onClick, -}: MoreInfoPRops): VNode { +function ShowExchangeStateTransition({ events }: MoreInfoPRops): VNode { if (!events.length) return <Fragment />; const not = events[0]; if (not.type !== NotificationType.ExchangeStateTransition) @@ -323,7 +350,7 @@ type ObservaNotifWithTime = ( }; function ShowObservabilityEvent({ events, onClick }: MoreInfoPRops): VNode { // let prev: ObservaNotifWithTime; - const asd = events.map((not) => { + const asd = events.map((not, idx) => { if ( not.type !== NotificationType.RequestObservabilityEvent && not.type !== NotificationType.TaskObservabilityEvent @@ -364,7 +391,12 @@ function ShowObservabilityEvent({ events, onClick }: MoreInfoPRops): VNode { })(); return ( - <ShowObervavilityDetails title={title} notif={not} onClick={onClick} /> + <ShowObervavilityDetails + key={idx} + title={title} + notif={not} + onClick={onClick} + /> ); }); return ( @@ -673,235 +705,64 @@ function ShowObervavilityDetails({ } } -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) { +function refresh( + api: WxApiType, + onUpdate: (list: WalletActivityTrack[]) => void, + filter: string, +) { api.background - .call("getNotifications", undefined) + .call("getNotifications", { filter }) .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); + onUpdate(notif); }) .catch((error) => { console.log(error); }); } -export function ObservabilityEventsTable({}: {}): VNode { +export function ObservabilityEventsTable(): VNode { const { i18n } = useTranslationContext(); const api = useBackendContext(); - const [notifications, setNotifications] = useState<Notif[]>([]); + const [notifications, setNotifications] = useState<WalletActivityTrack[]>([]); const [showDetails, setShowDetails] = useState<VNode>(); + const [filter, onChangeFilter] = useState(""); useEffect(() => { let lastTimeout: ReturnType<typeof setTimeout>; function periodicRefresh() { - refresh(api, setNotifications); + refresh(api, setNotifications, filter); lastTimeout = setTimeout(() => { periodicRefresh(); }, 1000); - //clear on unload return () => { clearTimeout(lastTimeout); }; } return periodicRefresh(); - }, [1]); + }, [filter]); return ( <div> <div style={{ display: "flex", justifyContent: "space-between" }}> + <TextField + label="Filter" + variant="outlined" + value={filter} + onChange={onChangeFilter} + /> <div - style={{ padding: 4, margin: 2, border: "solid 1px black" }} + style={{ + padding: 4, + margin: 2, + border: "solid 1px black", + alignSelf: "center", + }} onClick={() => { - api.background.call("clearNotifications", undefined).then((d) => { - refresh(api, setNotifications); + api.background.call("clearNotifications", undefined).then(() => { + refresh(api, setNotifications, filter); }); }} > @@ -914,7 +775,7 @@ export function ObservabilityEventsTable({}: {}): VNode { onClose={{ onClick: (async () => { setShowDetails(undefined); - }) as any, + }) as SafeHandler<void>, }} > {showDetails} @@ -932,7 +793,40 @@ export function ObservabilityEventsTable({}: {}): VNode { padding: 4, }} > - <div style={{ padding: 4 }}>{not.description}</div> + <div style={{ padding: 4 }}> + {(() => { + 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); + } + } + })()} + </div> <div style={{ padding: 4 }}> <Time timestamp={not.start} format="yyyy/MM/dd HH:mm:ss" /> </div> @@ -941,12 +835,76 @@ export function ObservabilityEventsTable({}: {}): VNode { </div> </div> </summary> - <not.MoreInfo - events={not.events} - onClick={(details) => { - setShowDetails(details); - }} - /> + {(() => { + switch (not.type) { + case NotificationType.BalanceChange: { + return ( + <ShowBalanceChange + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.BackupOperationError: { + return ( + <ShowBackupOperationError + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.TransactionStateTransition: { + return ( + <ShowTransactionStateTransition + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.ExchangeStateTransition: { + return ( + <ShowExchangeStateTransition + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.Idle: { + return <div>not implemented</div>; + } + case NotificationType.TaskObservabilityEvent: { + return ( + <ShowObservabilityEvent + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.RequestObservabilityEvent: { + return ( + <ShowObservabilityEvent + events={not.events} + onClick={(details) => { + setShowDetails(details); + }} + /> + ); + } + case NotificationType.WithdrawalOperationTransition: { + return <div>not implemented</div>; + } + } + })()} </details> ); })} @@ -965,7 +923,7 @@ function ErroDetailModal({ <Modal title="Full detail" onClose={{ - onClick: onClose as any, + onClick: onClose as SafeHandler<void>, }} > <dl> @@ -987,7 +945,7 @@ function ErroDetailModal({ ); } -export function ActiveTasksTable({}: {}): VNode { +export function ActiveTasksTable(): VNode { const { i18n } = useTranslationContext(); const api = useBackendContext(); const state = useAsyncAsHook(() => { @@ -1006,13 +964,6 @@ export function ActiveTasksTable({}: {}): VNode { }; }, [tasks]); - // const listenAllEvents = Array.from<NotificationType>({ length: 1 }); - // listenAllEvents.includes = () => true - // useEffect(() => { - // return api.listener.onUpdateNotification(listenAllEvents, (notif) => { - // state?.retry() - // }); - // }); return ( <Fragment> {showError && ( @@ -1051,7 +1002,7 @@ export function ActiveTasksTable({}: {}): VNode { {tasks.map((task) => { const [type, id] = task.taskId.split(":"); return ( - <tr> + <tr key={id}> <td>{type}</td> <td title={id}>{id.substring(0, 10)}</td> <td> diff --git a/packages/taler-wallet-webextension/src/mui/Button.tsx b/packages/taler-wallet-webextension/src/mui/Button.tsx index 1af281d42..12a4d91ea 100644 --- a/packages/taler-wallet-webextension/src/mui/Button.tsx +++ b/packages/taler-wallet-webextension/src/mui/Button.tsx @@ -371,7 +371,11 @@ function ButtonBase({ ); } return ( - <button onClick={doClick} class={classNames} {...rest}> + <button onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + doClick(); + }} class={classNames} {...rest}> {children} </button> ); diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index 195efecd4..4394a982f 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -46,6 +46,7 @@ import { MessageFromFrontendWallet, } from "./platform/api.js"; import { platform } from "./platform/foreground.js"; +import { WalletActivityTrack } from "./wxBackend.js"; /** * @@ -74,8 +75,10 @@ export interface BackgroundOperations { response: void; }; getNotifications: { - request: void; - response: WalletEvent[]; + request: { + filter: string; + }; + response: WalletActivityTrack[]; }; clearNotifications: { request: void; diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index 008f80c57..5fa255f5d 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -25,7 +25,6 @@ */ import { AbsoluteTime, - BalanceFlag, LogLevel, Logger, NotificationType, @@ -34,14 +33,13 @@ import { TalerError, TalerErrorCode, TalerErrorDetail, - TransactionMajorState, TransactionMinorState, WalletNotification, getErrorDetailFromException, makeErrorDetail, openPromise, setGlobalLogLevelFromString, - setLogLevelFromString, + setLogLevelFromString } from "@gnu-taler/taler-util"; import { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; import { @@ -55,11 +53,11 @@ import { exportDb, importDb, } from "@gnu-taler/taler-wallet-core"; +import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser"; import { MessageFromFrontend, MessageResponse } from "./platform/api.js"; import { platform } from "./platform/background.js"; import { ExtensionOperations } from "./taler-wallet-interaction-loader.js"; -import { BackgroundOperations, WalletEvent } from "./wxApi.js"; -import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser"; +import { BackgroundOperations } from "./wxApi.js"; /** * Currently active wallet instance. Might be unloaded and @@ -92,14 +90,162 @@ async function resetDb(): Promise<void> { await reinitWallet(); } +export type WalletActivityTrack = { + id: number; + events: (WalletNotification & {when: AbsoluteTime})[]; + start: AbsoluteTime; + type: NotificationType; + end: AbsoluteTime; + groupId: string; +}; + +let counter = 0; +function getUniqueId(): number { + return counter++; +} + //FIXME: maybe circular buffer -const notifications: WalletEvent[] = []; -async function getNotifications(): Promise<WalletEvent[]> { - return notifications; +const activity: WalletActivityTrack[] = []; + +function addNewWalletActivityNotification(list: WalletActivityTrack[], n: WalletNotification) { + const start = AbsoluteTime.now(); + const ev = {...n, when:start}; + switch (n.type) { + case NotificationType.BalanceChange: { + const groupId = `${n.type}:${n.hintTransactionId}`; + const found = list.find((a)=>a.groupId === groupId) + if (found) { + found.end = start; + found.events.unshift(ev) + return; + } + list.push({ + id: getUniqueId(), + type: n.type, + start, + end: AbsoluteTime.never(), + events: [ev], + groupId, + }); + return; + } + case NotificationType.BackupOperationError: { + const groupId = ""; + list.push({ + id: getUniqueId(), + type: n.type, + start, + end: AbsoluteTime.never(), + events: [ev], + groupId, + }); + return; + } + case NotificationType.TransactionStateTransition: { + const groupId = `${n.type}:${n.transactionId}`; + const found = list.find((a)=>a.groupId === groupId) + if (found) { + found.end = start; + found.events.unshift(ev) + return; + } + list.push({ + id: getUniqueId(), + type: n.type, + start, + end: AbsoluteTime.never(), + events: [ev], + groupId, + }); + return; + } + case NotificationType.WithdrawalOperationTransition: { + return; + } + case NotificationType.ExchangeStateTransition: { + const groupId = `${n.type}:${n.exchangeBaseUrl}`; + const found = list.find((a)=>a.groupId === groupId) + if (found) { + found.end = start; + found.events.unshift(ev) + return; + } + list.push({ + id: getUniqueId(), + type: n.type, + start, + end: AbsoluteTime.never(), + events: [ev], + groupId, + }); + return; + } + case NotificationType.Idle: { + const groupId = ""; + list.push({ + id: getUniqueId(), + type: n.type, + start, + end: AbsoluteTime.never(), + events: [ev], + groupId, + }); + return; + } + case NotificationType.TaskObservabilityEvent: { + const groupId = `${n.type}:${n.taskId}`; + const found = list.find((a)=>a.groupId === groupId) + if (found) { + found.end = start; + found.events.unshift(ev) + return; + } + list.push({ + id: getUniqueId(), + type: n.type, + start, + end: AbsoluteTime.never(), + events: [ev], + groupId, + }); + return; + } + case NotificationType.RequestObservabilityEvent: { + const groupId = `${n.type}:${n.operation}:${n.requestId}`; + const found = list.find((a)=>a.groupId === groupId) + if (found) { + found.end = start; + found.events.unshift(ev) + return; + } + list.push({ + id: getUniqueId(), + type: n.type, + start, + end: AbsoluteTime.never(), + events: [ev], + groupId, + }); + return; + } + } +} + +async function getNotifications({ + filter, +}: { + filter: string; +}): Promise<WalletActivityTrack[]> { + if (!filter) return activity; + + const rg = new RegExp(`.*${filter}.*`); + return activity.filter((event) => { + return rg.test(event.groupId.toLowerCase()); + }); } async function clearNotifications(): Promise<void> { - notifications.splice(0, notifications.length); + activity.splice(0, activity.length); } async function runGarbageCollector(): Promise<void> { @@ -327,10 +473,7 @@ async function reinitWallet(): Promise<void> { } wallet.addNotificationListener((message) => { if (settings.showWalletActivity) { - notifications.push({ - notification: message, - when: AbsoluteTime.now(), - }); + addNewWalletActivityNotification(activity, message); } processWalletNotification(message); @@ -394,7 +537,7 @@ async function updateIconBasedOnBalance() { let showAlert = false; for (const b of balance.balances) { if (b.flags.length > 0) { - console.log("b.flags", JSON.stringify(b.flags)) + console.log("b.flags", JSON.stringify(b.flags)); showAlert = true; break; } |