mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix(frontend): improve agent runs page loading state (#10914)
## Changes 🏗️ https://github.com/user-attachments/assets/356e5364-45be-4f6e-bd1c-cc8e42bf294d And also tidy up the some of the logic around hooks. I also added a `okData` helper to avoid having to type case ( `as` ) so much with the generated types ( given the `response` is a union depending on `status: 200 | 400 | 401` ... ) ## Checklist 📋 ### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Run PR locally with the `new-agent-runs` flag enabled - [x] Check the nice loading state ### For configuration changes: None
This commit is contained in:
@@ -5,89 +5,73 @@ import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { useAgentRunsView } from "./useAgentRunsView";
|
||||
import { AgentRunsLoading } from "./components/AgentRunsLoading";
|
||||
import { RunsSidebar } from "./components/RunsSidebar/RunsSidebar";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { RunDetails } from "./components/RunDetails/RunDetails";
|
||||
import { ScheduleDetails } from "./components/ScheduleDetails/ScheduleDetails";
|
||||
import { EmptyAgentRuns } from "./components/EmptyAgentRuns/EmptyAgentRuns";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { RunAgentModal } from "./components/RunAgentModal/RunAgentModal";
|
||||
import { PlusIcon } from "@phosphor-icons/react";
|
||||
|
||||
export function AgentRunsView() {
|
||||
const {
|
||||
response,
|
||||
agent,
|
||||
hasAnyItems,
|
||||
showSidebarLayout,
|
||||
ready,
|
||||
error,
|
||||
agentId,
|
||||
selectedRun,
|
||||
handleSelectRun,
|
||||
clearSelectedRun,
|
||||
handleCountsChange,
|
||||
handleClearSelectedRun,
|
||||
} = useAgentRunsView();
|
||||
const [sidebarCounts, setSidebarCounts] = useState({
|
||||
runsCount: 0,
|
||||
schedulesCount: 0,
|
||||
});
|
||||
|
||||
const hasAnyItems = useMemo(
|
||||
() =>
|
||||
(sidebarCounts.runsCount ?? 0) > 0 ||
|
||||
(sidebarCounts.schedulesCount ?? 0) > 0,
|
||||
[sidebarCounts],
|
||||
);
|
||||
|
||||
if (!ready) {
|
||||
return <AgentRunsLoading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorCard
|
||||
isSuccess={false}
|
||||
responseError={error || undefined}
|
||||
httpError={
|
||||
response?.status !== 200
|
||||
? {
|
||||
status: response?.status,
|
||||
statusText: "Request failed",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
context="agent"
|
||||
onRetry={() => window.location.reload()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!response?.data || response.status !== 200) {
|
||||
return (
|
||||
<ErrorCard
|
||||
isSuccess={false}
|
||||
responseError={{ message: "No agent data found" }}
|
||||
context="agent"
|
||||
onRetry={() => window.location.reload()}
|
||||
/>
|
||||
);
|
||||
if (!ready || !agent) {
|
||||
return <AgentRunsLoading />;
|
||||
}
|
||||
|
||||
const agent = response.data;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
hasAnyItems
|
||||
showSidebarLayout
|
||||
? "grid h-screen grid-cols-1 gap-0 pt-6 md:gap-4 lg:grid-cols-[25%_70%]"
|
||||
: "grid h-screen grid-cols-1 gap-0 pt-6 md:gap-4"
|
||||
}
|
||||
>
|
||||
<div className={hasAnyItems ? "" : "hidden"}>
|
||||
<div className={showSidebarLayout ? "p-4 pl-5" : "hidden p-4 pl-5"}>
|
||||
<div className="mb-6">
|
||||
<RunAgentModal
|
||||
triggerSlot={
|
||||
<Button variant="primary" size="large" className="w-full">
|
||||
<PlusIcon size={20} /> New Run
|
||||
</Button>
|
||||
}
|
||||
agent={agent}
|
||||
agentId={agent.id.toString()}
|
||||
/>
|
||||
</div>
|
||||
<RunsSidebar
|
||||
agent={agent}
|
||||
selectedRunId={selectedRun}
|
||||
onSelectRun={handleSelectRun}
|
||||
onCountsChange={setSidebarCounts}
|
||||
onCountsChange={handleCountsChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Content - 70% */}
|
||||
<div className="p-4">
|
||||
<div className={!hasAnyItems ? "px-2" : ""}>
|
||||
<div className={!showSidebarLayout ? "px-2" : ""}>
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ name: "My Library", link: "/library" },
|
||||
@@ -101,14 +85,14 @@ export function AgentRunsView() {
|
||||
<ScheduleDetails
|
||||
agent={agent}
|
||||
scheduleId={selectedRun.replace("schedule:", "")}
|
||||
onClearSelectedRun={clearSelectedRun}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
/>
|
||||
) : (
|
||||
<RunDetails
|
||||
agent={agent}
|
||||
runId={selectedRun}
|
||||
onSelectRun={handleSelectRun}
|
||||
onClearSelectedRun={clearSelectedRun}
|
||||
onClearSelectedRun={handleClearSelectedRun}
|
||||
/>
|
||||
)
|
||||
) : hasAnyItems ? (
|
||||
|
||||
@@ -4,9 +4,9 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
export function AgentRunsLoading() {
|
||||
return (
|
||||
<div className="px-6 py-6">
|
||||
<div className="flex h-screen w-full gap-4">
|
||||
<div className="flex h-screen w-full gap-8">
|
||||
{/* Left Sidebar */}
|
||||
<div className="w-80 space-y-4">
|
||||
<div className="w-[20vw] space-y-4">
|
||||
<Skeleton className="h-12 w-full" />
|
||||
<Skeleton className="h-32 w-full" />
|
||||
<Skeleton className="h-24 w-full" />
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import React from "react";
|
||||
import {
|
||||
TabsLine,
|
||||
TabsLineList,
|
||||
TabsLineTrigger,
|
||||
TabsLineContent,
|
||||
} from "@/components/molecules/TabsLine/TabsLine";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { PlusIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
import { RunAgentModal } from "../RunAgentModal/RunAgentModal";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { useRunsSidebar } from "./useRunsSidebar";
|
||||
import { RunListItem } from "./components/RunListItem";
|
||||
@@ -26,6 +23,7 @@ interface RunsSidebarProps {
|
||||
onCountsChange?: (info: {
|
||||
runsCount: number;
|
||||
schedulesCount: number;
|
||||
loading?: boolean;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
@@ -47,11 +45,11 @@ export function RunsSidebar({
|
||||
isFetchingMoreRuns,
|
||||
tabValue,
|
||||
setTabValue,
|
||||
} = useRunsSidebar({ graphId: agent.graph_id, onSelectRun });
|
||||
|
||||
useEffect(() => {
|
||||
if (onCountsChange) onCountsChange({ runsCount, schedulesCount });
|
||||
}, [runsCount, schedulesCount, onCountsChange]);
|
||||
} = useRunsSidebar({
|
||||
graphId: agent.graph_id,
|
||||
onSelectRun,
|
||||
onCountsChange,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <ErrorCard responseError={error} />;
|
||||
@@ -59,7 +57,7 @@ export function RunsSidebar({
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="ml-6 w-80 space-y-4">
|
||||
<div className="ml-6 w-[20vw] space-y-4">
|
||||
<Skeleton className="h-12 w-full" />
|
||||
<Skeleton className="h-32 w-full" />
|
||||
<Skeleton className="h-24 w-full" />
|
||||
@@ -68,77 +66,64 @@ export function RunsSidebar({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-w-0 bg-gray-50 p-4 pl-5">
|
||||
<RunAgentModal
|
||||
triggerSlot={
|
||||
<Button variant="primary" size="large" className="w-full">
|
||||
<PlusIcon size={20} /> New Run
|
||||
</Button>
|
||||
<TabsLine
|
||||
value={tabValue}
|
||||
onValueChange={(v) => {
|
||||
const value = v as "runs" | "scheduled";
|
||||
setTabValue(value);
|
||||
if (value === "runs") {
|
||||
if (runs && runs.length) onSelectRun(runs[0].id);
|
||||
} else {
|
||||
if (schedules && schedules.length)
|
||||
onSelectRun(`schedule:${schedules[0].id}`);
|
||||
}
|
||||
agent={agent}
|
||||
agentId={agent.id.toString()}
|
||||
/>
|
||||
}}
|
||||
className="min-w-0 overflow-hidden"
|
||||
>
|
||||
<TabsLineList>
|
||||
<TabsLineTrigger value="runs">
|
||||
Runs <span className="ml-3 inline-block">{runsCount}</span>
|
||||
</TabsLineTrigger>
|
||||
<TabsLineTrigger value="scheduled">
|
||||
Scheduled <span className="ml-3 inline-block">{schedulesCount}</span>
|
||||
</TabsLineTrigger>
|
||||
</TabsLineList>
|
||||
|
||||
<TabsLine
|
||||
value={tabValue}
|
||||
onValueChange={(v) => {
|
||||
const value = v as "runs" | "scheduled";
|
||||
setTabValue(value);
|
||||
if (value === "runs") {
|
||||
if (runs && runs.length) onSelectRun(runs[0].id);
|
||||
} else {
|
||||
if (schedules && schedules.length)
|
||||
onSelectRun(`schedule:${schedules[0].id}`);
|
||||
}
|
||||
}}
|
||||
className="mt-6 min-w-0 overflow-hidden"
|
||||
>
|
||||
<TabsLineList>
|
||||
<TabsLineTrigger value="runs">
|
||||
Runs <span className="ml-3 inline-block">{runsCount}</span>
|
||||
</TabsLineTrigger>
|
||||
<TabsLineTrigger value="scheduled">
|
||||
Scheduled{" "}
|
||||
<span className="ml-3 inline-block">{schedulesCount}</span>
|
||||
</TabsLineTrigger>
|
||||
</TabsLineList>
|
||||
|
||||
<>
|
||||
<TabsLineContent value="runs">
|
||||
<InfiniteList
|
||||
items={runs}
|
||||
hasMore={!!hasMoreRuns}
|
||||
isFetchingMore={isFetchingMoreRuns}
|
||||
onEndReached={fetchMoreRuns}
|
||||
className="flex flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 lg:flex-col lg:gap-1 lg:overflow-x-hidden"
|
||||
itemWrapperClassName="w-auto lg:w-full"
|
||||
renderItem={(run) => (
|
||||
<div className="mb-3 w-[15rem] lg:w-full">
|
||||
<RunListItem
|
||||
run={run}
|
||||
title={agent.name}
|
||||
selected={selectedRunId === run.id}
|
||||
onClick={() => onSelectRun && onSelectRun(run.id)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="scheduled">
|
||||
<div className="flex flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 lg:flex-col lg:gap-1 lg:overflow-x-hidden">
|
||||
{schedules.map((s: GraphExecutionJobInfo) => (
|
||||
<div className="mb-3 w-[15rem] lg:w-full" key={s.id}>
|
||||
<ScheduleListItem
|
||||
schedule={s}
|
||||
selected={selectedRunId === `schedule:${s.id}`}
|
||||
onClick={() => onSelectRun(`schedule:${s.id}`)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
</>
|
||||
</TabsLine>
|
||||
</div>
|
||||
<>
|
||||
<TabsLineContent value="runs">
|
||||
<InfiniteList
|
||||
items={runs}
|
||||
hasMore={!!hasMoreRuns}
|
||||
isFetchingMore={isFetchingMoreRuns}
|
||||
onEndReached={fetchMoreRuns}
|
||||
className="flex flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 lg:flex-col lg:gap-1 lg:overflow-x-hidden"
|
||||
itemWrapperClassName="w-auto lg:w-full"
|
||||
renderItem={(run) => (
|
||||
<div className="mb-3 w-[15rem] lg:w-full">
|
||||
<RunListItem
|
||||
run={run}
|
||||
title={agent.name}
|
||||
selected={selectedRunId === run.id}
|
||||
onClick={() => onSelectRun && onSelectRun(run.id)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="scheduled">
|
||||
<div className="flex flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 lg:flex-col lg:gap-1 lg:overflow-x-hidden">
|
||||
{schedules.map((s: GraphExecutionJobInfo) => (
|
||||
<div className="mb-3 w-[15rem] lg:w-full" key={s.id}>
|
||||
<ScheduleListItem
|
||||
schedule={s}
|
||||
selected={selectedRunId === `schedule:${s.id}`}
|
||||
onClick={() => onSelectRun(`schedule:${s.id}`)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
</>
|
||||
</TabsLine>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { GraphExecutionsPaginated } from "@/app/api/__generated__/models/graphExecutionsPaginated";
|
||||
import type { InfiniteData } from "@tanstack/react-query";
|
||||
|
||||
const AGENT_RUNNING_POLL_INTERVAL = 1500;
|
||||
|
||||
function hasValidExecutionsData(
|
||||
page: unknown,
|
||||
): page is { data: GraphExecutionsPaginated } {
|
||||
return (
|
||||
typeof page === "object" &&
|
||||
page !== null &&
|
||||
"data" in page &&
|
||||
typeof (page as { data: unknown }).data === "object" &&
|
||||
(page as { data: unknown }).data !== null &&
|
||||
"executions" in (page as { data: GraphExecutionsPaginated }).data
|
||||
);
|
||||
}
|
||||
|
||||
export function getRunsPollingInterval(
|
||||
pages: Array<unknown> | undefined,
|
||||
isRunsTab: boolean,
|
||||
): number | false {
|
||||
if (!isRunsTab || !pages?.length) return false;
|
||||
|
||||
try {
|
||||
const executions = pages.flatMap((page) => {
|
||||
if (!hasValidExecutionsData(page)) return [];
|
||||
return page.data.executions || [];
|
||||
});
|
||||
const hasActive = executions.some(
|
||||
(e) => e.status === "RUNNING" || e.status === "QUEUED",
|
||||
);
|
||||
return hasActive ? AGENT_RUNNING_POLL_INTERVAL : false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function computeRunsCount(
|
||||
infiniteData: InfiniteData<unknown> | undefined,
|
||||
runsLength: number,
|
||||
): number {
|
||||
const lastPage = infiniteData?.pages.at(-1);
|
||||
if (!hasValidExecutionsData(lastPage)) return runsLength;
|
||||
return lastPage.data.pagination?.total_items || runsLength;
|
||||
}
|
||||
|
||||
export function getNextRunsPageParam(lastPage: unknown): number | undefined {
|
||||
if (!hasValidExecutionsData(lastPage)) return undefined;
|
||||
|
||||
const { pagination } = lastPage.data;
|
||||
const hasMore =
|
||||
pagination.current_page * pagination.page_size < pagination.total_items;
|
||||
return hasMore ? pagination.current_page + 1 : undefined;
|
||||
}
|
||||
|
||||
export function extractRunsFromPages(
|
||||
infiniteData: InfiniteData<unknown> | undefined,
|
||||
) {
|
||||
return (
|
||||
infiniteData?.pages.flatMap((page) => {
|
||||
if (!hasValidExecutionsData(page)) return [];
|
||||
return page.data.executions || [];
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
@@ -4,18 +4,27 @@ import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useGetV1ListGraphExecutionsInfinite } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { useGetV1ListExecutionSchedulesForAGraph } from "@/app/api/__generated__/endpoints/schedules/schedules";
|
||||
import { GraphExecutionsPaginated } from "@/app/api/__generated__/models/graphExecutionsPaginated";
|
||||
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
const AGENT_RUNNING_POLL_INTERVAL = 1500;
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import {
|
||||
getRunsPollingInterval,
|
||||
computeRunsCount,
|
||||
getNextRunsPageParam,
|
||||
extractRunsFromPages,
|
||||
} from "./helpers";
|
||||
|
||||
type Args = {
|
||||
graphId?: string;
|
||||
onSelectRun: (runId: string) => void;
|
||||
onCountsChange?: (info: {
|
||||
runsCount: number;
|
||||
schedulesCount: number;
|
||||
loading?: boolean;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export function useRunsSidebar({ graphId, onSelectRun }: Args) {
|
||||
export function useRunsSidebar({ graphId, onSelectRun, onCountsChange }: Args) {
|
||||
const params = useSearchParams();
|
||||
const existingRunId = params.get("executionId") as string | undefined;
|
||||
const [tabValue, setTabValue] = useState<"runs" | "scheduled">("runs");
|
||||
@@ -26,38 +35,11 @@ export function useRunsSidebar({ graphId, onSelectRun }: Args) {
|
||||
{
|
||||
query: {
|
||||
enabled: !!graphId,
|
||||
// Lightweight polling so statuses refresh; only poll if any run is active
|
||||
refetchInterval: (q) => {
|
||||
if (tabValue !== "runs") return false;
|
||||
const pages = q.state.data?.pages as
|
||||
| Array<{ data: unknown }>
|
||||
| undefined;
|
||||
if (!pages || pages.length === 0) return false;
|
||||
try {
|
||||
const executions = pages.flatMap((p) => {
|
||||
const response = p.data as GraphExecutionsPaginated;
|
||||
return response.executions || [];
|
||||
});
|
||||
const hasActive = executions.some(
|
||||
(e: { status?: string }) =>
|
||||
e.status === "RUNNING" || e.status === "QUEUED",
|
||||
);
|
||||
return hasActive ? AGENT_RUNNING_POLL_INTERVAL : false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
refetchInterval: (q) =>
|
||||
getRunsPollingInterval(q.state.data?.pages, tabValue === "runs"),
|
||||
refetchIntervalInBackground: true,
|
||||
refetchOnWindowFocus: false,
|
||||
getNextPageParam: (lastPage) => {
|
||||
const pagination = (lastPage.data as GraphExecutionsPaginated)
|
||||
.pagination;
|
||||
const hasMore =
|
||||
pagination.current_page * pagination.page_size <
|
||||
pagination.total_items;
|
||||
|
||||
return hasMore ? pagination.current_page + 1 : undefined;
|
||||
},
|
||||
getNextPageParam: getNextRunsPageParam,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -65,19 +47,31 @@ export function useRunsSidebar({ graphId, onSelectRun }: Args) {
|
||||
const schedulesQuery = useGetV1ListExecutionSchedulesForAGraph(
|
||||
graphId || "",
|
||||
{
|
||||
query: { enabled: !!graphId },
|
||||
query: {
|
||||
enabled: !!graphId,
|
||||
select: (r) => okData<GraphExecutionJobInfo[]>(r) ?? [],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const runs = useMemo(
|
||||
() =>
|
||||
runsQuery.data?.pages.flatMap((p) => {
|
||||
const response = p.data as GraphExecutionsPaginated;
|
||||
return response.executions;
|
||||
}) || [],
|
||||
() => extractRunsFromPages(runsQuery.data),
|
||||
[runsQuery.data],
|
||||
);
|
||||
|
||||
const schedules = schedulesQuery.data || [];
|
||||
|
||||
const runsCount = computeRunsCount(runsQuery.data, runs.length);
|
||||
const schedulesCount = schedules.length;
|
||||
const loading = !schedulesQuery.isSuccess || !runsQuery.isSuccess;
|
||||
|
||||
// Notify parent about counts and loading state
|
||||
useEffect(() => {
|
||||
if (onCountsChange) {
|
||||
onCountsChange({ runsCount, schedulesCount, loading });
|
||||
}
|
||||
}, [runsCount, schedulesCount, loading, onCountsChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (runs.length > 0) {
|
||||
if (existingRunId) {
|
||||
@@ -94,9 +88,6 @@ export function useRunsSidebar({ graphId, onSelectRun }: Args) {
|
||||
else setTabValue("runs");
|
||||
}, [existingRunId]);
|
||||
|
||||
const schedules: GraphExecutionJobInfo[] =
|
||||
schedulesQuery.data?.status === 200 ? schedulesQuery.data.data : [];
|
||||
|
||||
// If there are no runs but there are schedules, and nothing is selected, auto-select the first schedule
|
||||
useEffect(() => {
|
||||
if (!existingRunId && runs.length === 0 && schedules.length > 0)
|
||||
@@ -107,17 +98,12 @@ export function useRunsSidebar({ graphId, onSelectRun }: Args) {
|
||||
runs,
|
||||
schedules,
|
||||
error: schedulesQuery.error || runsQuery.error,
|
||||
loading: !schedulesQuery.isSuccess || !runsQuery.isSuccess,
|
||||
loading,
|
||||
runsQuery,
|
||||
tabValue,
|
||||
setTabValue,
|
||||
runsCount:
|
||||
(
|
||||
runsQuery.data?.pages.at(-1)?.data as
|
||||
| GraphExecutionsPaginated
|
||||
| undefined
|
||||
)?.pagination.total_items || runs.length,
|
||||
schedulesCount: schedules.length,
|
||||
runsCount,
|
||||
schedulesCount,
|
||||
fetchMoreRuns: runsQuery.fetchNextPage,
|
||||
hasMoreRuns: runsQuery.hasNextPage,
|
||||
isFetchingMoreRuns: runsQuery.isFetchingNextPage,
|
||||
|
||||
@@ -1,30 +1,77 @@
|
||||
import { useGetV2GetLibraryAgent } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import { useParams } from "next/navigation";
|
||||
import { parseAsString, useQueryState } from "nuqs";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
export function useAgentRunsView() {
|
||||
const { id } = useParams();
|
||||
const agentId = id as string;
|
||||
const { data: response, isSuccess, error } = useGetV2GetLibraryAgent(agentId);
|
||||
const {
|
||||
data: response,
|
||||
isSuccess,
|
||||
error,
|
||||
} = useGetV2GetLibraryAgent(agentId, {
|
||||
query: {
|
||||
select: okData<LibraryAgent>,
|
||||
},
|
||||
});
|
||||
|
||||
const [runParam, setRunParam] = useQueryState("executionId", parseAsString);
|
||||
const selectedRun = runParam ?? undefined;
|
||||
|
||||
const [sidebarCounts, setSidebarCounts] = useState({
|
||||
runsCount: 0,
|
||||
schedulesCount: 0,
|
||||
});
|
||||
const [sidebarLoading, setSidebarLoading] = useState(true);
|
||||
|
||||
const hasAnyItems = useMemo(
|
||||
() =>
|
||||
(sidebarCounts.runsCount ?? 0) > 0 ||
|
||||
(sidebarCounts.schedulesCount ?? 0) > 0,
|
||||
[sidebarCounts],
|
||||
);
|
||||
|
||||
// Show sidebar layout while loading or when there are items
|
||||
const showSidebarLayout = sidebarLoading || hasAnyItems;
|
||||
|
||||
function handleSelectRun(id: string) {
|
||||
setRunParam(id, { shallow: true });
|
||||
}
|
||||
|
||||
function clearSelectedRun() {
|
||||
function handleClearSelectedRun() {
|
||||
setRunParam(null, { shallow: true });
|
||||
}
|
||||
|
||||
const handleCountsChange = useCallback(
|
||||
(counts: {
|
||||
runsCount: number;
|
||||
schedulesCount: number;
|
||||
loading?: boolean;
|
||||
}) => {
|
||||
setSidebarCounts({
|
||||
runsCount: counts.runsCount,
|
||||
schedulesCount: counts.schedulesCount,
|
||||
});
|
||||
if (counts.loading !== undefined) {
|
||||
setSidebarLoading(counts.loading);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
agentId: id,
|
||||
ready: isSuccess,
|
||||
error,
|
||||
response,
|
||||
agent: response,
|
||||
hasAnyItems,
|
||||
showSidebarLayout,
|
||||
selectedRun,
|
||||
handleClearSelectedRun,
|
||||
handleCountsChange,
|
||||
handleSelectRun,
|
||||
clearSelectedRun,
|
||||
};
|
||||
}
|
||||
|
||||
25
autogpt_platform/frontend/src/app/api/helpers.ts
Normal file
25
autogpt_platform/frontend/src/app/api/helpers.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Narrow an orval response to its success payload if and only if it is a `200` status with OK shape.
|
||||
*
|
||||
* Usage with React Query select:
|
||||
* ```ts
|
||||
* const { data: agent } = useGetV2GetLibraryAgent(agentId, {
|
||||
* query: { select: okData<LibraryAgent> },
|
||||
* });
|
||||
*
|
||||
* data // is now properly typed as LibraryAgent | undefined
|
||||
* ```
|
||||
*/
|
||||
export function okData<T>(res: unknown): T | undefined {
|
||||
if (!res || typeof res !== "object") return undefined;
|
||||
|
||||
// status must exist and be exactly 200
|
||||
const maybeStatus = (res as { status?: unknown }).status;
|
||||
if (maybeStatus !== 200) return undefined;
|
||||
|
||||
// data must exist and be an object/array/string/number/etc. We only need to
|
||||
// check presence to safely return it as T; the generic T is enforced at call sites.
|
||||
if (!("data" in (res as Record<string, unknown>))) return undefined;
|
||||
|
||||
return (res as { data: T }).data;
|
||||
}
|
||||
Reference in New Issue
Block a user