feat: add VirtualList support for feed events

This commit is contained in:
moonrailgun 2024-07-03 00:12:50 +08:00
parent 1b859e3176
commit caf7e9ca72
3 changed files with 90 additions and 63 deletions

View File

@ -5,44 +5,51 @@ import dayjs from 'dayjs';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
import { MarkdownViewer } from '../MarkdownEditor';
import { FeedIcon } from './FeedIcon';
import { cn } from '@/utils/style';
type FeedEventItemType = AppRouterOutput['feed']['events'][number];
export const FeedEventItem: React.FC<{ event: FeedEventItemType }> = React.memo(
({ event }) => {
return (
<div className="border-muted flex items-center rounded-lg border px-4 py-2">
<div className="flex-1 gap-2">
<div className="mb-2 flex items-center gap-2 text-sm">
<div className="border-muted rounded-lg border p-2">
<FeedIcon source={event.source} size={24} />
</div>
<div>
<MarkdownViewer value={event.eventContent} />
</div>
export const FeedEventItem: React.FC<{
className?: string;
event: FeedEventItemType;
}> = React.memo(({ className, event }) => {
return (
<div
className={cn(
'border-muted flex items-center rounded-lg border px-4 py-2',
className
)}
>
<div className="flex-1 gap-2">
<div className="mb-2 flex items-center gap-2 text-sm">
<div className="border-muted rounded-lg border p-2">
<FeedIcon source={event.source} size={24} />
</div>
<div className="flex flex-wrap gap-2">
<Badge>{event.source}</Badge>
<Badge variant="secondary">{event.eventName}</Badge>
{event.tags.map((tag) => (
<Badge variant="outline">{tag}</Badge>
))}
<div>
<MarkdownViewer value={event.eventContent} />
</div>
</div>
<Tooltip>
<TooltipTrigger className="cursor-default self-end text-xs opacity-60">
<div>{dayjs(event.createdAt).fromNow()}</div>
</TooltipTrigger>
<TooltipContent>
<p>{dayjs(event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p>
</TooltipContent>
</Tooltip>
<div className="flex flex-wrap gap-2">
<Badge>{event.source}</Badge>
<Badge variant="secondary">{event.eventName}</Badge>
{event.tags.map((tag) => (
<Badge variant="outline">{tag}</Badge>
))}
</div>
</div>
);
}
);
<Tooltip>
<TooltipTrigger className="cursor-default self-end text-xs opacity-60">
<div>{dayjs(event.createdAt).fromNow()}</div>
</TooltipTrigger>
<TooltipContent>
<p>{dayjs(event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p>
</TooltipContent>
</Tooltip>
</div>
);
});
FeedEventItem.displayName = 'FeedEventItem';

View File

@ -1,7 +1,6 @@
import { defaultErrorHandler, defaultSuccessHandler, trpc } from '@/api/trpc';
import { CommonHeader } from '@/components/CommonHeader';
import { CommonWrapper } from '@/components/CommonWrapper';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useCurrentWorkspaceId } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react';
@ -16,6 +15,7 @@ import { FeedIntegration } from '@/components/feed/FeedIntegration';
import { DialogWrapper } from '@/components/DialogWrapper';
import { useSocketSubscribeList } from '@/api/socketio';
import { useMemo } from 'react';
import { SimpleVirtualList } from '@/components/SimpleVirtualList';
export const Route = createFileRoute('/feed/$channelId/')({
beforeLoad: routeAuthBeforeLoad,
@ -30,10 +30,18 @@ function PageComponent() {
workspaceId,
channelId,
});
const { data: events = [] } = trpc.feed.events.useQuery({
workspaceId,
channelId,
});
const { data, hasNextPage, fetchNextPage, isFetchingNextPage } =
trpc.feed.fetchEventsByCursor.useInfiniteQuery(
{
workspaceId,
channelId,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
const deleteMutation = trpc.feed.deleteChannel.useMutation({
onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler,
@ -55,8 +63,8 @@ function PageComponent() {
});
const fullEvents = useMemo(
() => [...realtimeEvents, ...events],
[realtimeEvents, events]
() => [...realtimeEvents, ...(data?.pages.flatMap((p) => p.items) ?? [])],
[realtimeEvents, data]
);
return (
@ -102,19 +110,21 @@ function PageComponent() {
/>
}
>
{fullEvents && fullEvents.length === 0 ? (
<div className="w-full overflow-hidden p-4">
<FeedApiGuide channelId={channelId} />
</div>
) : (
<ScrollArea className="h-full overflow-hidden p-4">
<div className="space-y-2">
{(fullEvents ?? []).map((event) => (
<FeedEventItem key={event.id} event={event} />
))}
</div>
</ScrollArea>
)}
<div className="h-full w-full overflow-hidden p-4">
<SimpleVirtualList
allData={fullEvents}
estimateSize={100}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
onFetchNextPage={fetchNextPage}
renderItem={(item) => <FeedEventItem className="mb-2" event={item} />}
renderEmpty={() => (
<div className="w-full overflow-hidden p-4">
<FeedApiGuide channelId={channelId} />
</div>
)}
/>
</div>
</CommonWrapper>
);
}

View File

@ -15,6 +15,7 @@ import {
import { prisma } from '../../../model/_client';
import _ from 'lodash';
import { buildFeedPublicOpenapi, feedIntegrationRouter } from './integration';
import { fetchDataByCursor } from '../../../utils/prisma';
export const feedRouter = router({
channels: workspaceProcedure
@ -113,33 +114,42 @@ export const feedRouter = router({
return channel;
}),
events: workspaceProcedure
fetchEventsByCursor: workspaceProcedure
.meta(
buildFeedOpenapi({
method: 'GET',
path: '/{channelId}/events',
path: '/{channelId}/fetchEventsByCursor',
description: 'Fetch workspace feed channel events',
})
)
.input(
z.object({
channelId: z.string(),
limit: z.number().min(1).max(100).default(50),
cursor: z.string().optional(),
})
)
.output(
z.object({
items: z.array(FeedEventModelSchema),
nextCursor: z.string().optional(),
})
)
.output(z.array(FeedEventModelSchema))
.query(async ({ input }) => {
const { channelId } = input;
const { channelId, cursor, limit } = input;
const events = await prisma.feedEvent.findMany({
const { items, nextCursor } = await fetchDataByCursor(prisma.feedEvent, {
where: {
channelId: channelId,
},
take: 50,
orderBy: {
createdAt: 'desc',
channelId,
},
limit,
cursor,
});
return events;
return {
items,
nextCursor,
};
}),
createChannel: workspaceOwnerProcedure
.meta(