[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-wallet-core] branch master updated: resolve #7751
From: |
gnunet |
Subject: |
[taler-wallet-core] branch master updated: resolve #7751 |
Date: |
Tue, 12 Dec 2023 17:58:49 +0100 |
This is an automated email from the git hooks/post-receive script.
sebasjm pushed a commit to branch master
in repository wallet-core.
The following commit(s) were added to refs/heads/master by this push:
new f7e01690b resolve #7751
f7e01690b is described below
commit f7e01690b44e42b7088457374a2a8606fd94f84e
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Dec 12 13:58:29 2023 -0300
resolve #7751
---
packages/demobank-ui/src/Routing.tsx | 6 +-
packages/demobank-ui/src/pages/DownloadStats.tsx | 398 +++++++++++++++++----
packages/demobank-ui/src/pages/admin/AdminHome.tsx | 4 +-
3 files changed, 344 insertions(+), 64 deletions(-)
diff --git a/packages/demobank-ui/src/Routing.tsx
b/packages/demobank-ui/src/Routing.tsx
index 711b7f871..4a250a0d5 100644
--- a/packages/demobank-ui/src/Routing.tsx
+++ b/packages/demobank-ui/src/Routing.tsx
@@ -122,7 +122,11 @@ export function Routing(): VNode {
/>
<Route
path="/download-stats"
- component={() => <DownloadStats />}
+ component={() => <DownloadStats
+ onCancel={() => {
+ route("/account")
+ }}
+ />}
/>
<Route
diff --git a/packages/demobank-ui/src/pages/DownloadStats.tsx
b/packages/demobank-ui/src/pages/DownloadStats.tsx
index cd3e6d875..596539e7e 100644
--- a/packages/demobank-ui/src/pages/DownloadStats.tsx
+++ b/packages/demobank-ui/src/pages/DownloadStats.tsx
@@ -14,109 +14,383 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AccessToken, Logger, RefreshReason, TalerCoreBankHttpClient,
TalerCorebankApi, TalerError } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { AccessToken, AmountString, Logger, TalerCoreBankHttpClient,
TalerCorebankApi, TalerError } from "@gnu-taler/taler-util";
+import { Attention, LocalNotificationBanner, useLocalNotification,
useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { Loading } from "@gnu-taler/web-util/browser";
-import { Transactions } from "../components/Transactions/index.js";
-import { usePublicAccounts } from "../hooks/access.js";
-import { useBackendState } from "../hooks/backend.js";
import { useBankCoreApiContext } from "../context/config.js";
+import { useBackendState } from "../hooks/backend.js";
import { getTimeframesForDate } from "./admin/AdminHome.js";
const logger = new Logger("PublicHistoriesPage");
-interface Props { }
+interface Props {
+ onCancel: () => void;
+}
+
+type Options = {
+ dayMetric: boolean;
+ hourMetric: boolean;
+ monthMetric: boolean;
+ yearMetric: boolean;
+ compareWithPrevious: boolean;
+ endOnFirstFail: boolean;
+ includeHeader: boolean;
+}
/**
* Show histories of public accounts.
*/
-export function DownloadStats({ }: Props): VNode {
+export function DownloadStats({ onCancel }: Props): VNode {
const { i18n } = useTranslationContext();
const { state: credentials } = useBackendState();
const creds = credentials.status !== "loggedIn" ||
!credentials.isUserAdministrator ? undefined : credentials
- const { api, config } = useBankCoreApiContext();
+ const { api } = useBankCoreApiContext();
- const [state, setState] = useState<number>()
+ const [options, setOptions] = useState<Options>({
+ compareWithPrevious: true,
+ dayMetric: true,
+ endOnFirstFail: false,
+ hourMetric: true,
+ includeHeader: true,
+ monthMetric: true,
+ yearMetric: true,
+ })
+ const [lastStep, setLastStep] = useState<{ step: number, total: number }>()
+ const [downloaded, setDownloaded] = useState<string>()
+ const referenceDates = [new Date()]
+ const [notification, notify, handleError] = useLocalNotification()
if (!creds) {
return <div>only admin can download stats</div>
}
return (
- <Fragment>
- <h1 class="nav">{i18n.str`Stats`}</h1>
- <button type="button"
- class="inline-flex items-center disabled:opacity-50
disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2
text-sm font-semibold text-white shadow-sm hover:bg-indigo-500
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2
focus-visible:outline-indigo-600"
- onClick={() => {
- fetchAllStatus(api, creds.token, new Date(), (p, total) => {
- console.log("doing...", total - p)
- setState(total - p)
- })
- }}
- >
- start
- </button>
- progress {state}
- </Fragment>
+ <div>
+
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3
bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+ <LocalNotificationBanner notification={notification} />
+
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <i18n.Translate>Download bank stats</i18n.Translate>
+ </h2>
+ </div>
+
+
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl
md:col-span-2"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={e => {
+ e.preventDefault()
+ }}
+ >
+ <div class="px-4 py-6 sm:p-8">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8
sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span class="text-sm text-black font-medium leading-6 "
id="availability-label">
+ <i18n.Translate>Include hour metric</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={options.hourMetric}
class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6
w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2
focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false"
aria-labelledby="availability-label" aria-describedby="availability-description"
+
+ onClick={() => { setOptions({ ...options, hourMetric:
!options.hourMetric }) }}>
+ <span aria-hidden="true" data-enabled={options.hourMetric}
class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none
inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition
duration-200 ease-in-out"></span>
+ </button>
+ </div>
+ </div>
+ <div class="sm:col-span-5">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span class="text-sm text-black font-medium leading-6 "
id="availability-label">
+ <i18n.Translate>Include day metric</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={!!options.dayMetric}
class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6
w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2
focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false"
aria-labelledby="availability-label" aria-describedby="availability-description"
+
+ onClick={() => { setOptions({ ...options, dayMetric:
!options.dayMetric }) }}>
+ <span aria-hidden="true" data-enabled={options.dayMetric}
class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none
inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition
duration-200 ease-in-out"></span>
+ </button>
+ </div>
+ </div>
+ <div class="sm:col-span-5">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span class="text-sm text-black font-medium leading-6 "
id="availability-label">
+ <i18n.Translate>Include month metric</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={!!options.monthMetric}
class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6
w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2
focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false"
aria-labelledby="availability-label" aria-describedby="availability-description"
+
+ onClick={() => { setOptions({ ...options, monthMetric:
!options.monthMetric }) }}>
+ <span aria-hidden="true"
data-enabled={options.monthMetric} class="translate-x-5
data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5
transform rounded-full bg-white shadow ring-0 transition duration-200
ease-in-out"></span>
+ </button>
+ </div>
+ </div>
+ <div class="sm:col-span-5">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span class="text-sm text-black font-medium leading-6 "
id="availability-label">
+ <i18n.Translate>Include year metric</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={!!options.yearMetric}
class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6
w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2
focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false"
aria-labelledby="availability-label" aria-describedby="availability-description"
+
+ onClick={() => { setOptions({ ...options, yearMetric:
!options.yearMetric }) }}>
+ <span aria-hidden="true" data-enabled={options.yearMetric}
class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none
inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition
duration-200 ease-in-out"></span>
+ </button>
+ </div>
+ </div>
+ <div class="sm:col-span-5">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span class="text-sm text-black font-medium leading-6 "
id="availability-label">
+ <i18n.Translate>Include table header</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={!!options.includeHeader}
class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6
w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2
focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false"
aria-labelledby="availability-label" aria-describedby="availability-description"
+
+ onClick={() => { setOptions({ ...options, includeHeader:
!options.includeHeader }) }}>
+ <span aria-hidden="true"
data-enabled={options.includeHeader} class="translate-x-5
data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5
transform rounded-full bg-white shadow ring-0 transition duration-200
ease-in-out"></span>
+ </button>
+ </div>
+ </div>
+ <div class="sm:col-span-5">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span class="text-sm text-black font-medium leading-6 "
id="availability-label">
+ <i18n.Translate>Add previous metric for
compare</i18n.Translate>
+ </span>
+ </span>
+ <button type="button"
data-enabled={!!options.compareWithPrevious} class="bg-indigo-600
data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0
cursor-pointer rounded-full border-2 border-transparent transition-colors
duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600
focus:ring-offset-2" role="switch" aria-checked="false"
aria-labelledby="availability-label" aria-describedby="availability-description"
+
+ onClick={() => { setOptions({ ...options,
compareWithPrevious: !options.compareWithPrevious }) }}>
+ <span aria-hidden="true"
data-enabled={options.compareWithPrevious} class="translate-x-5
data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5
transform rounded-full bg-white shadow ring-0 transition duration-200
ease-in-out"></span>
+ </button>
+ </div>
+ </div>
+ <div class="sm:col-span-5">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span class="text-sm text-black font-medium leading-6 "
id="availability-label">
+ <i18n.Translate>Fail on first error</i18n.Translate>
+ </span>
+ </span>
+ <button type="button"
data-enabled={!!options.endOnFirstFail} class="bg-indigo-600
data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0
cursor-pointer rounded-full border-2 border-transparent transition-colors
duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600
focus:ring-offset-2" role="switch" aria-checked="false"
aria-labelledby="availability-label" aria-describedby="availability-description"
+
+ onClick={() => { setOptions({ ...options, endOnFirstFail:
!options.endOnFirstFail }) }}>
+ <span aria-hidden="true"
data-enabled={options.endOnFirstFail} class="translate-x-5
data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5
transform rounded-full bg-white shadow ring-0 transition duration-200
ease-in-out"></span>
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+
+
+ <div class="flex items-center justify-between gap-x-6 border-t
border-gray-900/10 px-4 py-4 sm:px-8">
+ <button type="button" class="text-sm font-semibold leading-6
text-gray-900"
+ onClick={onCancel}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <button type="submit"
+ class="disabled:opacity-50 disabled:cursor-default
cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold
text-white shadow-sm hover:bg-indigo-500 focus-visible:outline
focus-visible:outline-2 focus-visible:outline-offset-2
focus-visible:outline-indigo-600"
+ disabled={lastStep !== undefined}
+ onClick={async () => {
+ setDownloaded(undefined)
+ await handleError(async () => {
+ const csv = await fetchAllStatus(api, creds.token, options,
referenceDates, (step, total) => {
+ setLastStep({ step, total })
+ })
+ setDownloaded(csv)
+ })
+ setLastStep(undefined)
+ }}
+ >
+ <i18n.Translate>Download</i18n.Translate>
+ </button>
+ </div>
+ </form>
+
+ </div>
+ {!lastStep || lastStep.step === lastStep.total ? <div class="h-5 mb-5"/>
: <div>
+ <div class="relative mb-5 h-5 rounded-full bg-gray-200">
+ <div class="h-full animate-pulse rounded-full bg-blue-500" style={{
+ width: `${Math.round((((lastStep.step / lastStep.total))* 100) )}%`
+ }}>
+ <span class="absolute inset-0 flex items-center justify-center
text-xs font-semibold text-white">
+ <i18n.Translate>downloading... {Math.round((((lastStep.step /
lastStep.total))* 100) )}</i18n.Translate>
+ </span>
+ </div>
+ </div>
+ </div>}
+ {!downloaded ? <div class="h-5 mb-5"/> :
+ <a href={"data:text/plain;charset=utf-8," +
encodeURIComponent(downloaded)} download={"bank-stats.csv"}>
+ <Attention title={i18n.str`Download completed`}>
+ <i18n.Translate>click here to save the file in your
computer</i18n.Translate>
+ </Attention>
+ </a>
+ }
+ </div>
);
}
-async function fetchAllStatus(api: TalerCoreBankHttpClient, token:
AccessToken, reference: Date, progres: (current: number, total: number) =>
void) {
- const allMetrics = [
- TalerCorebankApi.MonitorTimeframeParam.day,
- TalerCorebankApi.MonitorTimeframeParam.hour,
- // TalerCorebankApi.MonitorTimeframeParam.month,
- // TalerCorebankApi.MonitorTimeframeParam.year,
- // TalerCorebankApi.MonitorTimeframeParam.decade,
- ]
- const allFrames = allMetrics.map(timeframe => ({
- timeframe, moment: getTimeframesForDate(reference, timeframe)
- }))
-
- type Data = {
- previous: TalerCorebankApi.MonitorResponse;
- current: TalerCorebankApi.MonitorResponse;
+async function fetchAllStatus(api: TalerCoreBankHttpClient, token:
AccessToken, options: Options, references: Date[], progres: (current: number,
total: number) => void): Promise<string> {
+ const allMetrics: TalerCorebankApi.MonitorTimeframeParam[] = [];
+ if (options.hourMetric) {
+ allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.hour)
+ }
+ if (options.dayMetric) {
+ allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.day)
}
+ if (options.monthMetric) {
+ allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.month)
+ }
+ if (options.yearMetric) {
+ allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.year)
+ }
+
+ /**
+ * conver request into frames
+ */
+ const allFrames = allMetrics.flatMap(timeframe => references.map(reference
=> ({
+ reference,
+ timeframe,
+ moment: getTimeframesForDate(reference, timeframe)
+ }))
+ )
const total = allFrames.length
-
- const dataResolvers = allFrames.map((frame, index) => async function
getData(): Promise<Data | undefined> {
- const previous = await api.getMonitor(token, {
+
+ /**
+ * call API for info
+ */
+ const allInfo = await allFrames.reduce(async (prev, frame, index) => {
+ const accumulatedMap = await prev
+ progres(index, total)
+ // await delay()
+ const previous = options.compareWithPrevious ? (await
api.getMonitor(token, {
timeframe: frame.timeframe,
which: frame.moment.previous
- })
- await delay()
- if (previous.type !== "ok") return undefined;
+ })) : undefined
+
+ if (previous && previous.type === "fail" && options.endOnFirstFail) {
+ throw TalerError.fromUncheckedDetail(previous.detail)
+ }
+
const current = await api.getMonitor(token, {
timeframe: frame.timeframe,
which: frame.moment.current
})
- await delay()
- if (current.type !== "ok") return undefined;
- return { previous: previous.body, current: current.body }
- });
- const csv = await dataResolvers.reduce(async (prev, resolver, index) => {
- const accumulatedMap = await prev
- console.log(index)
- progres(index, total)
- const data = await resolver()
- if (!data) return accumulatedMap
+ if (current.type === "fail" && options.endOnFirstFail) {
+ throw TalerError.fromUncheckedDetail(current.detail)
+ }
const metricName =
TalerCorebankApi.MonitorTimeframeParam[allMetrics[index]]
- accumulatedMap[metricName] = data
+ accumulatedMap[metricName] = {
+ reference: frame.reference,
+ current: current.type !== "ok" ? undefined : current.body,
+ previous: !previous || previous.type !== "ok" ? undefined :
previous.body,
+ }
return accumulatedMap
}, Promise.resolve({} as Record<string, Data>))
progres(total, total)
- console.log(csv)
+
+ /**
+ * conver into table format
+ *
+ */
+ const table: Array<string[]> = [];
+ if (options.includeHeader) {
+ table.push(["date",
+ "metric",
+ "reference",
+ "talerInCount",
+ "talerInVolume",
+ "talerOutCount",
+ "talerOutVolume",
+ "cashinCount",
+ "cashinFiatVolume",
+ "cashinRegionalVolume",
+ "cashoutCount",
+ "cashoutFiatVolume",
+ "cashoutRegionalVolume",])
+ }
+ Object.entries(allInfo).forEach(([name, data]) => {
+ if (data.current) {
+ const row: TableRow = {
+ date: data.reference.getTime(),
+ metric: name,
+ reference: "current",
+ ...dataToRow(data.current)
+ }
+ table.push((Object.values(row) as string[]))
+ }
+
+ if (data.previous) {
+ const row: TableRow = {
+ date: data.reference.getTime(),
+ metric: name,
+ reference: "previous",
+ ...dataToRow(data.previous)
+ }
+ table.push((Object.values(row) as string[]))
+ }
+ })
+
+ const csv = table.reduce((acc, row) => {
+ return acc + row.join(",") + "\n"
+ }, "")
+
+ return csv
+}
+
+type JustData = Omit<Omit<Omit<TableRow, "metric">, "date">, "reference">
+function dataToRow(info: TalerCorebankApi.MonitorResponse): JustData {
+ return {
+ talerInCount: info.talerInCount,
+ talerInVolume: info.talerInVolume,
+ talerOutCount: info.talerOutCount,
+ talerOutVolume: info.talerOutVolume,
+ cashinCount: info.type === "no-conversions" ? undefined : info.cashinCount,
+ cashinFiatVolume: info.type === "no-conversions" ? undefined :
info.cashinFiatVolume,
+ cashinRegionalVolume: info.type === "no-conversions" ? undefined :
info.cashinRegionalVolume,
+ cashoutCount: info.type === "no-conversions" ? undefined :
info.cashoutCount,
+ cashoutFiatVolume: info.type === "no-conversions" ? undefined :
info.cashoutFiatVolume,
+ cashoutRegionalVolume: info.type === "no-conversions" ? undefined :
info.cashoutRegionalVolume,
+ }
+}
+
+type Data = {
+ reference: Date,
+ previous: TalerCorebankApi.MonitorResponse | undefined;
+ current: TalerCorebankApi.MonitorResponse | undefined;
+}
+type TableRow = {
+ date: number,
+ metric: string,
+ reference: "current" | "previous",
+ cashinCount?: number;
+ cashinRegionalVolume?: AmountString;
+ cashinFiatVolume?: AmountString;
+ cashoutCount?: number;
+ cashoutRegionalVolume?: AmountString;
+ cashoutFiatVolume?: AmountString;
+ talerInCount: number;
+ talerInVolume: AmountString;
+ talerOutCount: number;
+ talerOutVolume: AmountString;
}
async function delay() {
- return new Promise((res, rej) => {
- setTimeout(() => {
- res(null);
- }, 1000)
+ return new Promise(res => {
+ setTimeout(( )=> {
+ res(null)
+ }, 500)
})
}
\ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx
b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
index 9c6e6cde6..82a341dbe 100644
--- a/packages/demobank-ui/src/pages/admin/AdminHome.tsx
+++ b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
@@ -184,7 +184,9 @@ function Metrics(): VNode {
</div>
</dl>
<div class="flex justify-end mt-2">
- <a href="#/download-stats" class="link"><i18n.Translate>
+ <a href="#/download-stats"
+ class="disabled:opacity-50 disabled:cursor-default
cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold
text-white shadow-sm hover:bg-indigo-500 focus-visible:outline
focus-visible:outline-2 focus-visible:outline-offset-2
focus-visible:outline-indigo-600"
+ ><i18n.Translate>
download stats as csv
</i18n.Translate></a>
</div>
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [taler-wallet-core] branch master updated: resolve #7751,
gnunet <=