diff --git a/components/logger.js b/components/logger.js index 9f2923c1..45ac1607 100644 --- a/components/logger.js +++ b/components/logger.js @@ -1,11 +1,6 @@ -import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { useMe } from './me' import fancyNames from '@/lib/fancy-names.json' -import { gql, useMutation, useQuery } from '@apollo/client' -import { WALLET_LOGS } from '@/fragments/wallet' -import { getWalletBy } from '@/lib/constants' -// TODO: why can't I import this without errors? -// import { getWalletByName } from './wallet' const generateFancyName = () => { // 100 adjectives * 100 nouns * 10000 = 100M possible names @@ -46,9 +41,7 @@ export const LoggerContext = createContext() export const LoggerProvider = ({ children }) => { return ( - - {children} - + {children} ) } @@ -124,191 +117,3 @@ function ServiceWorkerLoggerProvider ({ children }) { export function useServiceWorkerLogger () { return useContext(ServiceWorkerLoggerContext) } - -const WalletLoggerContext = createContext() -const WalletLogsContext = createContext() - -const initIndexedDB = async (dbName, storeName) => { - return new Promise((resolve, reject) => { - if (!window.indexedDB) { - return reject(new Error('IndexedDB not supported')) - } - - // https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB - const request = window.indexedDB.open(dbName, 1) - - let db - request.onupgradeneeded = () => { - // this only runs if version was changed during open - db = request.result - if (!db.objectStoreNames.contains(storeName)) { - const objectStore = db.createObjectStore(storeName, { autoIncrement: true }) - objectStore.createIndex('ts', 'ts') - objectStore.createIndex('wallet_ts', ['wallet', 'ts']) - } - } - - request.onsuccess = () => { - // this gets called after onupgradeneeded finished - db = request.result - resolve(db) - } - - request.onerror = () => { - reject(new Error('failed to open IndexedDB')) - } - }) -} - -const WalletLoggerProvider = ({ children }) => { - const me = useMe() - const [logs, setLogs] = useState([]) - let dbName = 'app:storage' - if (me) { - dbName = `${dbName}:${me.id}` - } - const idbStoreName = 'wallet_logs' - const idb = useRef() - const logQueue = useRef([]) - - useQuery(WALLET_LOGS, { - fetchPolicy: 'network-only', - // required to trigger onCompleted on refetches - notifyOnNetworkStatusChange: true, - onCompleted: ({ walletLogs }) => { - setLogs((prevLogs) => { - const existingIds = prevLogs.map(({ id }) => id) - const logs = walletLogs - .filter(({ id }) => !existingIds.includes(id)) - .map(({ createdAt, wallet: walletType, ...log }) => { - return { - ts: +new Date(createdAt), - wallet: getWalletBy('type', walletType).logTag, - ...log - } - }) - return [...prevLogs, ...logs].sort((a, b) => a.ts - b.ts) - }) - } - }) - - const [deleteServerWalletLogs] = useMutation( - gql` - mutation deleteWalletLogs($wallet: String) { - deleteWalletLogs(wallet: $wallet) - } - `, - { - onCompleted: (_, { variables: { wallet: walletType } }) => { - setLogs((logs) => { - return logs.filter(l => walletType ? l.wallet !== getWalletBy('type', walletType).logTag : false) - }) - } - } - ) - - const saveLog = useCallback((log) => { - if (!idb.current) { - // IDB may not be ready yet - return logQueue.current.push(log) - } - const tx = idb.current.transaction(idbStoreName, 'readwrite') - const request = tx.objectStore(idbStoreName).add(log) - request.onerror = () => console.error('failed to save log:', log) - }, []) - - useEffect(() => { - initIndexedDB(dbName, idbStoreName) - .then(db => { - idb.current = db - - // load all logs from IDB - const tx = idb.current.transaction(idbStoreName, 'readonly') - const store = tx.objectStore(idbStoreName) - const index = store.index('ts') - const request = index.getAll() - request.onsuccess = () => { - const logs = request.result - setLogs((prevLogs) => { - // sort oldest first to keep same order as logs are appended - return [...prevLogs, ...logs].sort((a, b) => a.ts - b.ts) - }) - } - - // flush queued logs to IDB - logQueue.current.forEach(q => { - const isLog = !!q.wallet - if (isLog) saveLog(q) - }) - - logQueue.current = [] - }) - .catch(console.error) - return () => idb.current?.close() - }, []) - - const appendLog = useCallback((walletName, level, message) => { - const log = { wallet: walletName, level, message, ts: +new Date() } - saveLog(log) - setLogs((prevLogs) => [...prevLogs, log]) - }, [saveLog]) - - const deleteLogs = useCallback(async (walletName) => { - const wallet = getWalletByName(walletName, me) - - if (!walletName || wallet.server) { - await deleteServerWalletLogs({ variables: { wallet: wallet?.type } }) - } - if (!walletName || !wallet.server) { - const tx = idb.current.transaction(idbStoreName, 'readwrite') - const objectStore = tx.objectStore(idbStoreName) - const idx = objectStore.index('wallet_ts') - const request = walletName ? idx.openCursor(window.IDBKeyRange.bound([walletName, -Infinity], [walletName, Infinity])) : idx.openCursor() - request.onsuccess = function (event) { - const cursor = event.target.result - if (cursor) { - cursor.delete() - cursor.continue() - } else { - // finished - setLogs((logs) => logs.filter(l => walletName ? l.wallet !== walletName : false)) - } - } - } - }, [me, setLogs]) - - return ( - - - {children} - - - ) -} - -export function useWalletLogger (walletName) { - const { appendLog, deleteLogs: innerDeleteLogs } = useContext(WalletLoggerContext) - - const log = useCallback(level => message => { - // TODO: - // also send this to us if diagnostics was enabled, - // very similar to how the service worker logger works. - appendLog(walletName, level, message) - console[level !== 'error' ? 'info' : 'error'](`[${walletName}]`, message) - }, [appendLog, walletName]) - - const logger = useMemo(() => ({ - ok: (...message) => log('ok')(message.join(' ')), - info: (...message) => log('info')(message.join(' ')), - error: (...message) => log('error')(message.join(' ')) - }), [log, walletName]) - - const deleteLogs = useCallback((w) => innerDeleteLogs(w || walletName), [innerDeleteLogs, walletName]) - - return { logger, deleteLogs } -} - -export function useWalletLogs (walletName) { - const logs = useContext(WalletLogsContext) - return logs.filter(l => !walletName || l.wallet === walletName) -} diff --git a/components/nav/common.js b/components/nav/common.js index 667a34db..d04cf3f2 100644 --- a/components/nav/common.js +++ b/components/nav/common.js @@ -22,8 +22,7 @@ import SearchIcon from '../../svgs/search-line.svg' import classNames from 'classnames' import SnIcon from '@/svgs/sn.svg' import { useHasNewNotes } from '../use-has-new-notes' -import { useWalletLogger } from '@/components/logger' -// import { useWallet } from '@/components/wallet' +import { useWalletLogger } from '@/components/wallet-logger' export function Brand ({ className }) { return ( diff --git a/components/wallet-logger.js b/components/wallet-logger.js new file mode 100644 index 00000000..27993913 --- /dev/null +++ b/components/wallet-logger.js @@ -0,0 +1,314 @@ +import { useRouter } from 'next/router' +import LogMessage from './log-message' +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' +import { Checkbox, Form } from './form' +import { useField } from 'formik' +import styles from '@/styles/log.module.css' +import { Button } from 'react-bootstrap' +import { useToast } from './toast' +import { useShowModal } from './modal' +import { WALLET_LOGS } from '@/fragments/wallet' +import { getWalletByName } from './wallet' +import { gql, useMutation, useQuery } from '@apollo/client' +import { useMe } from './me' + +const FollowCheckbox = ({ value, ...props }) => { + const [,, helpers] = useField(props.name) + + useEffect(() => { + helpers.setValue(value) + }, [value]) + + return ( + + ) +} + +export function WalletLogs ({ wallet, embedded }) { + const logs = useWalletLogs(wallet) + + const router = useRouter() + const { follow: defaultFollow } = router.query + const [follow, setFollow] = useState(defaultFollow ?? true) + const tableRef = useRef() + const scrollY = useRef() + const showModal = useShowModal() + + useEffect(() => { + if (follow) { + tableRef.current?.scroll({ top: tableRef.current.scrollHeight, behavior: 'smooth' }) + } + }, [logs, follow]) + + useEffect(() => { + function onScroll (e) { + const y = e.target.scrollTop + + const down = y - scrollY.current >= -1 + if (!!scrollY.current && !down) { + setFollow(false) + } + + const maxY = e.target.scrollHeight - e.target.clientHeight + const dY = maxY - y + const isBottom = dY >= -1 && dY <= 1 + if (isBottom) { + setFollow(true) + } + + scrollY.current = y + } + tableRef.current?.addEventListener('scroll', onScroll) + return () => tableRef.current?.removeEventListener('scroll', onScroll) + }, []) + + return ( + <> +
+
+ + + { + showModal(onClose => ) + }} + >clear + +
+
+
------ start of logs ------
+ {logs.length === 0 &&
empty
} + + + {logs.map((log, i) => )} + +
+
+ + ) +} + +function DeleteWalletLogsObstacle ({ wallet, onClose }) { + const toaster = useToast() + const { deleteLogs } = useWalletLogger(wallet) + + const prompt = `Do you really want to delete all ${wallet ? '' : 'wallet'} logs ${wallet ? 'of this wallet' : ''}?` + return ( +
+ {prompt} +
+ cancel + +
+
+ ) +} + +const WalletLoggerContext = createContext() +const WalletLogsContext = createContext() + +const initIndexedDB = async (dbName, storeName) => { + return new Promise((resolve, reject) => { + if (!window.indexedDB) { + return reject(new Error('IndexedDB not supported')) + } + + // https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB + const request = window.indexedDB.open(dbName, 1) + + let db + request.onupgradeneeded = () => { + // this only runs if version was changed during open + db = request.result + if (!db.objectStoreNames.contains(storeName)) { + const objectStore = db.createObjectStore(storeName, { autoIncrement: true }) + objectStore.createIndex('ts', 'ts') + objectStore.createIndex('wallet_ts', ['wallet', 'ts']) + } + } + + request.onsuccess = () => { + // this gets called after onupgradeneeded finished + db = request.result + resolve(db) + } + + request.onerror = () => { + reject(new Error('failed to open IndexedDB')) + } + }) +} + +export const WalletLoggerProvider = ({ children }) => { + const me = useMe() + const [logs, setLogs] = useState([]) + let dbName = 'app:storage' + if (me) { + dbName = `${dbName}:${me.id}` + } + const idbStoreName = 'wallet_logs' + const idb = useRef() + const logQueue = useRef([]) + + useQuery(WALLET_LOGS, { + fetchPolicy: 'network-only', + // required to trigger onCompleted on refetches + notifyOnNetworkStatusChange: true, + onCompleted: ({ walletLogs }) => { + setLogs((prevLogs) => { + const existingIds = prevLogs.map(({ id }) => id) + const logs = walletLogs + .filter(({ id }) => !existingIds.includes(id)) + .map(({ createdAt, wallet: walletType, ...log }) => { + return { + ts: +new Date(createdAt), + // TODO: use wallet defs + // wallet: getWalletBy('type', walletType).logTag, + ...log + } + }) + return [...prevLogs, ...logs].sort((a, b) => a.ts - b.ts) + }) + } + }) + + const [deleteServerWalletLogs] = useMutation( + gql` + mutation deleteWalletLogs($wallet: String) { + deleteWalletLogs(wallet: $wallet) + } + `, + { + onCompleted: (_, { variables: { wallet: walletType } }) => { + setLogs((logs) => { + // TODO: use wallet defs + return logs.filter(l => walletType ? l.wallet !== getWalletByName('type', walletType) : false) + }) + } + } + ) + + const saveLog = useCallback((log) => { + if (!idb.current) { + // IDB may not be ready yet + return logQueue.current.push(log) + } + const tx = idb.current.transaction(idbStoreName, 'readwrite') + const request = tx.objectStore(idbStoreName).add(log) + request.onerror = () => console.error('failed to save log:', log) + }, []) + + useEffect(() => { + initIndexedDB(dbName, idbStoreName) + .then(db => { + idb.current = db + + // load all logs from IDB + const tx = idb.current.transaction(idbStoreName, 'readonly') + const store = tx.objectStore(idbStoreName) + const index = store.index('ts') + const request = index.getAll() + request.onsuccess = () => { + const logs = request.result + setLogs((prevLogs) => { + // sort oldest first to keep same order as logs are appended + return [...prevLogs, ...logs].sort((a, b) => a.ts - b.ts) + }) + } + + // flush queued logs to IDB + logQueue.current.forEach(q => { + const isLog = !!q.wallet + if (isLog) saveLog(q) + }) + + logQueue.current = [] + }) + .catch(console.error) + return () => idb.current?.close() + }, []) + + const appendLog = useCallback((walletName, level, message) => { + const log = { wallet: walletName, level, message, ts: +new Date() } + saveLog(log) + setLogs((prevLogs) => [...prevLogs, log]) + }, [saveLog]) + + const deleteLogs = useCallback(async (walletName) => { + const wallet = getWalletByName(walletName, me) + + if (!walletName || wallet.server) { + await deleteServerWalletLogs({ variables: { wallet: wallet?.type } }) + } + if (!walletName || !wallet.server) { + const tx = idb.current.transaction(idbStoreName, 'readwrite') + const objectStore = tx.objectStore(idbStoreName) + const idx = objectStore.index('wallet_ts') + const request = walletName ? idx.openCursor(window.IDBKeyRange.bound([walletName, -Infinity], [walletName, Infinity])) : idx.openCursor() + request.onsuccess = function (event) { + const cursor = event.target.result + if (cursor) { + cursor.delete() + cursor.continue() + } else { + // finished + setLogs((logs) => logs.filter(l => walletName ? l.wallet !== walletName : false)) + } + } + } + }, [me, setLogs]) + + return ( + + + {children} + + + ) +} + +export function useWalletLogger (walletName) { + const { appendLog, deleteLogs: innerDeleteLogs } = useContext(WalletLoggerContext) + + const log = useCallback(level => message => { + // TODO: + // also send this to us if diagnostics was enabled, + // very similar to how the service worker logger works. + appendLog(walletName, level, message) + console[level !== 'error' ? 'info' : 'error'](`[${walletName}]`, message) + }, [appendLog, walletName]) + + const logger = useMemo(() => ({ + ok: (...message) => log('ok')(message.join(' ')), + info: (...message) => log('info')(message.join(' ')), + error: (...message) => log('error')(message.join(' ')) + }), [log, walletName]) + + const deleteLogs = useCallback((w) => innerDeleteLogs(w || walletName), [innerDeleteLogs, walletName]) + + return { logger, deleteLogs } +} + +export function useWalletLogs (walletName) { + const logs = useContext(WalletLogsContext) + return logs.filter(l => !walletName || l.wallet === walletName) +} diff --git a/components/wallet-logs.js b/components/wallet-logs.js deleted file mode 100644 index c5cb678d..00000000 --- a/components/wallet-logs.js +++ /dev/null @@ -1,121 +0,0 @@ -import { useRouter } from 'next/router' -import LogMessage from './log-message' -import { useWalletLogger, useWalletLogs } from './logger' -import { useEffect, useRef, useState } from 'react' -import { Checkbox, Form } from './form' -import { useField } from 'formik' -import styles from '@/styles/log.module.css' -import { Button } from 'react-bootstrap' -import { useToast } from './toast' -import { useShowModal } from './modal' - -const FollowCheckbox = ({ value, ...props }) => { - const [,, helpers] = useField(props.name) - - useEffect(() => { - helpers.setValue(value) - }, [value]) - - return ( - - ) -} - -export default function WalletLogs ({ wallet, embedded }) { - const logs = useWalletLogs(wallet) - - const router = useRouter() - const { follow: defaultFollow } = router.query - const [follow, setFollow] = useState(defaultFollow ?? true) - const tableRef = useRef() - const scrollY = useRef() - const showModal = useShowModal() - - useEffect(() => { - if (follow) { - tableRef.current?.scroll({ top: tableRef.current.scrollHeight, behavior: 'smooth' }) - } - }, [logs, follow]) - - useEffect(() => { - function onScroll (e) { - const y = e.target.scrollTop - - const down = y - scrollY.current >= -1 - if (!!scrollY.current && !down) { - setFollow(false) - } - - const maxY = e.target.scrollHeight - e.target.clientHeight - const dY = maxY - y - const isBottom = dY >= -1 && dY <= 1 - if (isBottom) { - setFollow(true) - } - - scrollY.current = y - } - tableRef.current?.addEventListener('scroll', onScroll) - return () => tableRef.current?.removeEventListener('scroll', onScroll) - }, []) - - return ( - <> -
-
- - - { - showModal(onClose => ) - }} - >clear - -
-
-
------ start of logs ------
- {logs.length === 0 &&
empty
} - - - {logs.map((log, i) => )} - -
-
- - ) -} - -function DeleteWalletLogsObstacle ({ wallet, onClose }) { - const toaster = useToast() - const { deleteLogs } = useWalletLogger(wallet) - - const prompt = `Do you really want to delete all ${wallet ? '' : 'wallet'} logs ${wallet ? 'of this wallet' : ''}?` - return ( -
- {prompt} -
- cancel - -
-
- ) -} diff --git a/components/wallet/index.js b/components/wallet/index.js index fbddb07f..321a14ea 100644 --- a/components/wallet/index.js +++ b/components/wallet/index.js @@ -1,7 +1,7 @@ import { useCallback } from 'react' import { useMe } from '@/components/me' import useLocalState from '@/components/use-local-state' -import { useWalletLogger } from '@/components/logger' +import { useWalletLogger } from '@/components/wallet-logger' import { SSR } from '@/lib/constants' // wallet definitions diff --git a/pages/_app.js b/pages/_app.js index 82bd105a..9161017e 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -17,6 +17,7 @@ import { SSR } from '@/lib/constants' import NProgress from 'nprogress' import 'nprogress/nprogress.css' import { LoggerProvider } from '@/components/logger' +import { WalletLoggerProvider } from '@/components/wallet-logger' import { ChainFeeProvider } from '@/components/chain-fee.js' import dynamic from 'next/dynamic' import { HasNewNotesProvider } from '@/components/use-has-new-notes' @@ -104,24 +105,26 @@ export default function MyApp ({ Component, pageProps: { ...props } }) { - - - - - - - - - - {!router?.query?.disablePrompt && } - - - - - - - - + + + + + + + + + + + {!router?.query?.disablePrompt && } + + + + + + + + + diff --git a/pages/settings/wallets/[wallet].js b/pages/settings/wallets/[wallet].js index de6a223d..8c82a35c 100644 --- a/pages/settings/wallets/[wallet].js +++ b/pages/settings/wallets/[wallet].js @@ -4,7 +4,7 @@ import { CenterLayout } from '@/components/layout' import { WalletButtonBar } from '@/components/wallet-card' import { lnbitsSchema } from '@/lib/validate' import { WalletSecurityBanner } from '@/components/banners' -import WalletLogs from '@/components/wallet-logs' +import { WalletLogs } from '@/components/wallet-logger' import { useToast } from '@/components/toast' import { useRouter } from 'next/router' import { useWallet } from '@/components/wallet' diff --git a/pages/wallet/logs.js b/pages/wallet/logs.js index debad4c2..86b540b5 100644 --- a/pages/wallet/logs.js +++ b/pages/wallet/logs.js @@ -1,6 +1,6 @@ import { CenterLayout } from '@/components/layout' import { getGetServerSideProps } from '@/api/ssrApollo' -import WalletLogs from '@/components/wallet-logs' +import { WalletLogs } from '@/components/wallet-logs' export const getServerSideProps = getGetServerSideProps({ query: null })