Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: infinite scroll on webhook logs page #2082

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
71 changes: 47 additions & 24 deletions packages/www/components/WebhookDetails/LogsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,57 +6,48 @@ 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",
fontFamily: "$mono",
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<void>;
loadMore(): void;
isLogsLoading: boolean;
}) => {
const { resendWebhook } = useApi();

const [selected, setSelected] = useState<WebhookLogs>(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);
Expand All @@ -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 (
<Box
css={{
Expand All @@ -96,12 +107,13 @@ const LogsContainer = ({
LOGS:
</Text>
<Box
ref={logsContainerRef}
css={{
overflowY: "auto",
borderRight: "1px solid $colors$neutral6",
height: "calc(100vh - 450px)",
}}>
{renderedLogs.map((log: WebhookLogs, index) => (
{logs.map((log: WebhookLogs, index) => (
<Box
onClick={() => setSelected(log)}
key={log.id}
Expand Down Expand Up @@ -140,6 +152,17 @@ const LogsContainer = ({
</Cell>
</Box>
))}
{isLogsLoading && (
<Box
css={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: 50,
}}>
<Spinner />
</Box>
)}
</Box>
</Box>
<Box
Expand Down
36 changes: 23 additions & 13 deletions packages/www/components/WebhookDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,15 @@ type SearchFilters = {

const filters: FilterType[] = ["all", "succeeded", "failed"];

const WebhookDetails = ({ id, data, logs, handleLogFilters, refetchLogs }) => {
const WebhookDetails = ({
id,
data,
logs,
handleLogFilters,
refetchLogs,
loadMore,
isLogsLoading,
}) => {
const { deleteWebhook, updateWebhook } = useApi();
const [deleting, setDeleting] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -260,12 +275,13 @@ const WebhookDetails = ({ id, data, logs, handleLogFilters, refetchLogs }) => {

<Search handleSearchFilters={handleLogFilters} />

{logs.length > 0 ? (
{logs?.data?.length > 0 ? (
<LogsContainer
data={data}
logs={logs}
filter={activeFilter}
logs={logs.data}
refetchLogs={refetchLogs}
loadMore={loadMore}
isLogsLoading={isLogsLoading}
/>
) : (
<Flex
Expand Down Expand Up @@ -316,12 +332,6 @@ const WebhookDetails = ({ id, data, logs, handleLogFilters, refetchLogs }) => {
};

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 (
<Flex
css={{
Expand Down Expand Up @@ -358,10 +368,10 @@ const Filters = ({ filters, activeFilter, handleFilterClick, logs }) => {
color: activeFilter === filter && "$blue11",
}}>
{filter === "all"
? totalWebhookLogs
? logs?.totalCount
: filter === "succeeded"
? totalSucceededWebhookLogs
: totalFailedWebhookLogs}
? logs?.successCount
: logs?.failedCount}
</Text>
</Box>
))}
Expand Down
57 changes: 45 additions & 12 deletions packages/www/hooks/use-api/endpoints/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,52 @@ export const deleteWebhooks = async (ids: Array<string>): Promise<void> => {

export const getWebhookLogs = async (
webhookId,
filters = null
): Promise<WebhookLogs[]> => {
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: {
Expand Down
72 changes: 66 additions & 6 deletions packages/www/pages/dashboard/developers/webhooks/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>({
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;
Expand All @@ -23,24 +30,75 @@ 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();
};

if (!user) {
return <Layout />;
}

const loadMore = () => {
if (logs.cursor) {
setLoadingMore(true);
refetchLogs();
}
};

return (
<Layout
id="developers/webhooks"
Expand All @@ -56,6 +114,8 @@ const WebhookDetail = () => {
data={webhookData}
logs={logs}
refetchLogs={refetchLogs}
loadMore={loadMore}
isLogsLoading={isLogsLoading}
/>
</Layout>
);
Expand Down