From 0b90a34e7c7c5d9bcca9a2ebe74df9fdfafc6577 Mon Sep 17 00:00:00 2001 From: Nic Eigel Date: Mon, 24 Jun 2024 01:50:38 +0200 Subject: real-time-auditor --- .../auditor-backoffice-ui/src/hooks/backend.ts | 618 +++++++-------------- .../auditor-backoffice-ui/src/hooks/critical.ts | 70 +++ .../src/hooks/deposit_confirmations.ts | 161 ------ packages/auditor-backoffice-ui/src/hooks/entity.ts | 82 +++ .../auditor-backoffice-ui/src/hooks/finance.ts | 61 ++ packages/auditor-backoffice-ui/src/hooks/index.ts | 176 ++---- .../auditor-backoffice-ui/src/hooks/operational.ts | 83 +++ 7 files changed, 549 insertions(+), 702 deletions(-) create mode 100644 packages/auditor-backoffice-ui/src/hooks/critical.ts delete mode 100644 packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts create mode 100644 packages/auditor-backoffice-ui/src/hooks/entity.ts create mode 100644 packages/auditor-backoffice-ui/src/hooks/finance.ts create mode 100644 packages/auditor-backoffice-ui/src/hooks/operational.ts (limited to 'packages/auditor-backoffice-ui/src/hooks') diff --git a/packages/auditor-backoffice-ui/src/hooks/backend.ts b/packages/auditor-backoffice-ui/src/hooks/backend.ts index 5ed06bf78..4b0a5a828 100644 --- a/packages/auditor-backoffice-ui/src/hooks/backend.ts +++ b/packages/auditor-backoffice-ui/src/hooks/backend.ts @@ -17,461 +17,245 @@ /** * * @author Sebastian Javier Marchano (sebasjm) + * @author Nic Eigel */ -import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - HttpResponse, - HttpResponseOk, - RequestError, - RequestOptions, - useApiContext, + HttpResponse, + HttpResponseOk, + RequestError, + RequestOptions, + useApiContext, } from "@gnu-taler/web-util/browser"; -import { useCallback, useEffect, useState } from "preact/hooks"; -import { useSWRConfig } from "swr"; +import {useCallback, useEffect, useState} from "preact/hooks"; +import {useSWRConfig} from "swr"; import { useBackendContext } from "../context/backend.js"; -import { useInstanceContext } from "../context/instance.js"; -import { AccessToken, LoginToken, MerchantBackend, Timestamp } from "../declaration.js"; - +import { AuditorBackend } from "../declaration.js"; export function useMatchMutate(): ( - re?: RegExp, - value?: unknown, + re?: RegExp, + value?: unknown, ) => Promise { - const { cache, mutate } = useSWRConfig(); + const {cache, mutate} = useSWRConfig(); - if (!(cache instanceof Map)) { - throw new Error( - "matchMutate requires the cache provider to be a Map instance", - ); - } - - return function matchRegexMutate(re?: RegExp) { - return mutate((key) => { - // evict if no key or regex === all - if (!key || !re) return true - // match string - if (typeof key === 'string' && re.test(key)) return true - // record or object have the path at [0] - if (typeof key === 'object' && re.test(key[0])) return true - //key didn't match regex - return false - }, undefined, { - revalidate: true, - }); - }; + if (!(cache instanceof Map)) { + throw new Error( + "matchMutate requires the cache provider to be a Map instance", + ); + } + + return function matchRegexMutate(re?: RegExp) { + return mutate((key) => { + // evict if no key or regex === all + if (!key || !re) return true + // match string + if (typeof key === 'string' && re.test(key)) return true + // record or object have the path at [0] + if (typeof key === 'object' && re.test(key[0])) return true + //key didn't match regex + return false + }, undefined, { + revalidate: true, + }); + }; } -export function useBackendInstancesTestForAdmin(): HttpResponse< - MerchantBackend.Instances.InstancesResponse, - MerchantBackend.ErrorDetail +const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000; +const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000; + +export function useBackendConfig(): HttpResponse< + AuditorBackend.VersionResponse | undefined, + RequestError > { - const { request } = useBackendBaseRequest(); + const {request} = useBackendBaseRequest(); - type Type = MerchantBackend.Instances.InstancesResponse; + type Type = AuditorBackend.VersionResponse; + type State = { data: HttpResponse>, timer: number } + const [result, setResult] = useState({data: {loading: true}, timer: 0}); - const [result, setResult] = useState< - HttpResponse - >({ loading: true }); + useEffect(() => { + if (result.timer) { + clearTimeout(result.timer); + } - useEffect(() => { - request(`/management/instances`) - .then((data) => setResult(data)) - .catch((error: RequestError) => - setResult(error.cause), - ); - }, [request]); + function tryConfig(): void { + request(`/config`) + .then((data) => { + const timer: any = setTimeout(() => { + tryConfig(); + }, CHECK_CONFIG_INTERVAL_OK); + setResult({data, timer}); + }) + .catch((error) => { + const timer: any = setTimeout(() => { + tryConfig(); + }, CHECK_CONFIG_INTERVAL_FAIL); + const data = error.cause; + setResult({data, timer}); + }); + } - return result; -} + tryConfig(); + }, [request]); -const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000; -const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000; + return result.data; +} -export function useBackendConfig(): HttpResponse< - MerchantBackend.VersionResponse | undefined, - RequestError +export function useBackendToken(): HttpResponse< + AuditorBackend.VersionResponse, + RequestError > { - const { request } = useBackendBaseRequest(); + const {request} = useBackendBaseRequest(); - type Type = MerchantBackend.VersionResponse; - type State = { data: HttpResponse>, timer: number } - const [result, setResult] = useState({ data: { loading: true }, timer: 0 }); + type Type = AuditorBackend.VersionResponse; + type State = { data: HttpResponse>, timer: number } + const [result, setResult] = useState({data: {loading: true}, timer: 0}); - useEffect(() => { - if (result.timer) { - clearTimeout(result.timer) - } - function tryConfig(): void { - request(`/config`) - .then((data) => { - const timer: any = setTimeout(() => { - tryConfig() - }, CHECK_CONFIG_INTERVAL_OK) - setResult({ data, timer }) - }) - .catch((error) => { - const timer: any = setTimeout(() => { - tryConfig() - }, CHECK_CONFIG_INTERVAL_FAIL) - const data = error.cause - setResult({ data, timer }) - }); - } - tryConfig() - }, [request]); + useEffect(() => { + if (result.timer) { + clearTimeout(result.timer); + } + + function tryToken(): void { + request(`/monitoring/balances`) + .then((data) => { + const timer: any = setTimeout(() => { + tryToken(); + }, CHECK_CONFIG_INTERVAL_OK); + setResult({data, timer}); + }) + .catch((error) => { + const timer: any = setTimeout(() => { + tryToken(); + }, CHECK_CONFIG_INTERVAL_FAIL); + const data = error.cause; + setResult({data, timer}); + }); + } + + tryToken(); + }, [request]); - return result.data; + return result.data; } interface useBackendInstanceRequestType { - request: ( - endpoint: string, - options?: RequestOptions, - ) => Promise>; - fetcher: (endpoint: string) => Promise>; - reserveDetailFetcher: (endpoint: string) => Promise>; - rewardsDetailFetcher: (endpoint: string) => Promise>; - multiFetcher: (params: [url: string[]]) => Promise[]>; - orderFetcher: ( - params: [endpoint: string, - paid?: YesOrNo, - refunded?: YesOrNo, - wired?: YesOrNo, - searchDate?: Date, - delta?: number,] - ) => Promise>; - transferFetcher: ( - params: [endpoint: string, - payto_uri?: string, - verified?: string, - position?: string, - delta?: number,] - ) => Promise>; - templateFetcher: ( - params: [endpoint: string, - position?: string, - delta?: number] - ) => Promise>; - webhookFetcher: ( - params: [endpoint: string, - position?: string, - delta?: number] - ) => Promise>; -} -interface useBackendBaseRequestType { - request: ( - endpoint: string, - options?: RequestOptions, - ) => Promise>; -} -type YesOrNo = "yes" | "no"; -type LoginResult = { - valid: true; - token: string; - expiration: Timestamp; -} | { - valid: false; - cause: HttpError<{}>; + request: ( + endpoint: string, + options?: RequestOptions, + ) => Promise>; + fetcher: (endpoint: string) => Promise>; + multiFetcher: (params: string[]) => Promise[]>; + depositConfirmationFetcher: ( + params: [ + endpoint: string, + ], + ) => Promise>; } -export function useCredentialsChecker() { - const { request } = useApiContext(); - //check against instance details endpoint - //while merchant backend doesn't have a login endpoint - async function requestNewLoginToken( - baseUrl: string, - token: AccessToken, - ): Promise { - const data: MerchantBackend.Instances.LoginTokenRequest = { - scope: "write", - duration: { - d_us: "forever" - }, - refreshable: true, - } - try { - const response = await request(baseUrl, `/private/token`, { - method: "POST", - token, - data - }); - return { valid: true, token: response.data.token, expiration: response.data.expiration }; - } catch (error) { - if (error instanceof RequestError) { - return { valid: false, cause: error.cause }; - } - - return { - valid: false, cause: { - type: ErrorType.UNEXPECTED, - loading: false, - info: { - hasToken: true, - status: 0, - options: {}, - url: `/private/token`, - payload: {} - }, - exception: error, - message: (error instanceof Error ? error.message : "unpexepected error") - } - }; - } - }; - - async function refreshLoginToken( - baseUrl: string, - token: LoginToken - ): Promise { - - if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) { - return { - valid: false, cause: { - type: ErrorType.CLIENT, - status: HttpStatusCode.Unauthorized, - message: "login token expired, login again.", - info: { - hasToken: true, - status: 401, - options: {}, - url: `/private/token`, - payload: {} - }, - payload: {} - }, - } - } +interface useBackendBaseRequestType { - return requestNewLoginToken(baseUrl, token.token as AccessToken) - } - return { requestNewLoginToken, refreshLoginToken } + request: ( + endpoint: string, + options?: RequestOptions + ) => Promise>; } +type YesOrNo = "yes" | "no"; + /** * * @param root the request is intended to the base URL and no the instance URL * @returns request handler to */ +//TODO: Add token export function useBackendBaseRequest(): useBackendBaseRequestType { - const { url: backend, token: loginToken } = useBackendContext(); - const { request: requestHandler } = useApiContext(); - const token = loginToken?.token; - - const request = useCallback( - function requestImpl( - endpoint: string, - options: RequestOptions = {}, - ): Promise> { - return requestHandler(backend, endpoint, { ...options, token }).then(res => { - return res - }).catch(err => { - throw err - }); - }, - [backend, token], - ); - - return { request }; + const {url: backend} = useBackendContext(); + const {request: requestHandler} = useApiContext(); + //const { token } = useBackendTokenContext(); + const token = "D4CST1Z6AHN3RT03M0T9NSTF2QGHTB5ZD2D3RYZB4HAWG8SX0JEFWBXCKXZHMB7Y3Z7KVFW0B3XPXD5BHCFP8EB0R6CNH2KAWDWVET0"; + + + const request = useCallback( + function requestImpl( + endpoint: string, + //todo: remove + options: RequestOptions = {}, + ): Promise> { + return requestHandler(backend, endpoint, {...options, token}).then(res => { + return res; + }).catch(err => { + throw err; + }); + }, + [backend], + ); + + return {request}; } -export function useBackendInstanceRequest(): useBackendInstanceRequestType { - const { url: rootBackendUrl, token: rootToken } = useBackendContext(); - const { token: instanceToken, id, admin } = useInstanceContext(); - const { request: requestHandler } = useApiContext(); - - const { baseUrl, token: loginToken } = !admin - ? { baseUrl: rootBackendUrl, token: rootToken } - : { baseUrl: `${rootBackendUrl}/instances/${id}`, token: instanceToken }; - - const token = loginToken?.token; - - const request = useCallback( - function requestImpl( - endpoint: string, - options: RequestOptions = {}, - ): Promise> { - return requestHandler(baseUrl, endpoint, { token, ...options }); - }, - [baseUrl, token], - ); - - const multiFetcher = useCallback( - function multiFetcherImpl( - args: [endpoints: string[]], - ): Promise[]> { - const [endpoints] = args - return Promise.all( - endpoints.map((endpoint) => - requestHandler(baseUrl, endpoint, { token }), - ), - ); - }, - [baseUrl, token], - ); - - const fetcher = useCallback( - function fetcherImpl(endpoint: string): Promise> { - return requestHandler(baseUrl, endpoint, { token }); - }, - [baseUrl, token], - ); - - const orderFetcher = useCallback( - function orderFetcherImpl( - args: [endpoint: string, - paid?: YesOrNo, - refunded?: YesOrNo, - wired?: YesOrNo, - searchDate?: Date, - delta?: number,] - ): Promise> { - const [endpoint, paid, refunded, wired, searchDate, delta] = args - const date_s = - delta && delta < 0 && searchDate - ? Math.floor(searchDate.getTime() / 1000) + 1 - : searchDate !== undefined ? Math.floor(searchDate.getTime() / 1000) : undefined; - const params: any = {}; - if (paid !== undefined) params.paid = paid; - if (delta !== undefined) params.delta = delta; - if (refunded !== undefined) params.refunded = refunded; - if (wired !== undefined) params.wired = wired; - if (date_s !== undefined) params.date_s = date_s; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { orders: [] } as T, - }) - } - return requestHandler(baseUrl, endpoint, { params, token }); - }, - [baseUrl, token], - ); - - const reserveDetailFetcher = useCallback( - function reserveDetailFetcherImpl( - endpoint: string, - ): Promise> { - return requestHandler(baseUrl, endpoint, { - params: { - rewards: "yes", + +export function useBackendRequest(): useBackendInstanceRequestType { + const {url: rootBackendUrl} = useBackendContext(); + // const {id} = useInstanceContext(); + const {request: requestHandler} = useApiContext(); + + //TODO: check + const baseUrl = "http://localhost:8083/"; + const token = "D4CST1Z6AHN3RT03M0T9NSTF2QGHTB5ZD2D3RYZB4HAWG8SX0JEFWBXCKXZHMB7Y3Z7KVFW0B3XPXD5BHCFP8EB0R6CNH2KAWDWVET0"; + + + + + const request = useCallback( + function requestImpl( + endpoint: string, + options: RequestOptions = {}, + ): Promise> { + return requestHandler(baseUrl, endpoint, {...options, token}); }, - token, - }); - }, - [baseUrl, token], - ); - - const rewardsDetailFetcher = useCallback( - function rewardsDetailFetcherImpl( - endpoint: string, - ): Promise> { - return requestHandler(baseUrl, endpoint, { - params: { - pickups: "yes", + [baseUrl], + ); + + const multiFetcher = useCallback( + function multiFetcherImpl( + params: string[], + options: RequestOptions = {}, + ): Promise[]> { + return Promise.all( + params.map((endpoint) => + requestHandler(baseUrl, endpoint, {...options, token}), + ), + ); }, - token, - }); - }, - [baseUrl, token], - ); - - const transferFetcher = useCallback( - function transferFetcherImpl( - args: [endpoint: string, - payto_uri?: string, - verified?: string, - position?: string, - delta?: number,] - ): Promise> { - const [endpoint, payto_uri, verified, position, delta] = args - const params: any = {}; - if (payto_uri !== undefined) params.payto_uri = payto_uri; - if (verified !== undefined) params.verified = verified; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { transfers: [] } as T, - }) - } - if (delta !== undefined) { - params.limit = delta; - } - if (position !== undefined) params.offset = position; - - return requestHandler(baseUrl, endpoint, { params, token }); - }, - [baseUrl, token], - ); - - const templateFetcher = useCallback( - function templateFetcherImpl( - args: [endpoint: string, - position?: string, - delta?: number,] - ): Promise> { - const [endpoint, position, delta] = args - const params: any = {}; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { templates: [] } as T, - }) - } - if (delta !== undefined) { - params.limit = delta; - } - if (position !== undefined) params.offset = position; - - return requestHandler(baseUrl, endpoint, { params, token }); - }, - [baseUrl, token], - ); - - const webhookFetcher = useCallback( - function webhookFetcherImpl( - args: [endpoint: string, - position?: string, - delta?: number,] - ): Promise> { - const [endpoint, position, delta] = args - const params: any = {}; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { webhooks: [] } as T, - }) - } - if (delta !== undefined) { - params.limit = delta; - } - if (position !== undefined) params.offset = position; - - return requestHandler(baseUrl, endpoint, { params, token }); - }, - [baseUrl, token], - ); - - return { - request, - fetcher, - multiFetcher, - orderFetcher, - reserveDetailFetcher, - rewardsDetailFetcher, - transferFetcher, - templateFetcher, - webhookFetcher, - }; -} + [baseUrl], + ); + + const fetcher = useCallback( + function fetcherImpl(endpoint: string): Promise> { + return requestHandler(baseUrl, endpoint, {token}); + }, + [baseUrl], + ); + + const depositConfirmationFetcher = useCallback( + function orderFetcherImpl( + args: [endpoint: string, + ], + ): Promise> { + const [endpoint] = args; + const params: any = {"token": "D4CST1Z6AHN3RT03M0T9NSTF2QGHTB5ZD2D3RYZB4HAWG8SX0JEFWBXCKXZHMB7Y3Z7KVFW0B3XPXD5BHCFP8EB0R6CNH2KAWDWVET0"}; + return requestHandler(baseUrl, endpoint, {params, token}); + }, + [baseUrl], + ); + + + return { + request, + fetcher, + depositConfirmationFetcher, + multiFetcher + }; +} \ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/hooks/critical.ts b/packages/auditor-backoffice-ui/src/hooks/critical.ts new file mode 100644 index 000000000..6a25d3037 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/critical.ts @@ -0,0 +1,70 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ +import { + HttpResponse, + HttpResponseOk, + HttpResponsePaginated, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { useEffect, useState } from "preact/hooks"; +import { AuditorBackend, WithId } from "../declaration.js"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; +import { useBackendRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook, useSWRConfig } from "swr"; + +const useSWR = _useSWR as unknown as SWRHook; + +type YesOrNo = "yes" | "no"; + +export interface HelperDashboardFilter { + finance?: YesOrNo; + security?: YesOrNo; + operating?: YesOrNo; + detail?: YesOrNo; +} + +export function getCriticalData( + args?: HelperDashboardFilter, + updateFilter?: (d: Date) => void, +): HttpResponse { + const { multiFetcher } = useBackendRequest(); + const endpoints = [ + "monitoring/fee-time-inconsistency", + "monitoring/emergency", + "monitoring/emergency-by-count", + "monitoring/reserve-balance-insufficient-inconsistency", + ]; + + + const { data: list, error: listError } = useSWR< + HttpResponseOk[], RequestError + >(endpoints, multiFetcher, { + refreshInterval: 60, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (listError) return listError.cause; + + if (list) { + return { ok: true, data: [list] }; + } + return { loading: true }; +} \ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts b/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts deleted file mode 100644 index e4ec9a2f2..000000000 --- a/packages/auditor-backoffice-ui/src/hooks/deposit_confirmations.ts +++ /dev/null @@ -1,161 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ -import { - HttpResponse, - HttpResponseOk, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { AuditorBackend, MerchantBackend, WithId } from "../declaration.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; - -// FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, useSWRConfig } from "swr"; -const useSWR = _useSWR as unknown as SWRHook; - -export interface DepositConfirmationAPI { - getDepositConfirmation: ( - id: string, - ) => Promise; - createDepositConfirmation: ( - data: MerchantBackend.Products.ProductAddDetail, - ) => Promise; - updateDepositConfirmation: ( - id: string, - data: MerchantBackend.Products.ProductPatchDetail, - ) => Promise; - deleteDepositConfirmation: (id: string) => Promise; -} - -export function useDepositConfirmationAPI(): DepositConfirmationAPI { - const mutateAll = useMatchMutate(); - const { mutate } = useSWRConfig(); - - const { request } = useBackendInstanceRequest(); - - const createDepositConfirmation = async ( - data: MerchantBackend.Products.ProductAddDetail, - ): Promise => { - const res = await request(`/private/products`, { - method: "POST", - data, - }); - - return await mutateAll(/.*\/private\/products.*/); - }; - - const updateDepositConfirmation = async ( - productId: string, - data: MerchantBackend.Products.ProductPatchDetail, - ): Promise => { - const r = await request(`/private/products/${productId}`, { - method: "PATCH", - data, - }); - - return await mutateAll(/.*\/private\/products.*/); - }; - - const deleteDepositConfirmation = async (productId: string): Promise => { - await request(`/private/products/${productId}`, { - method: "DELETE", - }); - await mutate([`/private/products`]); - }; - - const getDepositConfirmation = async ( - serialId: string, - ): Promise => { - await request(`/deposit-confirmation/${serialId}`, { - method: "GET", - }); - - return - }; - - return {createDepositConfirmation, updateDepositConfirmation, deleteDepositConfirmation, getDepositConfirmation}; -} - -export function useDepositConfirmation(): HttpResponse< - (AuditorBackend.DepositConfirmation.DepositConfirmationDetail & WithId)[], - AuditorBackend.ErrorDetail -> { - const { fetcher, multiFetcher } = useBackendInstanceRequest(); - - const { data: list, error: listError } = useSWR< - HttpResponseOk, - RequestError - >([`/deposit-confirmation`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - const paths = (list?.data.depositConfirmations || []).map( - (p) => `/deposit-confirmation/${p.serial_id}`, - ); - const { data: depositConfirmations, error: depositConfirmationError } = useSWR< - HttpResponseOk[], - RequestError - >([paths], multiFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (listError) return listError.cause; - if (depositConfirmationError) return depositConfirmationError.cause; - - if (depositConfirmations) { - const dataWithId = depositConfirmations.map((d) => { - //take the id from the queried url - return { - ...d.data, - id: d.info?.url.replace(/.*\/deposit-confirmation\//, "") || "", - }; - }); - return { ok: true, data: dataWithId }; - } - return { loading: true }; -} - -export function useDepositConfirmationDetails( - serialId: string, -): HttpResponse< - AuditorBackend.DepositConfirmation.DepositConfirmationDetail, - AuditorBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk, - RequestError - >([`/deposit-confirmation/${serialId}`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} diff --git a/packages/auditor-backoffice-ui/src/hooks/entity.ts b/packages/auditor-backoffice-ui/src/hooks/entity.ts new file mode 100644 index 000000000..ae62da35e --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/entity.ts @@ -0,0 +1,82 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ +import { + HttpResponse, + HttpResponseOk, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { AuditorBackend, WithId } from "../declaration.js"; +import { useBackendRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook, useSWRConfig } from "swr"; +import { useEntityContext } from "../context/entity.js"; + +const useSWR = _useSWR as unknown as SWRHook; + +type YesOrNo = "yes" | "no"; + +interface Props { + endpoint: string; + entity: any; +} + +export function getEntityList({ endpoint, entity }: Props): HttpResponse { + const { fetcher } = useBackendRequest(); + + const { data: list, error: listError } = useSWR< + HttpResponseOk, + RequestError + >([`monitoring/` + endpoint], fetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (listError) return listError.cause; + + if (list?.data != null) { + return { ok: true, data: [list?.data] }; + } + return { loading: true }; +} +export interface EntityAPI { + updateEntity: ( + id: string + ) => Promise; +} + +export function useEntityAPI(): EntityAPI { + const mutateAll = useMatchMutate(); + const { request } = useBackendRequest(); + const { endpoint } = useEntityContext(); + const data = {"suppressed": true}; + + const updateEntity = async ( + id: string, + ): Promise => { + const r = await request(`monitoring/${endpoint}/${id}`, { + method: "PATCH", + data, + }); + + return await mutateAll(/.*\/monitoring.*/); + }; + + return { updateEntity }; +} \ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/hooks/finance.ts b/packages/auditor-backoffice-ui/src/hooks/finance.ts new file mode 100644 index 000000000..97bf2577f --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/finance.ts @@ -0,0 +1,61 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ +import { + HttpResponse, + HttpResponseOk, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { AuditorBackend, WithId } from "../declaration.js"; +import { useBackendRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook, useSWRConfig } from "swr"; + +const useSWR = _useSWR as unknown as SWRHook; + + +export function getKeyFiguresData(): HttpResponse { + const { multiFetcher } = useBackendRequest(); + const endpoints = [ + "monitoring/misattribution-in-inconsistency", + "monitoring/coin-inconsistency", + "monitoring/reserve-in-inconsistency", + "monitoring/bad-sig-losses", + "monitoring/balances", + "monitoring/amount-arithmetic-inconsistency", + "monitoring/wire-format-inconsistency", + "monitoring/wire-out-inconsistency", + "monitoring/reserve-balance-summary-wrong-inconsistency", + + ]; + + const { data: list, error: listError } = useSWR< + HttpResponseOk[], RequestError + >(endpoints, multiFetcher, { + refreshInterval: 60, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (listError) return listError.cause; + + if (list) { + return { ok: true, data: [list] }; + } + return { loading: true }; +} diff --git a/packages/auditor-backoffice-ui/src/hooks/index.ts b/packages/auditor-backoffice-ui/src/hooks/index.ts index 61afbc94a..cf1c57771 100644 --- a/packages/auditor-backoffice-ui/src/hooks/index.ts +++ b/packages/auditor-backoffice-ui/src/hooks/index.ts @@ -14,138 +14,66 @@ GNU Taler; see the file COPYING. If not, see */ -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { buildCodecForObject, codecForMap, codecForString, codecForTimestamp } from "@gnu-taler/taler-util"; -import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; -import { StateUpdater, useEffect, useState } from "preact/hooks"; -import { LoginToken } from "../declaration.js"; +import {StateUpdater, useState} from "preact/hooks"; import { ValueOrFunction } from "../utils/types.js"; -import { useMatchMutate } from "./backend.js"; - -const calculateRootPath = () => { - const rootPath = - typeof window !== undefined - ? window.location.origin + window.location.pathname - : "/"; - - /** - * By default, merchant backend serves the html content - * from the /webui root. This should cover most of the - * cases and the rootPath will be the merchant backend - * URL where the instances are - */ - return rootPath.replace("/webui/", ""); -}; - -const loginTokenCodec = buildCodecForObject() - .property("token", codecForString()) - .property("expiration", codecForTimestamp) - .build("loginToken") -const TOKENS_KEY = buildStorageKey("auditor-token", codecForMap(loginTokenCodec)); - export function useBackendURL( - url?: string, + url?: string, ): [string, StateUpdater] { - const [value, setter] = useSimpleLocalStorage( - "auditor-base-url", - url || calculateRootPath(), - ); - - const checkedSetter = (v: ValueOrFunction) => { - return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, "")); - }; - - return [value!, checkedSetter]; -} + const [value, setter] = useSimpleLocalStorage( + "auditor-base-url", + url || calculateRootPath(), + ); -export function useBackendDefaultToken( -): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] { - const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {}) + const checkedSetter = (v: ValueOrFunction) => { + return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, "")); + }; - const tokenOfDefaultInstance = tokenMap["default"] - const clearCache = useMatchMutate() - useEffect(() => { - clearCache() - }, [tokenOfDefaultInstance]) - - function updateToken( - value: (LoginToken | undefined) - ): void { - if (value === undefined) { - reset() - } else { - const res = { ...tokenMap, "default": value } - setToken(res) - } - } - return [tokenMap["default"], updateToken]; -} - -export function useBackendInstanceToken( - id: string, -): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] { - const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {}) - const [defaultToken, defaultSetToken] = useBackendDefaultToken(); - - // instance named 'default' use the default token - if (id === "default") { - return [defaultToken, defaultSetToken]; - } - function updateToken( - value: (LoginToken | undefined) - ): void { - if (value === undefined) { - reset() - } else { - const res = { ...tokenMap, [id]: value } - setToken(res) - } - } - - return [tokenMap[id], updateToken]; + return [value!, checkedSetter]; } -export function useLang(initial?: string): [string, StateUpdater] { - const browserLang = - typeof window !== "undefined" - ? navigator.language || (navigator as any).userLanguage - : undefined; - const defaultLang = (browserLang || initial || "en").substring(0, 2); - return useSimpleLocalStorage("lang-preference", defaultLang) as [string, StateUpdater]; -} +const calculateRootPath = () => { + const rootPath = + typeof window !== undefined + ? window.location.origin + window.location.pathname + : "/"; + + /** + * By default, auditor backend serves the html content + * from the /webui root. This should cover most of the + * cases and the rootPath will be the auditor backend + * URL where the instances are + */ + return rootPath.replace("/webui/", ""); +}; export function useSimpleLocalStorage( - key: string, - initialValue?: string, + key: string, + initialValue?: string, ): [string | undefined, StateUpdater] { - const [storedValue, setStoredValue] = useState( - (): string | undefined => { - return typeof window !== "undefined" - ? window.localStorage.getItem(key) || initialValue - : initialValue; - }, - ); - - const setValue = ( - value?: string | ((val?: string) => string | undefined), - ) => { - setStoredValue((p) => { - const toStore = value instanceof Function ? value(p) : value; - if (typeof window !== "undefined") { - if (!toStore) { - window.localStorage.removeItem(key); - } else { - window.localStorage.setItem(key, toStore); - } - } - return toStore; - }); - }; - - return [storedValue, setValue]; -} + const [storedValue, setStoredValue] = useState( + (): string | undefined => { + return typeof window !== "undefined" + ? window.localStorage.getItem(key) || initialValue + : initialValue; + }, + ); + + const setValue = ( + value?: string | ((val?: string) => string | undefined), + ) => { + setStoredValue((p) => { + const toStore = value instanceof Function ? value(p) : value; + if (typeof window !== "undefined") { + if (!toStore) { + window.localStorage.removeItem(key); + } else { + window.localStorage.setItem(key, toStore); + } + } + return toStore; + }); + }; + + return [storedValue, setValue]; +} \ No newline at end of file diff --git a/packages/auditor-backoffice-ui/src/hooks/operational.ts b/packages/auditor-backoffice-ui/src/hooks/operational.ts new file mode 100644 index 000000000..89524f24e --- /dev/null +++ b/packages/auditor-backoffice-ui/src/hooks/operational.ts @@ -0,0 +1,83 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ +import { + HttpResponse, + HttpResponseOk, + RequestError, +} from "@gnu-taler/web-util/browser"; +import { AuditorBackend, WithId } from "../declaration.js"; +import { useBackendRequest, useMatchMutate } from "./backend.js"; + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import _useSWR, { SWRHook, useSWRConfig } from "swr"; + +const useSWR = _useSWR as unknown as SWRHook; + +type YesOrNo = "yes" | "no"; + +export interface HelperDashboardFilter { + finance?: YesOrNo; + security?: YesOrNo; + operating?: YesOrNo; + detail?: YesOrNo; +} + +export function getOperationData( + args?: HelperDashboardFilter, + updateFilter?: (d: Date) => void, +): HttpResponse { + const { multiFetcher } = useBackendRequest(); + const endpoints = [ + "monitoring/row-inconsistency", + "monitoring/purse-not-closed-inconsistencies", + "monitoring/reserve-not-closed-inconsistency", + "monitoring/denominations-without-sigs", + "monitoring/deposit-confirmation", + "monitoring/denomination-key-validity-withdraw-inconsistency", + "monitoring/refreshes-hanging", + //TODO fix endpoint + // "monitoring/closure-lags", + // "monitoring/row-minor-inconsistencies", + // "monitoring/historic-denomination-revenue", + // "monitoring/denomination-pending", + "monitoring/historic-reserve-summary", + + ]; + + + const { data: list, error: listError } = useSWR< + HttpResponseOk[], RequestError + >(endpoints, multiFetcher, { + refreshInterval: 60, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }); + + if (listError) return listError.cause; + + if (list) { + return { ok: true, data: [list] }; + } + return { loading: true }; +} + +export interface EntityAPI { + updateEntity: ( + id: string, + ) => Promise; +} \ No newline at end of file -- cgit v1.2.3