diff options
author | Sebastian <sebasjm@gmail.com> | 2024-02-29 15:45:22 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-02-29 15:45:22 -0300 |
commit | 206780bb0ee763bcf50a3f4f9f78579a8adcdb3a (patch) | |
tree | 7d332c27a7c64f676d6ac7101af125a806cf6318 /packages | |
parent | 922d5b4d7aeed3f7c2f9eab1482f60d2fa50f234 (diff) |
observe UI, WIP
Diffstat (limited to 'packages')
8 files changed, 641 insertions, 246 deletions
diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx index 8c55d1fc9..41932f143 100644 --- a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx +++ b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx @@ -14,11 +14,13 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { + AbsoluteTime, NotificationType, ObservabilityEventType, + RequestProgressNotification, TalerErrorCode, TalerErrorDetail, - TransactionMajorState, + TaskProgressNotification, WalletNotification, assertUnreachable } from "@gnu-taler/taler-util"; @@ -29,10 +31,10 @@ 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 { Modal } from "./Modal.js"; import { Time } from "./Time.js"; -import { useSettings } from "../hooks/useSettings.js"; interface Props extends JSX.HTMLAttributes { } @@ -41,25 +43,29 @@ 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" | "calls">("tasks") + const [table, setTable] = useState<"tasks" | "events">("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 style={{ padding: 4, margin: 2, border: "solid 1px black" }} onClick={() => { + updateSettings("showWalletActivity", false) + }}> + close + </div> </div> </div> - <div> + <div style={{ display: "flex", justifyContent: "space-around" }}> <Button variant={table === "tasks" ? "contained" : "outlined"} + style={{ margin: 4 }} onClick={async () => { setTable("tasks") }} @@ -67,6 +73,7 @@ export function WalletActivity({ }: Props): VNode { <i18n.Translate>Tasks</i18n.Translate> </Button> <Button variant={table === "events" ? "contained" : "outlined"} + style={{ margin: 4 }} onClick={async () => { setTable("events") }} @@ -74,22 +81,12 @@ export function WalletActivity({ }: Props): VNode { <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 /> } @@ -102,238 +99,599 @@ export function WalletActivity({ }: Props): VNode { ); } -export function WalletCallsTable({ }: {}): VNode { - return <div /> +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 />; + const not = events[0]; + if (not.type !== NotificationType.BalanceChange) return <Fragment />; + return <Fragment> + <dt>Transaction</dt> + <dd> + <a title={not.hintTransactionId} href={Pages.balanceTransaction({ tid: not.hintTransactionId })}>{not.hintTransactionId.substring(0, 10)}</a> + </dd> + </Fragment> +} + +function ShowBackupOperationError({ events, onClick }: MoreInfoPRops): VNode { + if (!events.length) return <Fragment />; + const not = events[0]; + if (not.type !== NotificationType.BackupOperationError) return <Fragment />; + return <Fragment> + <dt>Error</dt> + <dd> + <a href="#" onClick={(e) => { + e.preventDefault(); + const error = not.error + onClick(<Fragment> + <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 style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}> + {JSON.stringify(error, undefined, 2)} + </pre> + </Fragment>) + }}>{TalerErrorCode[not.error.code]}</a> + </dd> + </Fragment> +} + +function ShowTransactionStateTransition({ events, onClick }: MoreInfoPRops): VNode { + if (!events.length) return <Fragment />; + const not = events[0]; + if (not.type !== NotificationType.TransactionStateTransition) return <Fragment />; + 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) => { + if (!not.errorInfo) return; + e.preventDefault(); + const error = not.errorInfo; + onClick(<Fragment> + <dl> + <dt>Code</dt> + <dd>{TalerErrorCode[error.code]} ({error.code})</dd> + <dt>Hint</dt> + <dd>{error.hint ?? "--"}</dd> + <dt>Message</dt> + <dd>{error.message ?? "--"}</dd> + </dl> + </Fragment>) + + }}>{TalerErrorCode[not.errorInfo.code]}</a> + </dd> + </Fragment> : undefined} + <dt>Experimental</dt> + <dd> + <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}> + {JSON.stringify(not.experimentalUserData, undefined, 2)} + </pre> + </dd> + + + </Fragment> +} +function ShowExchangeStateTransition({ events, onClick }: MoreInfoPRops): VNode { + if (!events.length) return <Fragment />; + const not = events[0]; + if (not.type !== NotificationType.ExchangeStateTransition) return <Fragment />; + return <Fragment> + <dt>Exchange</dt> + <dd> + {not.exchangeBaseUrl} + </dd> + {not.oldExchangeState && not.newExchangeState.exchangeEntryStatus !== not.oldExchangeState?.exchangeEntryStatus && <Fragment> + <dt>Entry status</dt> + <dd> + from {not.oldExchangeState.exchangeEntryStatus} to {not.newExchangeState.exchangeEntryStatus} + </dd> + </Fragment>} + {not.oldExchangeState && not.newExchangeState.exchangeUpdateStatus !== not.oldExchangeState?.exchangeUpdateStatus && <Fragment> + <dt>Update status</dt> + <dd> + from {not.oldExchangeState.exchangeUpdateStatus} to {not.newExchangeState.exchangeUpdateStatus} + </dd> + </Fragment>} + {not.oldExchangeState && not.newExchangeState.tosStatus !== not.oldExchangeState?.tosStatus && <Fragment> + <dt>Tos status</dt> + <dd> + from {not.oldExchangeState.tosStatus} to {not.newExchangeState.tosStatus} + </dd> + </Fragment>} + </Fragment> +} + +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 <Fragment />; + + 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 dependecy" + } + })(); + + return <ShowObervavilityDetails title={title} notif={not} onClick={onClick} /> + + }) + return <table> + <thead> + <td>Event</td> + <td>Info</td> + <td>Start</td> + <td>End</td> + </thead> + <tbody> + {asd} + </tbody> + </table> +} + +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 <tr> + <td><a href="#" onClick={(e) => { + e.preventDefault(); + onClick(<Fragment> + <pre + style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }} + > + {JSON.stringify({ event: notif, prev }, undefined, 2)} + </pre> + </Fragment>); + }}>{title}</a></td> + <td> + {notif.event.url} { + prev?.event.type === ObservabilityEventType.HttpFetchFinishSuccess ? `(${prev.event.status})` + : prev?.event.type === ObservabilityEventType.HttpFetchFinishError ? <a href="#" onClick={(e) => { + e.preventDefault(); + if (prev.event.type !== ObservabilityEventType.HttpFetchFinishError) return; + const error = prev.event.error + onClick(<Fragment> + <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 style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}> + {JSON.stringify(error, undefined, 2)} + </pre> + + </Fragment>) + }}>fail</a> : undefined + } + </td> + <td> <Time + timestamp={notif.when} + format="yyyy/MM/dd HH:mm:ss" + /></td> + <td> <Time + timestamp={prev?.when} + format="yyyy/MM/dd HH:mm:ss" + /></td> + </tr> + + } + case ObservabilityEventType.DbQueryStart: + case ObservabilityEventType.DbQueryFinishSuccess: + case ObservabilityEventType.DbQueryFinishError: { + return <tr> + <td><a href="#" onClick={(e) => { + e.preventDefault(); + onClick(<Fragment> + <pre + style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }} + > + {JSON.stringify({ event: notif, prev }, undefined, 2)} + </pre> + </Fragment>); + }}>{title}</a></td> + <td> + {notif.event.location} {notif.event.name} + </td> + <td> + <Time + timestamp={notif.when} + format="yyyy/MM/dd HH:mm:ss" + /> + </td> + <td> + <Time + timestamp={prev?.when} + format="yyyy/MM/dd HH:mm:ss" + /> + </td> + </tr> + } + + case ObservabilityEventType.TaskStart: + case ObservabilityEventType.TaskStop: + case ObservabilityEventType.DeclareTaskDependency: + case ObservabilityEventType.TaskReset: { + return <tr> + <td><a href="#" onClick={(e) => { + e.preventDefault(); + onClick(<Fragment> + <pre + style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }} + > + {JSON.stringify({ event: notif, prev }, undefined, 2)} + </pre> + </Fragment>); + }}>{title}</a></td> + <td> + {notif.event.taskId} + </td> + <td> + <Time + timestamp={notif.when} + format="yyyy/MM/dd HH:mm:ss" + /> + </td> + <td> + <Time + timestamp={prev?.when} + format="yyyy/MM/dd HH:mm:ss" + /> + </td> + </tr> + } + case ObservabilityEventType.ShepherdTaskResult: { + return <tr> + <td><a href="#" onClick={(e) => { + e.preventDefault(); + onClick(<Fragment> + <pre + style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }} + > + {JSON.stringify({ event: notif, prev }, undefined, 2)} + </pre> + </Fragment>); + }}>{title}</a></td> + <td> + {notif.event.resultType} + </td> + <td> + <Time + timestamp={notif.when} + format="yyyy/MM/dd HH:mm:ss" + /> + </td> + <td> + <Time + timestamp={prev?.when} + format="yyyy/MM/dd HH:mm:ss" + /> + </td> + </tr> + + } + case ObservabilityEventType.CryptoStart: + case ObservabilityEventType.CryptoFinishSuccess: + case ObservabilityEventType.CryptoFinishError: { + return <tr> + <td><a href="#" onClick={(e) => { + e.preventDefault(); + onClick(<Fragment> + <pre + style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }} + > + {JSON.stringify({ event: notif, prev }, undefined, 2)} + </pre> + </Fragment>); + }}>{title}</a></td> + <td> + {notif.event.operation} + </td> + <td> + <Time + timestamp={notif.when} + format="yyyy/MM/dd HH:mm:ss" + /> + </td> + <td> + <Time + timestamp={prev?.when} + format="yyyy/MM/dd HH:mm:ss" + /> + </td> + </tr> + } + case ObservabilityEventType.RequestStart: + case ObservabilityEventType.RequestFinishSuccess: + case ObservabilityEventType.RequestFinishError: { + return <tr > + <td><a href="#" onClick={(e) => { + e.preventDefault(); + onClick(<Fragment> + <pre + style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }} + > + {JSON.stringify({ event: notif, prev }, undefined, 2)} + </pre> + </Fragment>); + }}>{title}</a></td> + <td> + {notif.event.type} + </td> + <td> + <Time + timestamp={notif.when} + format="yyyy/MM/dd HH:mm:ss" + /> + </td> + <td> + <Time + timestamp={prev?.when} + format="yyyy/MM/dd HH:mm:ss" + /> + </td> + </tr> + } + } +} + +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.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) + } + } } -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()) + const [lastShow, setLastShow] = useState<Date>(new Date()) + + const [notifications, setNotifications] = useState<{ notif: WalletNotification, when: AbsoluteTime }[]>([]) useEffect(() => { - return api.listener.onUpdateNotification(listenAllEvents, (notif) => { - notifications.unshift(notif) - setLastEvent(new Date()) + return api.listener.onUpdateNotification(listenAllEvents, (not) => { + console.log(not) + const time = new Date(); + setLastEvent(time) + notifications.unshift({ + notif: not, + when: AbsoluteTime.now() + }) + setNotifications(Array.from(notifications)) }); }); - 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> - - } - 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> + const [showDetails, setShowDetails] = useState<VNode>() + const [notif, setnotif] = useState<Notif[]>([]) + return <div> + <div style={{ display: "flex", justifyContent: "space-between" }}> + {lastShow === lastEvent ? + <div>last event {lastEvent.toString()}</div> + : <div>there are more events, update to see them</div>} + <div> + {lastShow !== lastEvent ? + <div style={{ padding: 4, margin: 2, border: "solid 1px black" }} onClick={() => { + const list = [...notif] + for (const pepe of notifications) { + const event = getNotificationFor(String(list.length), pepe.notif, pepe.when, list) + if (event) { + list.push(event) } } - })()} + setnotif(list) + setLastShow(lastEvent) + }}> + update + </div> + : <div />} + <div style={{ padding: 4, margin: 2, border: "solid 1px black" }} onClick={() => { + setNotifications([]) + }}> + clear + </div> + </div> + </div> + {showDetails && <Modal title="event details" onClose={{ onClick: (async () => { setShowDetails(undefined) }) as any }} > + {showDetails} + </Modal>} + {notif.map((not) => { + return ( + <details key={not.id}> + <summary> + <div style={{ width: "90%", display: "inline-flex", justifyContent: "space-between", padding: 4 }}> + <div style={{ padding: 4 }}> + {not.description} + </div> + <div style={{ padding: 4 }}> + <Time + timestamp={not.start} + format="yyyy/MM/dd HH:mm:ss" + /> + </div> + <div style={{ padding: 4 }}><Time + timestamp={not.end} + format="yyyy/MM/dd HH:mm:ss" + /></div> + </div> + </summary> + <not.MoreInfo events={not.events} onClick={(details) => { + setShowDetails(details) + }} /> </details> ); })} @@ -355,7 +713,7 @@ function ErroDetailModal({ error, onClose }: { error: TalerErrorDetail, onClose: format="yyyy/MM/dd HH:mm:ss" /></dd> </dl> - <pre> + <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}> {JSON.stringify(error, undefined, 2)} </pre> </Modal> @@ -371,14 +729,15 @@ export function ActiveTasksTable({ }: {}): VNode { ); const [showError, setShowError] = useState<TalerErrorDetail>() const tasks = state && !state.hasError ? state.response.tasks : []; - useEffect(() => { - return api.listener.onUpdateNotification(listenAllEvents, (notif) => { - state?.retry() - }); - }); + // useEffect(() => { + // return api.listener.onUpdateNotification(listenAllEvents, (notif) => { + // state?.retry() + // }); + // }); return <Fragment> {showError && <ErroDetailModal error={showError} onClose={(async () => { setShowError(undefined) })} />} - <table> + + <table style={{ width: "100%" }}> <thead> <tr> <th> diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx index 2501c61c8..89678c74a 100644 --- a/packages/taler-wallet-webextension/src/components/styled/index.tsx +++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx @@ -35,7 +35,6 @@ export const WalletAction = styled.div` align-items: center; margin: auto; - height: 100%; & h1:first-child { margin-top: 0; diff --git a/packages/taler-wallet-webextension/src/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts index e3afe35bd..faf0f2820 100644 --- a/packages/taler-wallet-webextension/src/platform/api.ts +++ b/packages/taler-wallet-webextension/src/platform/api.ts @@ -55,6 +55,7 @@ export interface CrossBrowserPermissionsApi { export enum ExtensionNotificationType { SettingsChange = "settings-change", + ClearNotifications = "clear-notifications", } export interface SettingsChangeNotification { @@ -62,8 +63,11 @@ export interface SettingsChangeNotification { currentValue: Settings; } +export interface ClearNotificaitonNotification { + type: ExtensionNotificationType.ClearNotifications; +} -export type ExtensionNotification = SettingsChangeNotification +export type ExtensionNotification = SettingsChangeNotification | ClearNotificaitonNotification export type MessageFromBackend = { type: "wallet", @@ -300,6 +304,12 @@ export interface ForegroundPlatformAPI { ): Promise<MessageResponse>; /** + * Used by the wallet frontend to send notification about new information + * @param message + */ + triggerWalletEvent(message: MessageFromBackend): void; + + /** * Used from the frontend to receive notifications about new information * @param listener * @return function to unsubscribe the listener diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts index d88dae460..fc2d1db09 100644 --- a/packages/taler-wallet-webextension/src/platform/chrome.ts +++ b/packages/taler-wallet-webextension/src/platform/chrome.ts @@ -45,6 +45,7 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = { findTalerUriInClipboard, getPermissionsApi, getWalletWebExVersion, + triggerWalletEvent, listenToWalletBackground, notifyWhenAppIsReady, openWalletPage, @@ -324,6 +325,18 @@ function listenToWalletBackground(listener: (m: any) => void): () => void { const allPorts: chrome.runtime.Port[] = []; + +function triggerWalletEvent(message: MessageFromBackend): void { + for (const notif of allPorts) { + // const message: MessageFromBackend = { type: msg.type }; + try { + notif.postMessage(message); + } catch (e) { + logger.error("error posting a message", e); + } + } +} + function sendMessageToAllChannels(message: MessageFromBackend): void { for (const notif of allPorts) { // const message: MessageFromBackend = { type: msg.type }; diff --git a/packages/taler-wallet-webextension/src/platform/dev.ts b/packages/taler-wallet-webextension/src/platform/dev.ts index 2993c88bc..7cbbe5b25 100644 --- a/packages/taler-wallet-webextension/src/platform/dev.ts +++ b/packages/taler-wallet-webextension/src/platform/dev.ts @@ -36,6 +36,7 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = { findTalerUriInClipboard: async () => undefined, listenNetworkConnectionState, openNewURLFromPopup: () => undefined, + triggerWalletEvent: () => undefined, getPermissionsApi: () => ({ containsClipboardPermissions: async () => true, removeClipboardPermissions: async () => false, diff --git a/packages/taler-wallet-webextension/src/test-utils.ts b/packages/taler-wallet-webextension/src/test-utils.ts index d25326942..90037819f 100644 --- a/packages/taler-wallet-webextension/src/test-utils.ts +++ b/packages/taler-wallet-webextension/src/test-utils.ts @@ -115,6 +115,9 @@ export function createWalletApiMock(): { }, }), listener: { + trigger: () => { + + }, onUpdateNotification( mTypes: NotificationType[], callback: ((d: WalletNotification) => void) | undefined, diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx index cdd3994d7..c98538755 100644 --- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx @@ -97,7 +97,7 @@ export function DeveloperPage({ }: Props): VNode { const { safely } = useAlertContext(); const listenAllEvents = Array.from<NotificationType>({ length: 1 }); - listenAllEvents.includes = () => true + // listenAllEvents.includes = () => true const hook = useAsyncAsHook(async () => { const list = await api.wallet.call(WalletApiOperation.ListExchanges, {}); diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index df99d3f17..a1c09ef8d 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -41,6 +41,7 @@ import { WalletCoreResponseType, } from "@gnu-taler/taler-wallet-core"; import { + ExtensionNotification, MessageFromBackend, MessageFromFrontendBackground, MessageFromFrontendWallet, @@ -192,14 +193,23 @@ export type WxApiType = { wallet: WalletCoreApiClient; background: BackgroundApiClient; listener: { + trigger: (d: ExtensionNotification) => void; onUpdateNotification: typeof onUpdateNotification; }; }; +function trigger(w:ExtensionNotification) { + platform.triggerWalletEvent({ + type: "web-extension", + notification: w, + }) +} + export const wxApi = { wallet: new WalletApiClientImpl(), background: new BackgroundApiClientImpl(), listener: { + trigger, onUpdateNotification, }, }; |