diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/components/WalletActivity.tsx')
-rw-r--r-- | packages/taler-wallet-webextension/src/components/WalletActivity.tsx | 505 |
1 files changed, 228 insertions, 277 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> |