refactor(frontend): Clean up React Query-related code (#11604)

- #11603

### Changes 🏗️

Frontend:
- Make `okData` infer the response data type instead of casting
- Generalize infinite query utilities from `SidebarRunsList/helpers.ts`
  - Move to `@/app/api/helpers` and use wherever possible
- Simplify/replace boilerplate checks and conditions with `okData` in
many places
- Add `useUserTimezone` hook to replace all the boilerplate timezone
queries

Backend:
- Fix response type annotation of `GET
/api/store/graph/{store_listing_version_id}` endpoint
- Fix documentation and error behavior of `GET
/api/review/execution/{graph_exec_id}` endpoint

### 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:
  - CI passes
  - [x] Clicking around the app manually -> no obvious issues
  - [x] Test Onboarding step 5 (run)
  - [x] Library runs list loads normally
This commit is contained in:
Reinier van der Leer
2025-12-20 22:46:24 +01:00
committed by GitHub
parent de78d062a9
commit 08a60dcb9b
57 changed files with 453 additions and 542 deletions

View File

@@ -25,7 +25,7 @@ export default function Page() {
ready,
error,
showInput,
agent,
agentGraph,
onboarding,
storeAgent,
runningAgent,
@@ -76,19 +76,19 @@ export default function Page() {
<CardTitle className="font-poppins text-lg">Input</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{Object.entries(agent?.input_schema.properties || {}).map(
([key, inputSubSchema]) => (
<RunAgentInputs
key={key}
schema={inputSubSchema}
value={onboarding.state?.agentInput?.[key]}
placeholder={inputSubSchema.description}
onChange={(value) => handleSetAgentInput(key, value)}
/>
),
)}
{Object.entries(
agentGraph?.input_schema.properties || {},
).map(([key, inputSubSchema]) => (
<RunAgentInputs
key={key}
schema={inputSubSchema}
value={onboarding.state?.agentInput?.[key]}
placeholder={inputSubSchema.description}
onChange={(value) => handleSetAgentInput(key, value)}
/>
))}
<AgentOnboardingCredentials
agent={agent}
agent={agentGraph}
siblingInputs={
(onboarding.state?.agentInput as Record<string, any>) ||
undefined
@@ -104,7 +104,7 @@ export default function Page() {
className="mt-8 w-[136px]"
loading={runningAgent}
disabled={isRunDisabled({
agent,
agent: agentGraph,
isRunning: runningAgent,
agentInputs:
(onboarding.state?.agentInput as unknown as InputValues) ||

View File

@@ -1,6 +1,3 @@
import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput";
import { GraphMeta } from "@/app/api/__generated__/models/graphMeta";
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
@@ -8,20 +5,19 @@ import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { computeInitialAgentInputs } from "./helpers";
import { InputValues } from "./types";
import { okData, resolveResponse } from "@/app/api/helpers";
import { postV2AddMarketplaceAgent } from "@/app/api/__generated__/endpoints/library/library";
import {
useGetV2GetAgentByVersion,
useGetV2GetAgentGraph,
} from "@/app/api/__generated__/endpoints/store/store";
import { resolveResponse } from "@/app/api/helpers";
import { postV2AddMarketplaceAgent } from "@/app/api/__generated__/endpoints/library/library";
import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput";
import { GraphID } from "@/lib/autogpt-server-api";
export function useOnboardingRunStep() {
const onboarding = useOnboarding(undefined, "AGENT_CHOICE");
const [showInput, setShowInput] = useState(false);
const [agent, setAgent] = useState<GraphMeta | null>(null);
const [storeAgent, setStoreAgent] = useState<StoreAgentDetails | null>(null);
const [runningAgent, setRunningAgent] = useState(false);
const [inputCredentials, setInputCredentials] = useState<
@@ -38,12 +34,26 @@ export function useOnboardingRunStep() {
const currentAgentVersion =
onboarding.state?.selectedStoreListingVersionId ?? "";
const storeAgentQuery = useGetV2GetAgentByVersion(currentAgentVersion, {
query: { enabled: !!currentAgentVersion },
const {
data: storeAgent,
error: storeAgentQueryError,
isSuccess: storeAgentQueryIsSuccess,
} = useGetV2GetAgentByVersion(currentAgentVersion, {
query: {
enabled: !!currentAgentVersion,
select: okData,
},
});
const graphMetaQuery = useGetV2GetAgentGraph(currentAgentVersion, {
query: { enabled: !!currentAgentVersion },
const {
data: agentGraphMeta,
error: agentGraphQueryError,
isSuccess: agentGraphQueryIsSuccess,
} = useGetV2GetAgentGraph(currentAgentVersion, {
query: {
enabled: !!currentAgentVersion,
select: okData,
},
});
useEffect(() => {
@@ -51,29 +61,15 @@ export function useOnboardingRunStep() {
}, []);
useEffect(() => {
if (storeAgentQuery.data && storeAgentQuery.data.status === 200) {
setStoreAgent(storeAgentQuery.data.data);
}
}, [storeAgentQuery.data]);
useEffect(() => {
if (
graphMetaQuery.data &&
graphMetaQuery.data.status === 200 &&
onboarding.state
) {
const graphMeta = graphMetaQuery.data.data as GraphMeta;
setAgent(graphMeta);
const update = computeInitialAgentInputs(
graphMeta,
if (agentGraphMeta && onboarding.state) {
const initialAgentInputs = computeInitialAgentInputs(
agentGraphMeta,
(onboarding.state.agentInput as unknown as InputValues) || null,
);
onboarding.updateState({ agentInput: update });
onboarding.updateState({ agentInput: initialAgentInputs });
}
}, [graphMetaQuery.data]);
}, [agentGraphMeta]);
function handleNewRun() {
if (!onboarding.state) return;
@@ -95,7 +91,7 @@ export function useOnboardingRunStep() {
}
async function handleRunAgent() {
if (!agent || !storeAgent || !onboarding.state) {
if (!agentGraphMeta || !storeAgent || !onboarding.state) {
toast({
title: "Error getting agent",
description:
@@ -142,12 +138,12 @@ export function useOnboardingRunStep() {
}
return {
ready: graphMetaQuery.isSuccess && storeAgentQuery.isSuccess,
error: graphMetaQuery.error || storeAgentQuery.error,
agent,
ready: agentGraphQueryIsSuccess && storeAgentQueryIsSuccess,
error: agentGraphQueryError || storeAgentQueryError,
agentGraph: agentGraphMeta || null,
onboarding,
showInput,
storeAgent,
storeAgent: storeAgent || null,
runningAgent,
credentialsValid,
credentialsLoaded,

View File

@@ -1,6 +1,7 @@
"use client";
import { RunOutputs } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedRunView/components/RunOutputs";
import { okData } from "@/app/api/helpers";
import { useGetV1GetSharedExecution } from "@/app/api/__generated__/endpoints/default/default";
import {
Card,
@@ -17,12 +18,11 @@ export default function SharePage() {
const token = params.token as string;
const {
data: response,
data: executionData,
isLoading: loading,
error,
} = useGetV1GetSharedExecution(token);
} = useGetV1GetSharedExecution(token, { query: { select: okData } });
const executionData = response?.status === 200 ? response.data : undefined;
const is404 = !loading && !executionData;
if (loading) {

View File

@@ -41,6 +41,7 @@ interface FormData extends Omit<ExecutionAnalyticsRequest, "created_after"> {
// All other fields use the generated types as-is
}
import { AnalyticsResultsTable } from "./AnalyticsResultsTable";
import { okData } from "@/app/api/helpers";
export function ExecutionAnalyticsForm() {
const [results, setResults] = useState<ExecutionAnalyticsResponse | null>(
@@ -178,7 +179,7 @@ export function ExecutionAnalyticsForm() {
data: config,
isLoading: configLoading,
error: configError,
} = useGetV2GetExecutionAnalyticsConfiguration();
} = useGetV2GetExecutionAnalyticsConfiguration({ query: { select: okData } });
const generateAnalytics = usePostV2GenerateExecutionAnalytics({
mutation: {
@@ -231,10 +232,10 @@ export function ExecutionAnalyticsForm() {
// Update form defaults when config loads
useEffect(() => {
if (config?.data && config.status === 200 && !formData.model_name) {
if (config && !formData.model_name) {
setFormData((prev) => ({
...prev,
model_name: config.data.recommended_model,
model_name: config.recommended_model,
}));
}
}, [config, formData.model_name]);
@@ -307,7 +308,7 @@ export function ExecutionAnalyticsForm() {
}
// Show error state if config fails to load
if (configError || !config?.data || config.status !== 200) {
if (configError || !config) {
return (
<div className="flex items-center justify-center py-8">
<div className="text-red-500">Failed to load configuration</div>
@@ -315,8 +316,6 @@ export function ExecutionAnalyticsForm() {
);
}
const configData = config.data;
return (
<div className="space-y-6">
<form onSubmit={handleSubmit} className="space-y-4">
@@ -382,7 +381,7 @@ export function ExecutionAnalyticsForm() {
<SelectValue placeholder="Select AI model" />
</SelectTrigger>
<SelectContent>
{configData.available_models.map((model) => (
{config.available_models.map((model) => (
<SelectItem key={model.value} value={model.value}>
{model.label}
</SelectItem>
@@ -442,7 +441,7 @@ export function ExecutionAnalyticsForm() {
onChange={(e) =>
handleInputChange("system_prompt", e.target.value)
}
placeholder={configData.default_system_prompt}
placeholder={config.default_system_prompt}
rows={6}
className="resize-y"
/>
@@ -463,7 +462,7 @@ export function ExecutionAnalyticsForm() {
onChange={(e) =>
handleInputChange("user_prompt", e.target.value)
}
placeholder={configData.default_user_prompt}
placeholder={config.default_user_prompt}
rows={8}
className="resize-y"
/>
@@ -490,7 +489,7 @@ export function ExecutionAnalyticsForm() {
onClick={() => {
handleInputChange(
"system_prompt",
configData.default_system_prompt,
config.default_system_prompt,
);
}}
>
@@ -503,7 +502,7 @@ export function ExecutionAnalyticsForm() {
onClick={() => {
handleInputChange(
"user_prompt",
configData.default_user_prompt,
config.default_user_prompt,
);
}}
>

View File

@@ -17,7 +17,6 @@ import type {
import { CheckIcon, CircleIcon } from "@phosphor-icons/react";
import { useGetOauthGetOauthAppInfo } from "@/app/api/__generated__/endpoints/oauth/oauth";
import { okData } from "@/app/api/helpers";
import { OAuthApplicationPublicInfo } from "@/app/api/__generated__/models/oAuthApplicationPublicInfo";
// All credential types - we accept any type of credential
const ALL_CREDENTIAL_TYPES: CredentialsType[] = [
@@ -107,7 +106,7 @@ export default function IntegrationSetupWizardPage() {
const state = searchParams.get("state");
const { data: appInfo } = useGetOauthGetOauthAppInfo(clientID || "", {
query: { enabled: !!clientID, select: okData<OAuthApplicationPublicInfo> },
query: { enabled: !!clientID, select: okData },
});
// Parse providers from base64-encoded JSON

View File

@@ -1,6 +1,6 @@
import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth";
import { usePostV1CreateExecutionSchedule } from "@/app/api/__generated__/endpoints/schedules/schedules";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useUserTimezone } from "@/lib/hooks/useUserTimezone";
import { getTimezoneDisplayName } from "@/lib/timezone-utils";
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
import { useEffect, useState } from "react";
@@ -28,11 +28,7 @@ export const useCronSchedulerDialog = ({
flowExecutionID: parseAsString,
});
const { data: userTimezone } = useGetV1GetUserTimezone({
query: {
select: (res) => (res.status === 200 ? res.data.timezone : undefined),
},
});
const userTimezone = useUserTimezone();
const timezoneDisplay = getTimezoneDisplayName(userTimezone || "UTC");
const { mutateAsync: createSchedule, isPending: isCreatingSchedule } =

View File

@@ -17,7 +17,6 @@ import { FloatingReviewsPanel } from "@/components/organisms/FloatingReviewsPane
import { parseAsString, useQueryStates } from "nuqs";
import { CustomControls } from "./components/CustomControl";
import { useGetV1GetSpecificGraph } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
import { okData } from "@/app/api/helpers";
import { TriggerAgentBanner } from "./components/TriggerAgentBanner";
import { resolveCollisions } from "./helpers/resolve-collision";
@@ -34,7 +33,7 @@ export const Flow = () => {
{},
{
query: {
select: okData<GraphModel>,
select: okData,
enabled: !!flowID,
},
},

View File

@@ -14,7 +14,7 @@ import { NoSearchResult } from "../NoSearchResult";
export const BlockMenuSearch = () => {
const {
allSearchData,
searchResults,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
@@ -39,7 +39,7 @@ export const BlockMenuSearch = () => {
);
}
if (allSearchData.length === 0) {
if (searchResults.length === 0) {
return <NoSearchResult />;
}
@@ -53,7 +53,7 @@ export const BlockMenuSearch = () => {
loader={<LoadingSpinner className="size-13" />}
className="space-y-2.5"
>
{allSearchData.map((item: SearchResponseItemsItem, index: number) => {
{searchResults.map((item: SearchResponseItemsItem, index: number) => {
const { type, data } = getSearchItemType(item);
// backend give support to these 3 types only [right now] - we need to give support to integration and ai agent types in follow up PRs
switch (type) {

View File

@@ -1,19 +1,25 @@
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
import { useGetV2BuilderSearchInfinite } from "@/app/api/__generated__/endpoints/store/store";
import { SearchResponse } from "@/app/api/__generated__/models/searchResponse";
import { useCallback, useEffect, useState } from "react";
import { useBlockMenuStore } from "@/app/(platform)/build/stores/blockMenuStore";
import { useAddAgentToBuilder } from "../hooks/useAddAgentToBuilder";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { getV2GetSpecificAgent } from "@/app/api/__generated__/endpoints/store/store";
import {
getPaginationNextPageNumber,
okData,
unpaginate,
} from "@/app/api/helpers";
import {
getGetV2GetBuilderItemCountsQueryKey,
getGetV2GetBuilderSuggestionsQueryKey,
} from "@/app/api/__generated__/endpoints/default/default";
import {
getGetV2ListLibraryAgentsQueryKey,
getV2GetLibraryAgent,
usePostV2AddMarketplaceAgent,
} from "@/app/api/__generated__/endpoints/library/library";
import {
getGetV2GetBuilderItemCountsQueryKey,
getGetV2GetBuilderSuggestionsQueryKey,
} from "@/app/api/__generated__/endpoints/default/default";
getV2GetSpecificAgent,
useGetV2BuilderSearchInfinite,
} from "@/app/api/__generated__/endpoints/store/store";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { getQueryClient } from "@/lib/react-query/queryClient";
import { useToast } from "@/components/molecules/Toast/use-toast";
import * as Sentry from "@sentry/nextjs";
@@ -40,7 +46,7 @@ export const useBlockMenuSearch = () => {
>(null);
const {
data: searchData,
data: searchQueryData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
@@ -53,18 +59,7 @@ export const useBlockMenuSearch = () => {
search_id: searchId,
},
{
query: {
getNextPageParam: (lastPage) => {
const response = lastPage.data as SearchResponse;
const { pagination } = response;
if (!pagination) {
return undefined;
}
const { current_page, total_pages } = pagination;
return current_page < total_pages ? current_page + 1 : undefined;
},
},
query: { getNextPageParam: getPaginationNextPageNumber },
},
);
@@ -93,16 +88,15 @@ export const useBlockMenuSearch = () => {
});
useEffect(() => {
if (!searchData?.pages?.length) {
if (!searchQueryData?.pages?.length) {
return;
}
const latestPage = searchData.pages[searchData.pages.length - 1];
const response = latestPage?.data as SearchResponse;
if (response?.search_id && response.search_id !== searchId) {
setSearchId(response.search_id);
const lastPage = okData(searchQueryData.pages.at(-1));
if (lastPage?.search_id && lastPage.search_id !== searchId) {
setSearchId(lastPage.search_id);
}
}, [searchData, searchId, setSearchId]);
}, [searchQueryData, searchId, setSearchId]);
useEffect(() => {
if (searchId && !searchQuery) {
@@ -110,11 +104,9 @@ export const useBlockMenuSearch = () => {
}
}, [resetSearchSession, searchId, searchQuery]);
const allSearchData =
searchData?.pages?.flatMap((page) => {
const response = page.data as SearchResponse;
return response.items;
}) ?? [];
const searchResults = searchQueryData
? unpaginate(searchQueryData, "items")
: [];
const handleAddLibraryAgent = async (agent: LibraryAgent) => {
setAddingLibraryAgentId(agent.id);
@@ -177,7 +169,7 @@ export const useBlockMenuSearch = () => {
};
return {
allSearchData,
searchResults,
isFetchingNextPage,
fetchNextPage,
hasNextPage,

View File

@@ -1,6 +1,10 @@
import {
getPaginatedTotalCount,
getPaginationNextPageNumber,
unpaginate,
} from "@/app/api/helpers";
import { useGetV2GetBuilderBlocksInfinite } from "@/app/api/__generated__/endpoints/default/default";
import { BlockResponse } from "@/app/api/__generated__/models/blockResponse";
import { useBlockMenuStore } from "../../../../stores/blockMenuStore";
import { useBlockMenuStore } from "@/app/(platform)/build/stores/blockMenuStore";
const PAGE_SIZE = 10;
@@ -8,7 +12,7 @@ export const useIntegrationBlocks = () => {
const { integration } = useBlockMenuStore();
const {
data: blocks,
data: blocksQueryData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
@@ -22,30 +26,16 @@ export const useIntegrationBlocks = () => {
provider: integration,
},
{
query: {
getNextPageParam: (lastPage) => {
const pagination = (lastPage.data as BlockResponse).pagination;
const isMore =
pagination.current_page * pagination.page_size <
pagination.total_items;
return isMore ? pagination.current_page + 1 : undefined;
},
},
query: { getNextPageParam: getPaginationNextPageNumber },
},
);
const allBlocks =
blocks?.pages?.flatMap((page) => {
const response = page.data as BlockResponse;
return response.blocks;
}) ?? [];
const allBlocks = blocksQueryData
? unpaginate(blocksQueryData, "blocks")
: [];
const totalBlocks = getPaginatedTotalCount(blocksQueryData);
const totalBlocks = blocks?.pages[0]
? (blocks.pages[0].data as BlockResponse).pagination.total_items
: 0;
const status = blocks?.pages[0]?.status;
const status = blocksQueryData?.pages[0]?.status;
return {
allBlocks,

View File

@@ -1,3 +1,4 @@
import { getPaginationNextPageNumber, unpaginate } from "@/app/api/helpers";
import { getGetV2GetBuilderItemCountsQueryKey } from "@/app/api/__generated__/endpoints/default/default";
import {
getGetV2ListLibraryAgentsQueryKey,
@@ -8,13 +9,12 @@ import {
getV2GetSpecificAgent,
useGetV2ListStoreAgentsInfinite,
} from "@/app/api/__generated__/endpoints/store/store";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { StoreAgentsResponse } from "@/lib/autogpt-server-api";
import { getQueryClient } from "@/lib/react-query/queryClient";
import * as Sentry from "@sentry/nextjs";
import { useState } from "react";
import { useAddAgentToBuilder } from "../hooks/useAddAgentToBuilder";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
export const useMarketplaceAgentsContent = () => {
const { toast } = useToast();
@@ -22,7 +22,7 @@ export const useMarketplaceAgentsContent = () => {
const { addAgentToBuilder } = useAddAgentToBuilder();
const {
data: listStoreAgents,
data: storeAgentsQueryData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
@@ -36,26 +36,14 @@ export const useMarketplaceAgentsContent = () => {
page_size: 10,
},
{
query: {
getNextPageParam: (lastPage) => {
const pagination = (lastPage.data as StoreAgentsResponse).pagination;
const isMore =
pagination.current_page * pagination.page_size <
pagination.total_items;
return isMore ? pagination.current_page + 1 : undefined;
},
},
query: { getNextPageParam: getPaginationNextPageNumber },
},
);
const allAgents =
listStoreAgents?.pages?.flatMap((page) => {
const response = page.data as StoreAgentsResponse;
return response.agents;
}) ?? [];
const status = listStoreAgents?.pages[0]?.status;
const allAgents = storeAgentsQueryData
? unpaginate(storeAgentsQueryData, "agents")
: [];
const status = storeAgentsQueryData?.pages[0]?.status;
const { mutateAsync: addMarketplaceAgent } = usePostV2AddMarketplaceAgent({
mutation: {

View File

@@ -1,5 +1,5 @@
import { getPaginationNextPageNumber, unpaginate } from "@/app/api/helpers";
import { useGetV2ListLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library";
import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse";
import { useState } from "react";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { useAddAgentToBuilder } from "../hooks/useAddAgentToBuilder";
@@ -12,7 +12,7 @@ export const useMyAgentsContent = () => {
const { toast } = useToast();
const {
data: agents,
data: agentsQueryData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
@@ -26,26 +26,14 @@ export const useMyAgentsContent = () => {
page_size: 10,
},
{
query: {
getNextPageParam: (lastPage) => {
const pagination = (lastPage.data as LibraryAgentResponse).pagination;
const isMore =
pagination.current_page * pagination.page_size <
pagination.total_items;
return isMore ? pagination.current_page + 1 : undefined;
},
},
query: { getNextPageParam: getPaginationNextPageNumber },
},
);
const allAgents =
agents?.pages?.flatMap((page) => {
const response = page.data as LibraryAgentResponse;
return response.agents;
}) ?? [];
const status = agents?.pages[0]?.status;
const allAgents = agentsQueryData
? unpaginate(agentsQueryData, "agents")
: [];
const status = agentsQueryData?.pages[0]?.status;
const handleAddBlock = async (agent: LibraryAgent) => {
setSelectedAgentId(agent.id);

View File

@@ -1,5 +1,5 @@
import { getPaginationNextPageNumber, unpaginate } from "@/app/api/helpers";
import { useGetV2GetBuilderBlocksInfinite } from "@/app/api/__generated__/endpoints/default/default";
import { BlockResponse } from "@/app/api/__generated__/models/blockResponse";
interface UsePaginatedBlocksProps {
type?: "all" | "input" | "action" | "output" | null;
@@ -8,7 +8,7 @@ interface UsePaginatedBlocksProps {
const PAGE_SIZE = 10;
export const usePaginatedBlocks = ({ type }: UsePaginatedBlocksProps) => {
const {
data: blocks,
data: blocksQueryData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
@@ -22,26 +22,14 @@ export const usePaginatedBlocks = ({ type }: UsePaginatedBlocksProps) => {
type,
},
{
query: {
getNextPageParam: (lastPage) => {
const pagination = (lastPage.data as BlockResponse).pagination;
const isMore =
pagination.current_page * pagination.page_size <
pagination.total_items;
return isMore ? pagination.current_page + 1 : undefined;
},
},
query: { getNextPageParam: getPaginationNextPageNumber },
},
);
const allBlocks =
blocks?.pages?.flatMap((page) => {
const response = page.data as BlockResponse;
return response.blocks;
}) ?? [];
const status = blocks?.pages[0]?.status;
const allBlocks = blocksQueryData
? unpaginate(blocksQueryData, "blocks")
: [];
const status = blocksQueryData?.pages[0]?.status;
return {
allBlocks,

View File

@@ -1,11 +1,11 @@
import { getPaginationNextPageNumber, unpaginate } from "@/app/api/helpers";
import { useGetV2GetBuilderIntegrationProvidersInfinite } from "@/app/api/__generated__/endpoints/default/default";
import { ProviderResponse } from "@/app/api/__generated__/models/providerResponse";
const PAGE_SIZE = 10;
export const usePaginatedIntegrationList = () => {
const {
data: providers,
data: providersQueryData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
@@ -18,26 +18,14 @@ export const usePaginatedIntegrationList = () => {
page_size: PAGE_SIZE,
},
{
query: {
getNextPageParam: (lastPage: any) => {
const pagination = (lastPage.data as ProviderResponse).pagination;
const isMore =
pagination.current_page * pagination.page_size <
pagination.total_items;
return isMore ? pagination.current_page + 1 : undefined;
},
},
query: { getNextPageParam: getPaginationNextPageNumber },
},
);
const allProviders =
providers?.pages?.flatMap((page: any) => {
const response = page.data as ProviderResponse;
return response.providers;
}) ?? [];
const status = providers?.pages[0]?.status;
const allProviders = providersQueryData
? unpaginate(providersQueryData, "providers")
: [];
const status = providersQueryData?.pages[0]?.status;
return {
allProviders,

View File

@@ -11,6 +11,7 @@ import {
import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
import { storage, Key } from "@/services/storage/local-storage";
import { isValidUUID } from "@/app/(platform)/chat/helpers";
import { okData } from "@/app/api/helpers";
interface UseChatSessionArgs {
urlSessionId?: string | null;
@@ -70,6 +71,7 @@ export function useChatSession({
} = useGetV2GetSession(sessionId || "", {
query: {
enabled: !!sessionId,
select: okData,
staleTime: Infinity, // Never mark as stale
refetchOnMount: false, // Don't refetch on component mount
refetchOnWindowFocus: false, // Don't refetch when window regains focus
@@ -81,9 +83,8 @@ export function useChatSession({
const { mutateAsync: claimSessionMutation } = usePatchV2SessionAssignUser();
const session = useMemo(() => {
if (sessionData?.status === 200) {
return sessionData.data;
}
if (sessionData) return sessionData;
if (sessionId && justCreatedSessionIdRef.current === sessionId) {
return {
id: sessionId,

View File

@@ -1,15 +1,11 @@
import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth";
import { useUserTimezone } from "@/lib/hooks/useUserTimezone";
import { getTimezoneDisplayName } from "@/lib/timezone-utils";
import { InfoIcon } from "@phosphor-icons/react";
export function TimezoneNotice() {
const { data: userTimezone, isSuccess } = useGetV1GetUserTimezone({
query: {
select: (res) => (res.status === 200 ? res.data.timezone : undefined),
},
});
const userTimezone = useUserTimezone();
if (!isSuccess) {
if (!userTimezone) {
return null;
}

View File

@@ -1,7 +1,7 @@
"use client";
import {
getGetV1ListGraphExecutionsInfiniteQueryOptions,
getGetV1ListGraphExecutionsQueryKey,
getV1GetGraphVersion,
useDeleteV1DeleteGraphExecution,
} from "@/app/api/__generated__/endpoints/graphs/graphs";
@@ -127,9 +127,7 @@ export function AgentActionsDropdown({
toast({ title: "Task deleted" });
await queryClient.refetchQueries({
queryKey:
getGetV1ListGraphExecutionsInfiniteQueryOptions(agentGraphId)
.queryKey,
queryKey: getGetV1ListGraphExecutionsQueryKey(agentGraphId),
});
if (onClearSelectedRun) onClearSelectedRun();

View File

@@ -1,7 +1,7 @@
"use client";
import {
getGetV1ListGraphExecutionsInfiniteQueryOptions,
getGetV1ListGraphExecutionsQueryKey,
usePostV1ExecuteGraphAgent,
usePostV1StopGraphExecution,
} from "@/app/api/__generated__/endpoints/graphs/graphs";
@@ -11,6 +11,7 @@ import {
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { GraphExecution } from "@/app/api/__generated__/models/graphExecution";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { okData } from "@/app/api/helpers";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
@@ -58,9 +59,7 @@ export function useSelectedRunActions({
toast({ title: "Run stopped" });
await queryClient.invalidateQueries({
queryKey:
getGetV1ListGraphExecutionsInfiniteQueryOptions(agentGraphId)
.queryKey,
queryKey: getGetV1ListGraphExecutionsQueryKey(agentGraphId),
});
} catch (error: unknown) {
toast({
@@ -97,12 +96,10 @@ export function useSelectedRunActions({
},
});
const newRunId = res?.status === 200 ? (res?.data?.id ?? "") : "";
const newRunId = okData(res)?.id;
await queryClient.invalidateQueries({
queryKey:
getGetV1ListGraphExecutionsInfiniteQueryOptions(agentGraphId)
.queryKey,
queryKey: getGetV1ListGraphExecutionsQueryKey(agentGraphId),
});
if (newRunId && onSelectRun) onSelectRun(newRunId);

View File

@@ -3,14 +3,12 @@
import { useGetV1GetExecutionDetails } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { useGetV2GetASpecificPreset } from "@/app/api/__generated__/endpoints/presets/presets";
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import type { GetV1GetExecutionDetails200 } from "@/app/api/__generated__/models/getV1GetExecutionDetails200";
import type { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { okData } from "@/app/api/helpers";
export function useSelectedRunView(graphId: string, runId: string) {
const query = useGetV1GetExecutionDetails(graphId, runId, {
const executionQuery = useGetV1GetExecutionDetails(graphId, runId, {
query: {
refetchInterval: (q: any) => {
refetchInterval: (q) => {
const isSuccess = q.state.data?.status === 200;
if (!isSuccess) return false;
@@ -33,22 +31,15 @@ export function useSelectedRunView(graphId: string, runId: string) {
},
});
const status = query.data?.status;
const run = okData(executionQuery.data);
const status = executionQuery.data?.status;
const run: GetV1GetExecutionDetails200 | undefined =
status === 200
? (query.data?.data as GetV1GetExecutionDetails200)
: undefined;
const presetId =
run && "preset_id" in run && run.preset_id
? (run.preset_id as string)
: undefined;
const presetId = run?.preset_id || undefined;
const presetQuery = useGetV2GetASpecificPreset(presetId || "", {
query: {
enabled: !!presetId,
select: (res) => okData<LibraryAgentPreset>(res),
select: okData,
},
});
@@ -60,8 +51,8 @@ export function useSelectedRunView(graphId: string, runId: string) {
return {
run,
preset: presetQuery.data,
isLoading: query.isLoading || presetQuery.isLoading,
responseError: query.error || presetQuery.error,
isLoading: executionQuery.isLoading || presetQuery.isLoading,
responseError: executionQuery.error || presetQuery.error,
httpError,
} as const;
}

View File

@@ -1,12 +1,12 @@
"use client";
import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { Text } from "@/components/atoms/Text/Text";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
import { isLargeScreen, useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { useUserTimezone } from "@/lib/hooks/useUserTimezone";
import { formatInTimezone, getTimezoneDisplayName } from "@/lib/timezone-utils";
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
import { LoadingSelectedContent } from "../LoadingSelectedContent";
@@ -36,11 +36,7 @@ export function SelectedScheduleView({
scheduleId,
);
const { data: userTzRes } = useGetV1GetUserTimezone({
query: {
select: (res) => (res.status === 200 ? res.data.timezone : undefined),
},
});
const userTimezone = useUserTimezone();
const breakpoint = useBreakpoint();
const isLgScreenUp = isLargeScreen(breakpoint);
@@ -90,7 +86,7 @@ export function SelectedScheduleView({
run={undefined}
scheduleRecurrence={
schedule
? `${humanizeCronExpression(schedule.cron || "")} · ${getTimezoneDisplayName(schedule.timezone || userTzRes || "UTC")}`
? `${humanizeCronExpression(schedule.cron || "")} · ${getTimezoneDisplayName(schedule.timezone || userTimezone || "UTC")}`
: undefined
}
/>
@@ -125,7 +121,7 @@ export function SelectedScheduleView({
<span className="text-zinc-500"></span>{" "}
<span className="text-zinc-500">
{getTimezoneDisplayName(
schedule.timezone || userTzRes || "UTC",
schedule.timezone || userTimezone || "UTC",
)}
</span>
</Text>
@@ -135,7 +131,7 @@ export function SelectedScheduleView({
<Text variant="body" className="flex items-center gap-3">
{formatInTimezone(
schedule.next_run_time,
userTzRes || "UTC",
userTimezone || "UTC",
{
year: "numeric",
month: "long",
@@ -148,7 +144,7 @@ export function SelectedScheduleView({
<span className="text-zinc-500"></span>{" "}
<span className="text-zinc-500">
{getTimezoneDisplayName(
schedule.timezone || userTzRes || "UTC",
schedule.timezone || userTimezone || "UTC",
)}
</span>
</Text>

View File

@@ -1,7 +1,7 @@
"use client";
import { getGetV1ListGraphExecutionsInfiniteQueryOptions } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { getGetV1ListExecutionSchedulesForAGraphQueryKey } from "@/app/api/__generated__/endpoints/schedules/schedules";
import { getGetV1ListGraphExecutionsQueryKey } from "@/app/api/__generated__/endpoints/graphs/graphs";
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useMutation, useQueryClient } from "@tanstack/react-query";
@@ -94,8 +94,7 @@ export function useEditScheduleModal(
await queryClient.invalidateQueries({
queryKey: getGetV1ListExecutionSchedulesForAGraphQueryKey(graphId),
});
const runsKey = getGetV1ListGraphExecutionsInfiniteQueryOptions(graphId)
.queryKey as any;
const runsKey = getGetV1ListGraphExecutionsQueryKey(graphId);
await queryClient.invalidateQueries({ queryKey: runsKey });
setIsOpen(false);
},

View File

@@ -2,30 +2,29 @@
import { useMemo } from "react";
import { useGetV1ListExecutionSchedulesForAGraph } from "@/app/api/__generated__/endpoints/schedules/schedules";
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { okData } from "@/app/api/helpers";
export function useSelectedScheduleView(graphId: string, scheduleId: string) {
const query = useGetV1ListExecutionSchedulesForAGraph(graphId, {
const schedulesQuery = useGetV1ListExecutionSchedulesForAGraph(graphId, {
query: {
enabled: !!graphId,
select: (res) =>
res.status === 200 ? (res.data as GraphExecutionJobInfo[]) : [],
select: okData,
},
});
const schedule = useMemo(
() => query.data?.find((s) => s.id === scheduleId),
[query.data, scheduleId],
() => schedulesQuery.data?.find((s) => s.id === scheduleId),
[schedulesQuery.data, scheduleId],
);
const httpError =
query.isSuccess && !schedule
schedulesQuery.isSuccess && !schedule
? { status: 404, statusText: "Not found" }
: undefined;
return {
schedule,
isLoading: query.isLoading,
error: query.error || httpError,
isLoading: schedulesQuery.isLoading,
error: schedulesQuery.error || httpError,
} as const;
}

View File

@@ -2,10 +2,10 @@
import {
getGetV2ListPresetsQueryKey,
getV2ListPresets,
useDeleteV2DeleteAPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import type { LibraryAgentPresetResponse } from "@/app/api/__generated__/models/libraryAgentPresetResponse";
import { okData } from "@/app/api/helpers";
import { Button } from "@/components/atoms/Button/Button";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
@@ -56,15 +56,13 @@ export function SelectedTemplateActions({
queryKey,
});
const queryData = queryClient.getQueryData<{
data: LibraryAgentPresetResponse;
}>(queryKey);
const queryData =
queryClient.getQueryData<
Awaited<ReturnType<typeof getV2ListPresets>>
>(queryKey);
const presets =
okData<LibraryAgentPresetResponse>(queryData)?.presets ?? [];
const templates = presets.filter(
(preset) => !preset.webhook_id || !preset.webhook,
);
const presets = okData(queryData)?.presets ?? [];
const templates = presets.filter((preset) => !preset.webhook_id);
setShowDeleteDialog(false);
onDeleted?.();

View File

@@ -1,6 +1,6 @@
"use client";
import { getGetV1ListGraphExecutionsInfiniteQueryOptions } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { getGetV1ListGraphExecutionsQueryKey } from "@/app/api/__generated__/endpoints/graphs/graphs";
import {
getGetV2GetASpecificPresetQueryKey,
getGetV2ListPresetsQueryKey,
@@ -9,7 +9,6 @@ import {
usePostV2ExecuteAPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import type { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import type { LibraryAgentPresetUpdatable } from "@/app/api/__generated__/models/libraryAgentPresetUpdatable";
import { okData } from "@/app/api/helpers";
import { useToast } from "@/components/molecules/Toast/use-toast";
@@ -34,7 +33,7 @@ export function useSelectedTemplateView({
const query = useGetV2GetASpecificPreset(templateId, {
query: {
enabled: !!templateId,
select: (res) => okData<LibraryAgentPreset>(res),
select: okData,
},
});
@@ -83,15 +82,13 @@ export function useSelectedTemplateView({
mutation: {
onSuccess: (response) => {
if (response.status === 200) {
const execution = okData<GraphExecutionMeta>(response);
const execution = okData(response);
if (execution) {
toast({
title: "Task started",
});
queryClient.invalidateQueries({
queryKey:
getGetV1ListGraphExecutionsInfiniteQueryOptions(graphId)
.queryKey,
queryKey: getGetV1ListGraphExecutionsQueryKey(graphId),
});
onRunCreated?.(execution);
}

View File

@@ -2,10 +2,10 @@
import {
getGetV2ListPresetsQueryKey,
getV2ListPresets,
useDeleteV2DeleteAPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import type { LibraryAgentPresetResponse } from "@/app/api/__generated__/models/libraryAgentPresetResponse";
import { okData } from "@/app/api/helpers";
import { Button } from "@/components/atoms/Button/Button";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
@@ -52,15 +52,13 @@ export function SelectedTriggerActions({
queryKey,
});
const queryData = queryClient.getQueryData<{
data: LibraryAgentPresetResponse;
}>(queryKey);
const queryData =
queryClient.getQueryData<
Awaited<ReturnType<typeof getV2ListPresets>>
>(queryKey);
const presets =
okData<LibraryAgentPresetResponse>(queryData)?.presets ?? [];
const triggers = presets.filter(
(preset) => preset.webhook_id && preset.webhook,
);
const presets = okData(queryData)?.presets ?? [];
const triggers = presets.filter((preset) => preset.webhook_id);
setShowDeleteDialog(false);
onDeleted?.();

View File

@@ -6,7 +6,6 @@ import {
useGetV2GetASpecificPreset,
usePatchV2UpdateAnExistingPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import type { LibraryAgentPresetUpdatable } from "@/app/api/__generated__/models/libraryAgentPresetUpdatable";
import { okData } from "@/app/api/helpers";
import { useToast } from "@/components/molecules/Toast/use-toast";
@@ -26,7 +25,7 @@ export function useSelectedTriggerView({ triggerId, graphId }: Args) {
const query = useGetV2GetASpecificPreset(triggerId, {
query: {
enabled: !!triggerId,
select: (res) => okData<LibraryAgentPreset>(res),
select: okData,
},
});

View File

@@ -1,7 +1,7 @@
"use client";
import {
getGetV1ListGraphExecutionsInfiniteQueryOptions,
getGetV1ListGraphExecutionsQueryKey,
useDeleteV1DeleteGraphExecution,
} from "@/app/api/__generated__/endpoints/graphs/graphs";
import {
@@ -51,9 +51,7 @@ export function TaskActionsDropdown({ agent, run, onDeleted }: Props) {
toast({ title: "Task deleted" });
await queryClient.refetchQueries({
queryKey: getGetV1ListGraphExecutionsInfiniteQueryOptions(
agent.graph_id,
).queryKey,
queryKey: getGetV1ListGraphExecutionsQueryKey(agent.graph_id),
});
setShowDeleteDialog(false);

View File

@@ -1,44 +0,0 @@
import type { GraphExecutionsPaginated } from "@/app/api/__generated__/models/graphExecutionsPaginated";
import type { InfiniteData } from "@tanstack/react-query";
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 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 || [];
}) || []
);
}

View File

@@ -2,20 +2,18 @@
import { useEffect, useMemo } from "react";
import {
okData,
getPaginationNextPageNumber,
getPaginatedTotalCount,
unpaginate,
} from "@/app/api/helpers";
import { useGetV1ListGraphExecutionsInfinite } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { useGetV2ListPresets } from "@/app/api/__generated__/endpoints/presets/presets";
import { useGetV1ListExecutionSchedulesForAGraph } from "@/app/api/__generated__/endpoints/schedules/schedules";
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import type { LibraryAgentPresetResponse } from "@/app/api/__generated__/models/libraryAgentPresetResponse";
import { okData } from "@/app/api/helpers";
import { useExecutionEvents } from "@/hooks/useExecutionEvents";
import { useQueryClient } from "@tanstack/react-query";
import { parseAsString, useQueryStates } from "nuqs";
import {
computeRunsCount,
extractRunsFromPages,
getNextRunsPageParam,
} from "./helpers";
function parseTab(
value: string | null,
@@ -66,7 +64,7 @@ export function useSidebarRunsList({
query: {
enabled: !!graphId,
refetchOnWindowFocus: false,
getNextPageParam: getNextRunsPageParam,
getNextPageParam: getPaginationNextPageNumber,
},
},
);
@@ -74,7 +72,7 @@ export function useSidebarRunsList({
const schedulesQuery = useGetV1ListExecutionSchedulesForAGraph(graphId, {
query: {
enabled: !!graphId,
select: (r) => okData<GraphExecutionJobInfo[]>(r),
select: okData,
},
});
@@ -83,13 +81,13 @@ export function useSidebarRunsList({
{
query: {
enabled: !!graphId,
select: (r) => okData<LibraryAgentPresetResponse>(r)?.presets,
select: (r) => okData(r)?.presets,
},
},
);
const runs = useMemo(
() => extractRunsFromPages(runsQuery.data),
() => (runsQuery.data ? unpaginate(runsQuery.data, "executions") : []),
[runsQuery.data],
);
@@ -104,7 +102,7 @@ export function useSidebarRunsList({
[allPresets],
);
const runsCount = computeRunsCount(runsQuery.data, runs.length);
const runsCount = getPaginatedTotalCount(runsQuery.data, runs.length);
const schedulesCount = schedules.length;
const templatesCount = templates.length;
const triggersCount = triggers.length;

View File

@@ -2,7 +2,6 @@ import { useGetV2GetLibraryAgent } from "@/app/api/__generated__/endpoints/libra
import { useGetV2GetASpecificPreset } from "@/app/api/__generated__/endpoints/presets/presets";
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { okData } from "@/app/api/helpers";
import { useParams } from "next/navigation";
@@ -31,11 +30,7 @@ export function useNewAgentLibraryView() {
data: agent,
isSuccess,
error,
} = useGetV2GetLibraryAgent(agentId, {
query: {
select: okData<LibraryAgent>,
},
});
} = useGetV2GetLibraryAgent(agentId, { query: { select: okData } });
const [{ activeItem, activeTab: activeTabRaw }, setQueryStates] =
useQueryStates({
@@ -53,7 +48,7 @@ export function useNewAgentLibraryView() {
} = useGetV2GetASpecificPreset(activeItem ?? "", {
query: {
enabled: Boolean(activeTab === "templates" && activeItem),
select: okData<LibraryAgentPreset>,
select: okData,
},
});
const activeTemplate =

View File

@@ -23,7 +23,7 @@ import LoadingBox from "@/components/__legacy__/ui/loading";
import { useToastOnFail } from "@/components/molecules/Toast/use-toast";
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
import { formatScheduleTime } from "@/lib/timezone-utils";
import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth";
import { useUserTimezone } from "@/lib/hooks/useUserTimezone";
import { PlayIcon } from "lucide-react";
import { AgentRunStatus } from "./agent-run-status-chip";
@@ -48,11 +48,7 @@ export function AgentScheduleDetailsView({
const toastOnFail = useToastOnFail();
// Get user's timezone for displaying schedule times
const { data: userTimezone } = useGetV1GetUserTimezone({
query: {
select: (res) => (res.status === 200 ? res.data.timezone : undefined),
},
});
const userTimezone = useUserTimezone();
const infoStats: { label: string; value: React.ReactNode }[] = useMemo(() => {
return [

View File

@@ -4,8 +4,8 @@ import { Button } from "@/components/__legacy__/ui/button";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { CronScheduler } from "@/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/cron-scheduler";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth";
import { getTimezoneDisplayName } from "@/lib/timezone-utils";
import { useUserTimezone } from "@/lib/hooks/useUserTimezone";
import { InfoIcon } from "lucide-react";
// Base type for cron expression only
@@ -50,11 +50,7 @@ export function CronSchedulerDialog(props: CronSchedulerDialogProps) {
);
// Get user's timezone
const { data: userTimezone } = useGetV1GetUserTimezone({
query: {
select: (res) => (res.status === 200 ? res.data.timezone : undefined),
},
});
const userTimezone = useUserTimezone();
const timezoneDisplay = getTimezoneDisplayName(userTimezone || "UTC");
// Reset state when dialog opens

View File

@@ -1,15 +1,20 @@
import {
GraphExecutionMeta as LegacyGraphExecutionMeta,
GraphID,
GraphExecutionID,
} from "@/lib/autogpt-server-api";
import { getQueryClient } from "@/lib/react-query/queryClient";
import {
getPaginatedTotalCount,
getPaginationNextPageNumber,
unpaginate,
} from "@/app/api/helpers";
import {
getV1ListGraphExecutionsResponse,
getV1ListGraphExecutionsResponse200,
useGetV1ListGraphExecutionsInfinite,
} from "@/app/api/__generated__/endpoints/graphs/graphs";
import { GraphExecutionsPaginated } from "@/app/api/__generated__/models/graphExecutionsPaginated";
import { getQueryClient } from "@/lib/react-query/queryClient";
import {
GraphExecutionMeta as LegacyGraphExecutionMeta,
GraphID,
GraphExecutionID,
} from "@/lib/autogpt-server-api";
import { GraphExecutionMeta as RawGraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
export type GraphExecutionMeta = Omit<
@@ -44,15 +49,7 @@ export const useAgentRunsInfinite = (graphID?: GraphID) => {
{ page: 1, page_size: 20 },
{
query: {
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: getPaginationNextPageNumber,
// Prevent query from running if graphID is not available (yet)
...(!graphID
@@ -80,15 +77,8 @@ export const useAgentRunsInfinite = (graphID?: GraphID) => {
queryClient,
);
const agentRuns =
queryResults?.pages.flatMap((page) => {
const response = page.data as GraphExecutionsPaginated;
return response.executions;
}) ?? [];
const agentRunCount = (
queryResults?.pages.at(-1)?.data as GraphExecutionsPaginated | undefined
)?.pagination.total_items;
const agentRuns = queryResults ? unpaginate(queryResults, "executions") : [];
const agentRunCount = getPaginatedTotalCount(queryResults);
const upsertAgentRun = (newAgentRun: GraphExecutionMeta) => {
queryClient.setQueryData(

View File

@@ -1,7 +1,11 @@
"use client";
import {
getPaginatedTotalCount,
getPaginationNextPageNumber,
unpaginate,
} from "@/app/api/helpers";
import { useGetV2ListLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library";
import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse";
import { useLibraryPageContext } from "../state-provider";
import { useLibraryAgentsStore } from "@/hooks/useLibraryAgents/store";
import { getInitialData } from "./helpers";
@@ -11,7 +15,7 @@ export const useLibraryAgentList = () => {
const { agents: cachedAgents } = useLibraryAgentsStore();
const {
data: agents,
data: agentsQueryData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
@@ -26,27 +30,15 @@ export const useLibraryAgentList = () => {
{
query: {
initialData: getInitialData(cachedAgents, searchTerm, 8),
getNextPageParam: (lastPage) => {
const pagination = (lastPage.data as LibraryAgentResponse).pagination;
const isMore =
pagination.current_page * pagination.page_size <
pagination.total_items;
return isMore ? pagination.current_page + 1 : undefined;
},
getNextPageParam: getPaginationNextPageNumber,
},
},
);
const allAgents =
agents?.pages?.flatMap((page) => {
const response = page.data as LibraryAgentResponse;
return response.agents;
}) ?? [];
const agentCount = agents?.pages?.[0]
? (agents.pages[0].data as LibraryAgentResponse).pagination.total_items
: 0;
const allAgents = agentsQueryData
? unpaginate(agentsQueryData, "agents")
: [];
const agentCount = getPaginatedTotalCount(agentsQueryData);
return {
allAgents,

View File

@@ -1,10 +1,15 @@
"use client";
import {
getPaginatedTotalCount,
getPaginationNextPageNumber,
unpaginate,
} from "@/app/api/helpers";
import { useGetV2ListFavoriteLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library";
export function useFavoriteAgents() {
const {
data: agents,
data: agentsQueryData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
@@ -15,36 +20,14 @@ export function useFavoriteAgents() {
page_size: 10,
},
{
query: {
getNextPageParam: (lastPage) => {
// Only paginate on successful responses
if (!lastPage || lastPage.status !== 200) return undefined;
const pagination = lastPage.data.pagination;
const isMore =
pagination.current_page * pagination.page_size <
pagination.total_items;
return isMore ? pagination.current_page + 1 : undefined;
},
},
query: { getNextPageParam: getPaginationNextPageNumber },
},
);
const allAgents =
agents?.pages?.flatMap((page) => {
// Only process successful responses
if (!page || page.status !== 200) return [];
const response = page.data;
return response?.agents || [];
}) ?? [];
const agentCount = (() => {
const firstPage = agents?.pages?.[0];
// Only count from successful responses
if (!firstPage || firstPage.status !== 200) return 0;
return firstPage.data?.pagination?.total_items || 0;
})();
const allAgents = agentsQueryData
? unpaginate(agentsQueryData, "agents")
: [];
const agentCount = getPaginatedTotalCount(agentsQueryData);
return {
allAgents,

View File

@@ -15,11 +15,11 @@ import { ScrollArea } from "@/components/__legacy__/ui/scroll-area";
import { ClockIcon, Loader2 } from "lucide-react";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
import { useUserTimezone } from "@/lib/hooks/useUserTimezone";
import {
formatScheduleTime,
getTimezoneAbbreviation,
} from "@/lib/timezone-utils";
import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth";
import {
Select,
SelectContent,
@@ -66,11 +66,7 @@ export const SchedulesTable = ({
const [selectedFilter, setSelectedFilter] = useState<string>(""); // Graph ID
// Get user's timezone for displaying schedule times
const { data: userTimezone } = useGetV1GetUserTimezone({
query: {
select: (res) => (res.status === 200 ? res.data.timezone : "UTC"),
},
});
const userTimezone = useUserTimezone() ?? "UTC";
const filteredAndSortedSchedules = [...schedules]
.filter(

View File

@@ -7,6 +7,7 @@ import {
useGetV1ListExecutionSchedulesForAUser,
useDeleteV1DeleteExecutionSchedule,
} from "@/app/api/__generated__/endpoints/schedules/schedules";
import { okData } from "@/app/api/helpers";
import { Card } from "@/components/__legacy__/ui/card";
import { SchedulesTable } from "@/app/(platform)/monitoring/components/SchedulesTable";
@@ -34,8 +35,7 @@ const Monitor = () => {
useGetV1ListExecutionSchedulesForAUser();
const deleteScheduleMutation = useDeleteV1DeleteExecutionSchedule();
const schedules =
schedulesResponse?.status === 200 ? schedulesResponse.data : [];
const schedules = okData(schedulesResponse) ?? [];
const removeSchedule = useCallback(
async (scheduleId: string) => {

View File

@@ -4,6 +4,7 @@ import {
useDeleteV1RevokeApiKey,
useGetV1ListUserApiKeys,
} from "@/app/api/__generated__/endpoints/api-keys/api-keys";
import { okData } from "@/app/api/helpers";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { getQueryClient } from "@/lib/react-query/queryClient";
@@ -13,11 +14,7 @@ export const useAPISection = () => {
const { data: apiKeys, isLoading } = useGetV1ListUserApiKeys({
query: {
select: (res) => {
if (res.status !== 200) return undefined;
return res.data.filter((key) => key.status === "ACTIVE");
},
select: (res) => okData(res)?.filter((key) => key.status === "ACTIVE"),
},
});

View File

@@ -7,7 +7,6 @@ import {
usePostOauthUploadAppLogo,
getGetOauthListMyOauthAppsQueryKey,
} from "@/app/api/__generated__/endpoints/oauth/oauth";
import { OAuthApplicationInfo } from "@/app/api/__generated__/models/oAuthApplicationInfo";
import { okData } from "@/app/api/helpers";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { getQueryClient } from "@/lib/react-query/queryClient";
@@ -19,7 +18,7 @@ export const useOAuthApps = () => {
const [uploadingAppId, setUploadingAppId] = useState<string | null>(null);
const { data: oauthAppsResponse, isLoading } = useGetOauthListMyOauthApps({
query: { select: okData<OAuthApplicationInfo[]> },
query: { select: okData },
});
const { mutateAsync: updateStatus } = usePatchOauthUpdateAppStatus({

View File

@@ -6,6 +6,7 @@ import {
useGetV1GetNotificationPreferences,
useGetV1GetUserTimezone,
} from "@/app/api/__generated__/endpoints/auth/auth";
import { okData } from "@/app/api/helpers";
import { Text } from "@/components/atoms/Text/Text";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
@@ -24,7 +25,7 @@ export default function SettingsPage() {
} = useGetV1GetNotificationPreferences({
query: {
enabled: !!user,
select: (res) => (res.status === 200 ? res.data : null),
select: okData,
},
});
@@ -32,9 +33,7 @@ export default function SettingsPage() {
useGetV1GetUserTimezone({
query: {
enabled: !!user,
select: (res) => {
return res.status === 200 ? String(res.data.timezone) : "not-set";
},
select: (res) => okData(res)?.timezone ?? "not-set",
},
});

View File

@@ -1,7 +1,12 @@
import type { InfiniteData } from "@tanstack/react-query";
import {
getV1IsOnboardingEnabled,
getV1OnboardingState,
} from "./__generated__/endpoints/onboarding/onboarding";
import { Pagination } from "./__generated__/models/pagination";
export type OKData<TResponse extends { status: number; data?: any }> =
(TResponse & { status: 200 })["data"];
/**
* Narrow an orval response to its success payload if and only if it is a `200` status with OK shape.
@@ -9,13 +14,15 @@ import {
* Usage with React Query select:
* ```ts
* const { data: agent } = useGetV2GetLibraryAgent(agentId, {
* query: { select: okData<LibraryAgent> },
* query: { select: okData },
* });
*
* data // is now properly typed as LibraryAgent | undefined
* ```
*/
export function okData<T>(res: unknown): T | undefined {
export function okData<TResponse extends { status: number; data?: any }>(
res: TResponse | undefined,
): OKData<TResponse> | undefined {
if (!res || typeof res !== "object") return undefined;
// status must exist and be exactly 200
@@ -26,7 +33,88 @@ export function okData<T>(res: unknown): T | undefined {
// 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;
return res.data;
}
export function getPaginatedTotalCount(
infiniteData: InfiniteData<unknown> | undefined,
fallbackCount?: number,
): number {
const lastPage = infiniteData?.pages.at(-1);
if (!hasValidPaginationInfo(lastPage)) return fallbackCount ?? 0;
return lastPage.data.pagination.total_items ?? fallbackCount ?? 0;
}
export function getPaginationNextPageNumber(
lastPage:
| { data: { pagination?: Pagination; [key: string]: any } }
| undefined,
): number | undefined {
if (!hasValidPaginationInfo(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;
}
/** Make one list from a paginated infinite query result. */
export function unpaginate<
TResponse extends { status: number; data: any },
TPageDataKey extends {
// Only allow keys for which the value is an array:
[K in keyof OKData<TResponse>]: OKData<TResponse>[K] extends any[]
? K
: never;
}[keyof OKData<TResponse>] &
string,
TItemData extends OKData<TResponse>[TPageDataKey][number],
>(
infiniteData: InfiniteData<TResponse>,
pageListKey: TPageDataKey,
): TItemData[] {
return (
infiniteData?.pages.flatMap<TItemData>((page) => {
if (!hasValidListPage(page, pageListKey)) return [];
return page.data[pageListKey] || [];
}) || []
);
}
function hasValidListPage<TKey extends string>(
page: unknown,
pageListKey: TKey,
): page is { status: 200; data: { [key in TKey]: any[] } } {
return (
typeof page === "object" &&
page !== null &&
"status" in page &&
page.status === 200 &&
"data" in page &&
typeof page.data === "object" &&
page.data !== null &&
pageListKey in page.data &&
Array.isArray((page.data as Record<string, unknown>)[pageListKey])
);
}
function hasValidPaginationInfo(
page: unknown,
): page is { data: { pagination: Pagination; [key: string]: any } } {
return (
typeof page === "object" &&
page !== null &&
"data" in page &&
typeof page.data === "object" &&
page.data !== null &&
"pagination" in page.data &&
typeof page.data.pagination === "object" &&
page.data.pagination !== null &&
"total_items" in page.data.pagination &&
"total_pages" in page.data.pagination &&
"current_page" in page.data.pagination &&
"page_size" in page.data.pagination
);
}
type ResponseWithData = { status: number; data: unknown };

View File

@@ -4624,7 +4624,7 @@
"get": {
"tags": ["v2", "executions", "review", "v2", "executions", "review"],
"summary": "Get Pending Reviews for Execution",
"description": "Get all pending reviews for a specific graph execution.\n\nRetrieves all reviews with status \"WAITING\" for the specified graph execution\nthat belong to the authenticated user. Results are ordered by creation time\n(oldest first) to preserve review order within the execution.\n\nArgs:\n graph_exec_id: ID of the graph execution to get reviews for\n user_id: Authenticated user ID from security dependency\n\nReturns:\n List of pending review objects for the specified execution\n\nRaises:\n HTTPException:\n - 403: If user doesn't own the graph execution\n - 500: If authentication fails or database error occurs\n\nNote:\n Only returns reviews owned by the authenticated user for security.\n Reviews with invalid status are excluded with warning logs.",
"description": "Get all pending reviews for a specific graph execution.\n\nRetrieves all reviews with status \"WAITING\" for the specified graph execution\nthat belong to the authenticated user. Results are ordered by creation time\n(oldest first) to preserve review order within the execution.\n\nArgs:\n graph_exec_id: ID of the graph execution to get reviews for\n user_id: Authenticated user ID from security dependency\n\nReturns:\n List of pending review objects for the specified execution\n\nRaises:\n HTTPException:\n - 404: If the graph execution doesn't exist or isn't owned by this user\n - 500: If authentication fails or database error occurs\n\nNote:\n Only returns reviews owned by the authenticated user for security.\n Reviews with invalid status are excluded with warning logs.",
"operationId": "getV2Get pending reviews for execution",
"security": [{ "HTTPBearerJWT": [] }],
"parameters": [
@@ -4650,11 +4650,10 @@
}
}
},
"400": { "description": "Invalid graph execution ID" },
"401": {
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
},
"403": { "description": "Access denied to graph execution" },
"404": { "description": "Graph execution not found" },
"422": {
"description": "Validation Error",
"content": {
@@ -5349,7 +5348,11 @@
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/GraphMeta" }
}
}
},
"401": {
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"