mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2026-01-06 15:14:21 +01:00
perf: added pagination for session in order to improve large project's performance issue
This commit is contained in:
@@ -25,24 +25,30 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { projectDetailQuery } from "../../../../lib/api/queries";
|
||||
import { useConfig } from "../../../hooks/useConfig";
|
||||
import { useProject } from "../hooks/useProject";
|
||||
import { firstCommandToTitle } from "../services/firstCommandToTitle";
|
||||
import { NewChatModal } from "./newChat/NewChatModal";
|
||||
|
||||
export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
||||
const {
|
||||
data: { project, sessions },
|
||||
} = useProject(projectId);
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||
useProject(projectId);
|
||||
const { config } = useConfig();
|
||||
const queryClient = useQueryClient();
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
|
||||
// Flatten all pages to get project and sessions
|
||||
const project = data.pages.at(0)?.project;
|
||||
const sessions = data.pages.flatMap((page) => page.sessions);
|
||||
|
||||
if (!project) {
|
||||
throw new Error("Unreachable: Project must be defined.");
|
||||
}
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: invalidate when config changed
|
||||
useEffect(() => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: projectDetailQuery(projectId).queryKey,
|
||||
queryKey: ["projects", projectId],
|
||||
});
|
||||
}, [config.hideNoUserMessageSession, config.unifySameTitleSession]);
|
||||
|
||||
@@ -170,10 +176,8 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Last modified:{" "}
|
||||
{session.meta.lastModifiedAt
|
||||
? new Date(
|
||||
session.meta.lastModifiedAt,
|
||||
).toLocaleDateString()
|
||||
{session.lastModifiedAt
|
||||
? new Date(session.lastModifiedAt).toLocaleDateString()
|
||||
: ""}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
@@ -195,6 +199,21 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Load More Button */}
|
||||
{sessions.length > 0 && hasNextPage && (
|
||||
<div className="mt-6 flex justify-center">
|
||||
<Button
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={isFetchingNextPage}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="min-w-[200px]"
|
||||
>
|
||||
{isFetchingNextPage ? "Loading..." : "Load More"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useSuspenseInfiniteQuery } from "@tanstack/react-query";
|
||||
import { projectDetailQuery } from "../../../../lib/api/queries";
|
||||
|
||||
export const useProject = (projectId: string) => {
|
||||
return useSuspenseQuery({
|
||||
queryKey: projectDetailQuery(projectId).queryKey,
|
||||
queryFn: projectDetailQuery(projectId).queryFn,
|
||||
return useSuspenseInfiniteQuery({
|
||||
queryKey: ["projects", projectId],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
return await projectDetailQuery(projectId, pageParam).queryFn();
|
||||
},
|
||||
initialPageParam: undefined as string | undefined,
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
refetchOnReconnect: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,9 +15,12 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
await queryClient.prefetchQuery({
|
||||
queryKey: projectDetailQuery(projectId).queryKey,
|
||||
queryFn: projectDetailQuery(projectId).queryFn,
|
||||
await queryClient.prefetchInfiniteQuery({
|
||||
queryKey: ["projects", projectId],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
return await projectDetailQuery(projectId, pageParam).queryFn();
|
||||
},
|
||||
initialPageParam: undefined as string | undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -35,7 +35,9 @@ export const SessionPageContent: FC<{
|
||||
projectId,
|
||||
sessionId,
|
||||
);
|
||||
const { data: project } = useProject(projectId);
|
||||
const { data: projectData } = useProject(projectId);
|
||||
// biome-ignore lint/style/noNonNullAssertion: useSuspenseInfiniteQuery guarantees at least one page
|
||||
const project = projectData.pages[0]!.project;
|
||||
|
||||
const abortTask = useMutation({
|
||||
mutationFn: async (sessionId: string) => {
|
||||
@@ -111,7 +113,7 @@ export const SessionPageContent: FC<{
|
||||
</div>
|
||||
|
||||
<div className="px-1 sm:px-5 flex flex-wrap items-center gap-1 sm:gap-2">
|
||||
{project?.project.claudeProjectPath && (
|
||||
{project?.claudeProjectPath && (
|
||||
<Link
|
||||
href={`/projects/${projectId}`}
|
||||
target="_blank"
|
||||
@@ -122,8 +124,7 @@ export const SessionPageContent: FC<{
|
||||
className="h-6 sm:h-8 text-xs sm:text-sm flex items-center hover:bg-blue-50/60 hover:border-blue-300/60 hover:shadow-sm transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
<ExternalLinkIcon className="w-3 h-3 sm:w-4 sm:h-4 mr-1" />
|
||||
{project.project.meta.projectPath ??
|
||||
project.project.claudeProjectPath}
|
||||
{project.meta.projectPath ?? project.claudeProjectPath}
|
||||
</Badge>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -24,8 +24,12 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
|
||||
onClose,
|
||||
}) => {
|
||||
const {
|
||||
data: { sessions },
|
||||
data: projectData,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useProject(projectId);
|
||||
const sessions = projectData.pages.flatMap((page) => page.sessions);
|
||||
const [activeTab, setActiveTab] = useState<"sessions" | "mcp" | "settings">(
|
||||
"sessions",
|
||||
);
|
||||
@@ -71,9 +75,15 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
|
||||
case "sessions":
|
||||
return (
|
||||
<SessionsTab
|
||||
sessions={sessions}
|
||||
sessions={sessions.map((session) => ({
|
||||
...session,
|
||||
lastModifiedAt: new Date(session.lastModifiedAt),
|
||||
}))}
|
||||
currentSessionId={currentSessionId}
|
||||
projectId={projectId}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
onLoadMore={() => fetchNextPage()}
|
||||
/>
|
||||
);
|
||||
case "mcp":
|
||||
|
||||
@@ -30,8 +30,12 @@ export const SessionSidebar: FC<{
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const {
|
||||
data: { sessions },
|
||||
data: projectData,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useProject(projectId);
|
||||
const sessions = projectData.pages.flatMap((page) => page.sessions);
|
||||
const [activeTab, setActiveTab] = useState<"sessions" | "mcp" | "settings">(
|
||||
"sessions",
|
||||
);
|
||||
@@ -53,9 +57,15 @@ export const SessionSidebar: FC<{
|
||||
case "sessions":
|
||||
return (
|
||||
<SessionsTab
|
||||
sessions={sessions}
|
||||
sessions={sessions.map((session) => ({
|
||||
...session,
|
||||
lastModifiedAt: new Date(session.lastModifiedAt),
|
||||
}))}
|
||||
currentSessionId={currentSessionId}
|
||||
projectId={projectId}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
onLoadMore={() => fetchNextPage()}
|
||||
/>
|
||||
);
|
||||
case "mcp":
|
||||
|
||||
@@ -16,7 +16,17 @@ export const SessionsTab: FC<{
|
||||
sessions: Session[];
|
||||
currentSessionId: string;
|
||||
projectId: string;
|
||||
}> = ({ sessions, currentSessionId, projectId }) => {
|
||||
hasNextPage?: boolean;
|
||||
isFetchingNextPage?: boolean;
|
||||
onLoadMore?: () => void;
|
||||
}> = ({
|
||||
sessions,
|
||||
currentSessionId,
|
||||
projectId,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
onLoadMore,
|
||||
}) => {
|
||||
const aliveTasks = useAtomValue(aliveTasksAtom);
|
||||
|
||||
// Sort sessions: Running > Paused > Others, then by lastModifiedAt (newest first)
|
||||
@@ -43,12 +53,8 @@ export const SessionsTab: FC<{
|
||||
}
|
||||
|
||||
// Then sort by lastModifiedAt (newest first)
|
||||
const aTime = a.meta.lastModifiedAt
|
||||
? new Date(a.meta.lastModifiedAt).getTime()
|
||||
: 0;
|
||||
const bTime = b.meta.lastModifiedAt
|
||||
? new Date(b.meta.lastModifiedAt).getTime()
|
||||
: 0;
|
||||
const aTime = a.lastModifiedAt ? a.lastModifiedAt.getTime() : 0;
|
||||
const bTime = b.lastModifiedAt ? b.lastModifiedAt.getTime() : 0;
|
||||
return bTime - aTime;
|
||||
});
|
||||
|
||||
@@ -121,9 +127,9 @@ export const SessionsTab: FC<{
|
||||
<MessageSquareIcon className="w-3 h-3" />
|
||||
<span>{session.meta.messageCount}</span>
|
||||
</div>
|
||||
{session.meta.lastModifiedAt && (
|
||||
{session.lastModifiedAt && (
|
||||
<span className="text-xs text-sidebar-foreground/60">
|
||||
{new Date(session.meta.lastModifiedAt).toLocaleDateString(
|
||||
{new Date(session.lastModifiedAt).toLocaleDateString(
|
||||
"en-US",
|
||||
{
|
||||
month: "short",
|
||||
@@ -137,6 +143,21 @@ export const SessionsTab: FC<{
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Load More Button */}
|
||||
{hasNextPage && onLoadMore && (
|
||||
<div className="p-2">
|
||||
<Button
|
||||
onClick={onLoadMore}
|
||||
disabled={isFetchingNextPage}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
{isFetchingNextPage ? "Loading..." : "Load More"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -49,8 +49,8 @@ export const ProjectList: FC = () => {
|
||||
<CardContent className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Last modified:{" "}
|
||||
{project.meta.lastModifiedAt
|
||||
? new Date(project.meta.lastModifiedAt).toLocaleDateString()
|
||||
{project.lastModifiedAt
|
||||
? new Date(project.lastModifiedAt).toLocaleDateString()
|
||||
: ""}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user