diff --git a/packages/www/components/WebhookDetails/LogsContainer.tsx b/packages/www/components/WebhookDetails/LogsContainer.tsx index d453656ce4..65b2b77986 100644 --- a/packages/www/components/WebhookDetails/LogsContainer.tsx +++ b/packages/www/components/WebhookDetails/LogsContainer.tsx @@ -6,15 +6,15 @@ import { Flex, Button, } from "@livepeer/design-system"; +import { useEffect, useRef } from "react"; import moment from "moment"; import { CheckIcon, Cross1Icon } from "@radix-ui/react-icons"; import { useState } from "react"; import { WebhookLogs } from "hooks/use-api/types"; import JSONPretty from "react-json-pretty"; import { useApi } from "hooks"; -import { useRouter } from "next/navigation"; import { Webhook } from "@livepeer.studio/api"; -import { FilterType } from "./index"; +import Spinner from "components/Spinner"; const Cell = styled(Text, { py: "$2", @@ -22,41 +22,32 @@ const Cell = styled(Text, { fontSize: "$3", }); +const customTheme = { + key: "color:#606060;line-height:1.8;font-size:14px;", + string: "color:#DABAAB;font-size:14px", + value: "color:#788570;font-size:14px", + boolean: "color:#788570;font-size:14px", +}; + const LogsContainer = ({ data, logs, - filter, refetchLogs, + loadMore, + isLogsLoading, }: { data: Webhook; logs: WebhookLogs[]; - filter: FilterType; refetchLogs(): Promise; + loadMore(): void; + isLogsLoading: boolean; }) => { const { resendWebhook } = useApi(); const [selected, setSelected] = useState(logs[0]); const [isResending, setIsResending] = useState(false); - const succeededLogs = logs?.filter((log) => log.success); - - const failedLogs = logs?.filter((log) => !log.success); - - const router = useRouter(); - - const renderedLogs = - filter === "all" - ? logs - : filter === "succeeded" - ? succeededLogs - : failedLogs; - - const customTheme = { - key: "color:#606060;line-height:1.8;font-size:14px;", - string: "color:#DABAAB;font-size:14px", - value: "color:#788570;font-size:14px", - boolean: "color:#788570;font-size:14px", - }; + const logsContainerRef = useRef(null); const onResend = async (log: WebhookLogs) => { setIsResending(true); @@ -71,6 +62,26 @@ const LogsContainer = ({ } }; + const handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = logsContainerRef.current; + if (scrollTop + clientHeight >= scrollHeight) { + loadMore(); + } + }; + + useEffect(() => { + const scrollContainer = logsContainerRef.current; + if (scrollContainer) { + scrollContainer.addEventListener("scroll", handleScroll); + } + + return () => { + if (scrollContainer) { + scrollContainer.removeEventListener("scroll", handleScroll); + } + }; + }, [loadMore]); + return ( - {renderedLogs.map((log: WebhookLogs, index) => ( + {logs.map((log: WebhookLogs, index) => ( setSelected(log)} key={log.id} @@ -140,6 +152,17 @@ const LogsContainer = ({ ))} + {isLogsLoading && ( + + + + )} { +const WebhookDetails = ({ + id, + data, + logs, + handleLogFilters, + refetchLogs, + loadMore, + isLogsLoading, +}) => { const { deleteWebhook, updateWebhook } = useApi(); const [deleting, setDeleting] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); @@ -90,6 +98,13 @@ const WebhookDetails = ({ id, data, logs, handleLogFilters, refetchLogs }) => { const handleFilterClick = (filter: FilterType) => { setActiveFilter(filter); + handleLogFilters( + filter === "all" + ? [] + : filter === "succeeded" + ? [{ id: "success", value: "true" }] + : [{ id: "success", value: "false" }] + ); }; const revealSecretHandler = () => { @@ -260,12 +275,13 @@ const WebhookDetails = ({ id, data, logs, handleLogFilters, refetchLogs }) => { - {logs.length > 0 ? ( + {logs?.data?.length > 0 ? ( ) : ( { }; const Filters = ({ filters, activeFilter, handleFilterClick, logs }) => { - const totalWebhookLogs = logs?.length; - - const totalSucceededWebhookLogs = logs?.filter((log) => log.success).length; - - const totalFailedWebhookLogs = logs?.filter((log) => !log.success).length; - return ( { color: activeFilter === filter && "$blue11", }}> {filter === "all" - ? totalWebhookLogs + ? logs?.totalCount : filter === "succeeded" - ? totalSucceededWebhookLogs - : totalFailedWebhookLogs} + ? logs?.successCount + : logs?.failedCount} ))} diff --git a/packages/www/hooks/use-api/endpoints/webhook.ts b/packages/www/hooks/use-api/endpoints/webhook.ts index 9349c616ae..08dacb93f5 100644 --- a/packages/www/hooks/use-api/endpoints/webhook.ts +++ b/packages/www/hooks/use-api/endpoints/webhook.ts @@ -105,19 +105,52 @@ export const deleteWebhooks = async (ids: Array): Promise => { export const getWebhookLogs = async ( webhookId, - filters = null -): Promise => { - const f = filters ? JSON.stringify(filters) : undefined; + filters = [], + cursor, + count +) => { + const buildFilters = (additionalFilters) => [ + ...filters, + ...additionalFilters, + ]; - const [res, logs] = await context.fetch( - `/webhook/${webhookId}/log?${qs.stringify({ filters: f })}` - ); - if (res.status !== 200) { - throw logs && typeof logs === "object" - ? { ...logs, status: res.status } - : new Error(logs); - } - return logs; + const fetchLogs = async (fromStatus, additionalFilters = [], limit = 20) => { + const query = qs.stringify({ + limit, + cursor: fromStatus ? null : cursor, + count, + filters: JSON.stringify( + additionalFilters.length > 0 ? buildFilters(additionalFilters) : filters + ), + }); + const [res, data] = await context.fetch( + `/webhook/${webhookId}/log?${query}` + ); + if (res.status !== 200) { + throw data && typeof data === "object" + ? { ...data, status: res.status } + : new Error(data); + } + return { + data, + count: res.headers.get("X-Total-Count"), + cursor: getCursor(res.headers.get("link")), + }; + }; + + const [allLogs, failedLogs, successLogs] = await Promise.all([ + fetchLogs(false, []), + fetchLogs(true, [{ id: "success", value: "false" }], 1), + fetchLogs(true, [{ id: "success", value: "true" }], 1), + ]); + + return { + data: allLogs.data, + cursor: allLogs.cursor, + totalCount: allLogs.count || 0, + failedCount: failedLogs.count || 0, + successCount: successLogs.count || 0, + }; }; export const resendWebhook = async (params: { diff --git a/packages/www/pages/dashboard/developers/webhooks/[id].tsx b/packages/www/pages/dashboard/developers/webhooks/[id].tsx index e461a028e5..ad10a46e12 100644 --- a/packages/www/pages/dashboard/developers/webhooks/[id].tsx +++ b/packages/www/pages/dashboard/developers/webhooks/[id].tsx @@ -9,8 +9,15 @@ import { DashboardWebhooks as Content } from "content"; const WebhookDetail = () => { useLoggedIn(); const { user } = useApi(); - const [logFilters, setLogFilters] = useState(); - + const [logFilters, setLogFilters] = useState([]); + const [logs, setLogs] = useState({ + data: [], + cursor: null, + totalCount: 0, + failedCount: 0, + successCount: 0, + }); + const [loadingMore, setLoadingMore] = useState(false); const { getWebhook, getWebhookLogs } = useApi(); const router = useRouter(); const { id } = router.query; @@ -23,17 +30,61 @@ const WebhookDetail = () => { } ); - const { data: logs, refetch: refetchLogs } = useQuery( + const containsSuccessFilter = (logFilters) => { + return logFilters.some((filter) => filter.id === "success"); + }; + + const { refetch: refetchLogs, isLoading: isLogsLoading } = useQuery( ["webhookLogs", id, logFilters], - () => getWebhookLogs(id, logFilters), + () => + getWebhookLogs( + id, + logFilters, + logFilters.length > 0 ? null : logs.cursor, + true + ), { enabled: !!id, - initialData: [], + onSuccess: (data) => { + const isSuccess = containsSuccessFilter(logFilters); + + if (isSuccess) { + setLogs({ + ...data, + data: loadingMore ? [...logs.data, ...data.data] : data.data, + totalCount: logs.totalCount, + failedCount: logs.failedCount, + successCount: logs.successCount, + }); + } else { + setLogs({ + ...data, + data: loadingMore ? [...logs.data, ...data.data] : data.data, + }); + } + + setLoadingMore(false); + }, } ); const handleLogFilters = async (filters) => { - setLogFilters(filters); + if (filters.length === 0) { + setLogFilters([]); + setLogs({ + ...logs, + cursor: null, + }); + refetchLogs(); + return; + } + + const newFilters = logFilters.filter( + (existingFilter) => + !filters.some((newFilter) => newFilter.id === existingFilter.id) + ); + + setLogFilters([...newFilters, ...filters]); refetchLogs(); }; @@ -41,6 +92,13 @@ const WebhookDetail = () => { return ; } + const loadMore = () => { + if (logs.cursor) { + setLoadingMore(true); + refetchLogs(); + } + }; + return ( { data={webhookData} logs={logs} refetchLogs={refetchLogs} + loadMore={loadMore} + isLogsLoading={isLogsLoading} /> );