aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-12-12 13:58:29 -0300
committerSebastian <sebasjm@gmail.com>2023-12-12 13:58:44 -0300
commitf7e01690b44e42b7088457374a2a8606fd94f84e (patch)
tree6f9e57e69f119786320d8f9b08f5ccb2c9477946 /packages/demobank-ui/src
parentb448b77eb4e8dc0157d1780e11bc0f38d7b636cf (diff)
downloadwallet-core-f7e01690b44e42b7088457374a2a8606fd94f84e.tar.xz
resolve #7751
Diffstat (limited to 'packages/demobank-ui/src')
-rw-r--r--packages/demobank-ui/src/Routing.tsx6
-rw-r--r--packages/demobank-ui/src/pages/DownloadStats.tsx398
-rw-r--r--packages/demobank-ui/src/pages/admin/AdminHome.tsx4
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>