Compare commits

...

7 Commits

Author SHA1 Message Date
Reinier van der Leer
e1562ee0f6 Merge branch 'dev' into pwuts/open-2853-add-views-for-presets-run-templates 2025-12-08 19:13:11 +01:00
Reinier van der Leer
d785a89f2e add trigger enable/disable button 2025-12-02 23:46:00 +01:00
Reinier van der Leer
c6ce7b9fe9 merge EditTemplateModal into RunAgentModal 2025-12-02 23:40:10 +01:00
Reinier van der Leer
a72d49cd15 Merge branch 'dev' into pwuts/open-2853-add-views-for-presets-run-templates 2025-12-02 17:05:16 +01:00
Reinier van der Leer
0252afbef7 improve UI; support running presets; fix event handlers 2025-12-02 16:26:26 +01:00
Reinier van der Leer
0778d78e84 EOD: improve trigger support 2025-11-28 19:47:45 +01:00
Reinier van der Leer
a0604b1a06 feat(frontend/library): Add preset/trigger support to Library v3 - EOD: 80% 2025-11-27 21:34:34 +01:00
19 changed files with 995 additions and 148 deletions

View File

@@ -41,6 +41,12 @@ export default defineConfig({
useInfiniteQueryParam: "page",
},
},
"getV2List presets": {
query: {
useInfinite: true,
useInfiniteQueryParam: "page",
},
},
"getV1List graph executions": {
query: {
useInfinite: true,

View File

@@ -18,7 +18,6 @@ import { parseAsString, useQueryStates } from "nuqs";
import { CustomControls } from "./components/CustomControl";
import { FloatingSafeModeToggle } from "@/components/molecules/FloatingSafeModeToggle/FloatingSafeModeToggle";
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

@@ -13,6 +13,7 @@ import { EmptyTemplates } from "./components/other/EmptyTemplates";
import { SectionWrap } from "./components/other/SectionWrap";
import { LoadingSelectedContent } from "./components/selected-views/LoadingSelectedContent";
import { SelectedRunView } from "./components/selected-views/SelectedRunView/SelectedRunView";
import { SelectedTemplateView } from "./components/selected-views/SelectedTemplateView/SelectedTemplateView";
import { SelectedScheduleView } from "./components/selected-views/SelectedScheduleView/SelectedScheduleView";
import { SelectedViewLayout } from "./components/selected-views/SelectedViewLayout";
import { SidebarRunsList } from "./components/sidebar/SidebarRunsList/SidebarRunsList";
@@ -84,7 +85,6 @@ export function NewAgentLibraryView() {
</Button>
}
agent={agent}
agentId={agent.id.toString()}
onRunCreated={(execution) => handleSelectRun(execution.id, "runs")}
onScheduleCreated={(schedule) =>
handleSelectRun(schedule.id, "scheduled")
@@ -109,6 +109,16 @@ export function NewAgentLibraryView() {
scheduleId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
/>
) : activeTab === "templates" ? (
<SelectedTemplateView
agent={agent}
presetID={activeItem}
onCreateRun={(runId) => handleSelectRun(runId, "runs")}
onCreateSchedule={(scheduleId) =>
handleSelectRun(scheduleId, "scheduled")
}
onDelete={handleClearSelectedRun}
/>
) : (
<SelectedRunView
agent={agent}

View File

@@ -13,9 +13,11 @@ export function getCredentialTypeDisplayName(type: string): string {
}
export function getAgentInputFields(agent: LibraryAgent): Record<string, any> {
const schema = agent.input_schema as unknown as {
properties?: Record<string, any>;
} | null;
const schema = agent.trigger_setup_info
? agent.trigger_setup_info.config_schema
: (agent.input_schema as unknown as {
properties?: Record<string, any>;
} | null);
if (!schema || !schema.properties) return {};
const properties = schema.properties as Record<string, any>;
const visibleEntries = Object.entries(properties).filter(

View File

@@ -3,7 +3,9 @@
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 { Button } from "@/components/atoms/Button/Button";
import { Input } from "@/components/atoms/Input/Input";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { AlarmIcon } from "@phosphor-icons/react";
import { useState } from "react";
@@ -20,17 +22,30 @@ import { useAgentRunModal } from "./useAgentRunModal";
interface Props {
triggerSlot: React.ReactNode;
agent: LibraryAgent;
agentId: string;
agentVersion?: number;
initialInputValues?: Record<string, any>;
initialInputCredentials?: Record<string, any>;
initialPresetName?: string;
initialPresetDescription?: string;
onRunCreated?: (execution: GraphExecutionMeta) => void;
onTriggerSetup?: (preset: LibraryAgentPreset) => void;
onScheduleCreated?: (schedule: GraphExecutionJobInfo) => void;
editMode?: {
preset: LibraryAgentPreset;
onSaved?: (updatedPreset: LibraryAgentPreset) => void;
};
}
export function RunAgentModal({
triggerSlot,
agent,
initialInputValues,
initialInputCredentials,
initialPresetName,
initialPresetDescription,
onRunCreated,
onTriggerSetup,
onScheduleCreated,
editMode,
}: Props) {
const {
// UI state
@@ -54,6 +69,11 @@ export function RunAgentModal({
setPresetName,
setPresetDescription,
// Edit mode
hasChanges,
isUpdatingPreset,
handleSave,
// Validation/readiness
allRequiredInputsAreSet,
@@ -69,6 +89,13 @@ export function RunAgentModal({
handleRun,
} = useAgentRunModal(agent, {
onRun: onRunCreated,
onSetupTrigger: onTriggerSetup,
onScheduleCreated,
initialInputValues,
initialInputCredentials,
initialPresetName,
initialPresetDescription,
editMode,
});
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
@@ -114,6 +141,8 @@ export function RunAgentModal({
onScheduleCreated?.(schedule);
}
const templateOrTrigger = agent.trigger_setup_info ? "Trigger" : "Template";
return (
<>
<Dialog
@@ -131,23 +160,64 @@ export function RunAgentModal({
{/* Scrollable content */}
<div className="flex-1 pr-1" style={{ scrollbarGutter: "stable" }}>
{/* Template Info Section (Edit Mode Only) */}
{editMode && (
<div className="mt-10">
<AgentSectionHeader
title={`${templateOrTrigger} Information`}
/>
<div className="mb-10 mt-4 space-y-4">
<div className="flex flex-col space-y-2">
<label className="text-sm font-medium">
{templateOrTrigger} Name
</label>
<Input
id="template_name"
label="Template Name"
size="small"
hideLabel
value={presetName}
placeholder="Enter template name"
onChange={(e) => setPresetName(e.target.value)}
/>
</div>
<div className="flex flex-col space-y-2">
<label className="text-sm font-medium">
{templateOrTrigger} Description
</label>
<Input
id="template_description"
label="Template Description"
size="small"
hideLabel
value={presetDescription}
placeholder="Enter template description"
onChange={(e) => setPresetDescription(e.target.value)}
/>
</div>
</div>
</div>
)}
{/* Setup Section */}
<div className="mt-10">
<div className={editMode ? "mt-8" : "mt-10"}>
{hasAnySetupFields ? (
<RunAgentModalContextProvider
value={{
agent,
defaultRunType,
presetName,
setPresetName,
presetDescription,
setPresetDescription,
inputValues,
setInputValue: handleInputChange,
agentInputFields,
inputCredentials,
setInputCredentialsValue: handleCredentialsChange,
agentCredentialsInputFields,
presetEditMode: Boolean(
editMode || agent.trigger_setup_info,
),
presetName,
setPresetName,
presetDescription,
setPresetDescription,
}}
>
<>
@@ -176,32 +246,62 @@ export function RunAgentModal({
style={{ boxShadow: "0px -8px 10px white" }}
>
<div className="flex items-center justify-end gap-3">
<Button
variant="secondary"
onClick={handleOpenScheduleModal}
disabled={
isExecuting || isSettingUpTrigger || !allRequiredInputsAreSet
}
>
<AlarmIcon size={16} />
Schedule Agent
</Button>
<RunActions
defaultRunType={defaultRunType}
onRun={handleRun}
isExecuting={isExecuting}
isSettingUpTrigger={isSettingUpTrigger}
isRunReady={allRequiredInputsAreSet}
/>
{editMode ? (
<>
<Button
variant="secondary"
onClick={() => setIsOpen(false)}
disabled={isUpdatingPreset}
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSave}
disabled={
!hasChanges || isUpdatingPreset || !presetName.trim()
}
>
{isUpdatingPreset ? "Saving..." : "Save Changes"}
</Button>
</>
) : (
<>
{(defaultRunType == "manual" ||
defaultRunType == "schedule") && (
<Button
variant="secondary"
onClick={handleOpenScheduleModal}
disabled={
isExecuting ||
isSettingUpTrigger ||
!allRequiredInputsAreSet
}
>
<AlarmIcon size={16} />
Schedule Agent
</Button>
)}
<RunActions
defaultRunType={defaultRunType}
onRun={handleRun}
isExecuting={isExecuting}
isSettingUpTrigger={isSettingUpTrigger}
isRunReady={allRequiredInputsAreSet}
/>
</>
)}
</div>
<ScheduleAgentModal
isOpen={isScheduleModalOpen}
onClose={handleCloseScheduleModal}
agent={agent}
inputValues={inputValues}
inputCredentials={inputCredentials}
onScheduleCreated={handleScheduleCreated}
/>
{(defaultRunType == "manual" || defaultRunType == "schedule") && (
<ScheduleAgentModal
isOpen={isScheduleModalOpen}
onClose={handleCloseScheduleModal}
agent={agent}
inputValues={inputValues}
inputCredentials={inputCredentials}
onScheduleCreated={handleScheduleCreated}
/>
)}
</Dialog.Footer>
</Dialog.Content>
</Dialog>

View File

@@ -13,24 +13,25 @@ export function ModalRunSection() {
const {
agent,
defaultRunType,
presetName,
setPresetName,
presetDescription,
setPresetDescription,
inputValues,
setInputValue,
agentInputFields,
inputCredentials,
setInputCredentialsValue,
agentCredentialsInputFields,
presetEditMode,
presetName,
setPresetName,
presetDescription,
setPresetDescription,
} = useRunAgentModalContext();
return (
<div className="mb-10 mt-4">
<div className="mb-10 mt-4 flex flex-col gap-4">
{defaultRunType === "automatic-trigger" && <WebhookTriggerBanner />}
{/* Preset/Trigger fields */}
{defaultRunType === "automatic-trigger" && (
{presetEditMode && (
<div className="flex flex-col gap-4">
<div className="flex flex-col space-y-2">
<label className="flex items-center gap-1 text-sm font-medium">
@@ -119,7 +120,7 @@ export function ModalRunSection() {
{/* Selected Credentials Preview */}
{Object.keys(inputCredentials).length > 0 && (
<div className="mt-6 flex flex-col gap-6">
<div className="mt-2 flex flex-col gap-6">
{Object.entries(agentCredentialsInputFields || {}).map(
([key, _sub]) => {
const credential = inputCredentials[key];

View File

@@ -1,6 +1,13 @@
export function WebhookTriggerBanner() {
import { cn } from "@/lib/utils";
export function WebhookTriggerBanner({ className }: { className?: string }) {
return (
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
<div
className={cn(
"rounded-lg border border-blue-200 bg-blue-50 p-4",
className,
)}
>
<div className="flex items-start">
<div className="flex-shrink-0">
<svg

View File

@@ -4,14 +4,9 @@ import React, { createContext, useContext } from "react";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { RunVariant } from "./useAgentRunModal";
export interface RunAgentModalContextValue {
export type RunAgentModalContextValue = {
agent: LibraryAgent;
defaultRunType: RunVariant;
// Preset / Trigger
presetName: string;
setPresetName: (value: string) => void;
presetDescription: string;
setPresetDescription: (value: string) => void;
// Inputs
inputValues: Record<string, any>;
setInputValue: (key: string, value: any) => void;
@@ -20,7 +15,14 @@ export interface RunAgentModalContextValue {
inputCredentials: Record<string, any>;
setInputCredentialsValue: (key: string, value: any | undefined) => void;
agentCredentialsInputFields: Record<string, any>;
}
// Trigger / Preset fields
presetEditMode: boolean; // determines whether to show the name and description fields
presetName: string;
setPresetName: (value: string) => void;
presetDescription: string;
setPresetDescription: (value: string) => void;
};
const RunAgentModalContext = createContext<RunAgentModalContextValue | null>(
null,

View File

@@ -11,7 +11,11 @@ import {
usePostV1CreateExecutionSchedule as useCreateSchedule,
getGetV1ListExecutionSchedulesForAGraphQueryKey,
} from "@/app/api/__generated__/endpoints/schedules/schedules";
import { usePostV2SetupTrigger } from "@/app/api/__generated__/endpoints/presets/presets";
import {
getGetV2ListPresetsQueryKey,
usePostV2SetupTrigger,
usePatchV2UpdateAnExistingPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
@@ -26,8 +30,16 @@ export type RunVariant =
interface UseAgentRunModalCallbacks {
onRun?: (execution: GraphExecutionMeta) => void;
onCreateSchedule?: (schedule: GraphExecutionJobInfo) => void;
onSetupTrigger?: (preset: LibraryAgentPreset) => void;
onScheduleCreated?: (schedule: GraphExecutionJobInfo) => void;
initialInputValues?: Record<string, any>;
initialInputCredentials?: Record<string, any>;
initialPresetName?: string;
initialPresetDescription?: string;
editMode?: {
preset: LibraryAgentPreset;
onSaved?: (updatedPreset: LibraryAgentPreset) => void;
};
}
export function useAgentRunModal(
@@ -38,12 +50,22 @@ export function useAgentRunModal(
const queryClient = useQueryClient();
const [isOpen, setIsOpen] = useState(false);
const [showScheduleView, setShowScheduleView] = useState(false);
const [inputValues, setInputValues] = useState<Record<string, any>>({});
const [inputCredentials, setInputCredentials] = useState<Record<string, any>>(
{},
const [inputValues, setInputValues] = useState<Record<string, any>>(
callbacks?.initialInputValues || callbacks?.editMode?.preset?.inputs || {},
);
const [inputCredentials, setInputCredentials] = useState<Record<string, any>>(
callbacks?.initialInputCredentials ||
callbacks?.editMode?.preset?.credentials ||
{},
);
const [presetName, setPresetName] = useState<string>(
callbacks?.initialPresetName || callbacks?.editMode?.preset?.name || "",
);
const [presetDescription, setPresetDescription] = useState<string>(
callbacks?.initialPresetDescription ||
callbacks?.editMode?.preset?.description ||
"",
);
const [presetName, setPresetName] = useState<string>("");
const [presetDescription, setPresetDescription] = useState<string>("");
const defaultScheduleName = useMemo(() => `Run ${agent.name}`, [agent.name]);
const [scheduleName, setScheduleName] = useState(defaultScheduleName);
const [cronExpression, setCronExpression] = useState(
@@ -70,7 +92,7 @@ export function useAgentRunModal(
toast({
title: "Agent execution started",
});
callbacks?.onRun?.(response.data as unknown as GraphExecutionMeta);
callbacks?.onRun?.(response.data);
// Invalidate runs list for this graph
queryClient.invalidateQueries({
queryKey: getGetV1ListGraphExecutionsInfiniteQueryOptions(
@@ -105,7 +127,7 @@ export function useAgentRunModal(
toast({
title: "Schedule created",
});
callbacks?.onCreateSchedule?.(response.data);
callbacks?.onScheduleCreated?.(response.data);
// Invalidate schedules list for this graph
queryClient.invalidateQueries({
queryKey: getGetV1ListExecutionSchedulesForAGraphQueryKey(
@@ -132,18 +154,51 @@ export function useAgentRunModal(
const setupTriggerMutation = usePostV2SetupTrigger({
mutation: {
onSuccess: (response: any) => {
onSuccess: (response) => {
if (response.status === 200) {
toast({
title: "Trigger setup complete",
});
callbacks?.onSetupTrigger?.(response.data);
// Invalidate preset queries to show the newly created trigger
queryClient.invalidateQueries({
queryKey: getGetV2ListPresetsQueryKey({
graph_id: response.data.graph_id,
}),
});
analytics.sendDatafastEvent("setup_trigger", {
name: agent.name,
id: agent.graph_id,
});
setIsOpen(false);
}
},
onError: (error) => {
toast({
title: "❌ Failed to setup trigger",
description: String(error) || "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
// Edit mode mutation for updating presets
const updatePresetMutation = usePatchV2UpdateAnExistingPreset({
mutation: {
onSuccess: (response) => {
if (response.status === 200) {
toast({
title: "Template updated successfully",
variant: "default",
});
setIsOpen(false);
callbacks?.editMode?.onSaved?.(response.data);
}
},
onError: (error: any) => {
toast({
title: "Failed to setup trigger",
title: "Failed to update template",
description: error.message || "An unexpected error occurred.",
variant: "destructive",
});
@@ -151,11 +206,13 @@ export function useAgentRunModal(
},
});
// Input schema validation
const agentInputSchema = useMemo(
() => agent.input_schema || { properties: {}, required: [] },
[agent.input_schema],
);
// Input schema validation (use trigger schema for triggered agents)
const agentInputSchema = useMemo(() => {
if (agent.trigger_setup_info?.config_schema) {
return agent.trigger_setup_info.config_schema;
}
return agent.input_schema || { properties: {}, required: [] };
}, [agent.input_schema, agent.trigger_setup_info]);
const agentInputFields = useMemo(() => {
if (
@@ -259,9 +316,10 @@ export function useAgentRunModal(
return;
}
// FIXME: add support for "manual-trigger"
if (defaultRunType === "automatic-trigger") {
// Setup trigger
if (!scheduleName.trim()) {
if (!presetName.trim()) {
toast({
title: "⚠️ Trigger name required",
description: "Please provide a name for your trigger.",
@@ -272,7 +330,7 @@ export function useAgentRunModal(
setupTriggerMutation.mutate({
data: {
name: presetName || scheduleName,
name: presetName,
description: presetDescription || `Trigger for ${agent.name}`,
graph_id: agent.graph_id,
graph_version: agent.graph_version,
@@ -295,11 +353,10 @@ export function useAgentRunModal(
}, [
allRequiredInputsAreSet,
defaultRunType,
scheduleName,
presetName,
inputValues,
inputCredentials,
agent,
presetName,
presetDescription,
notifyMissingRequirements,
setupTriggerMutation,
@@ -345,6 +402,7 @@ export function useAgentRunModal(
createScheduleMutation,
toast,
userTimezone,
presetName,
]);
function handleShowSchedule() {
@@ -371,6 +429,47 @@ export function useAgentRunModal(
setCronExpression(expression);
}
// Edit mode save handler
const handleSave = useCallback(() => {
if (!callbacks?.editMode?.preset) return;
updatePresetMutation.mutate({
presetId: callbacks.editMode.preset.id,
data: {
name: presetName,
description: presetDescription,
inputs: inputValues,
credentials: inputCredentials,
},
});
}, [
callbacks?.editMode?.preset,
presetName,
presetDescription,
inputValues,
inputCredentials,
updatePresetMutation,
]);
// Check if there are changes in edit mode
const hasChanges = useMemo(() => {
if (!callbacks?.editMode?.preset) return false;
const preset = callbacks.editMode.preset;
return (
presetName !== preset.name ||
presetDescription !== preset.description ||
JSON.stringify(inputValues) !== JSON.stringify(preset.inputs || {}) ||
JSON.stringify(inputCredentials) !==
JSON.stringify(preset.credentials || {})
);
}, [
callbacks?.editMode?.preset,
presetName,
presetDescription,
inputValues,
inputCredentials,
]);
const hasInputFields = useMemo(() => {
return Object.keys(agentInputFields).length > 0;
}, [agentInputFields]);
@@ -382,7 +481,7 @@ export function useAgentRunModal(
showScheduleView,
// Run mode
defaultRunType,
defaultRunType: defaultRunType as RunVariant,
// Form: regular inputs
inputValues,
@@ -415,6 +514,7 @@ export function useAgentRunModal(
isExecuting: executeGraphMutation.isPending,
isCreatingSchedule: createScheduleMutation.isPending,
isSettingUpTrigger: setupTriggerMutation.isPending,
isUpdatingPreset: updatePresetMutation.isPending,
// Actions
handleRun,
@@ -423,5 +523,7 @@ export function useAgentRunModal(
handleGoBack,
handleSetScheduleName,
handleSetCronExpression,
handleSave,
hasChanges,
};
}

View File

@@ -45,7 +45,6 @@ export function EmptyTasks({ agent }: Props) {
</Button>
}
agent={agent}
agentId={agent.id.toString()}
/>
</div>
</div>

View File

@@ -0,0 +1,379 @@
"use client";
import React, { useCallback, useMemo, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { Text } from "@/components/atoms/Text/Text";
import { Button } from "@/components/atoms/Button/Button";
import {
PencilIcon,
PlayIcon,
StopIcon,
TrashIcon,
} from "@phosphor-icons/react";
import {
TabsLine,
TabsLineContent,
TabsLineList,
TabsLineTrigger,
} from "@/components/molecules/TabsLine/TabsLine";
import { useToast } from "@/components/molecules/Toast/use-toast";
import {
useDeleteV2DeleteAPreset,
getGetV2ListPresetsQueryKey,
useGetV2GetASpecificPreset,
getGetV2GetASpecificPresetQueryKey,
usePatchV2UpdateAnExistingPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import { getGetV1ListGraphExecutionsQueryKey } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { getGetV1ListExecutionSchedulesForAGraphQueryKey } from "@/app/api/__generated__/endpoints/schedules/schedules";
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
import { RunAgentModal } from "../../modals/RunAgentModal/RunAgentModal";
import { okData } from "@/app/api/helpers";
interface SelectedTemplateViewProps {
agent: LibraryAgent;
presetID: string;
onDelete?: (presetID: string) => void;
onCreateRun?: (runId: string) => void;
onCreateSchedule?: (scheduleId: string) => void;
}
export function SelectedTemplateView({
agent,
presetID,
onDelete,
onCreateRun: _onCreateRun,
onCreateSchedule: _onCreateSchedule,
}: SelectedTemplateViewProps) {
const { toast } = useToast();
const queryClient = useQueryClient();
const [isDeleting, setIsDeleting] = useState(false);
const templateOrTrigger = agent.trigger_setup_info ? "Trigger" : "Template";
const presetQuery = useGetV2GetASpecificPreset(presetID, {
query: {
enabled: !!agent.graph_id && !!presetID,
// select: okData,
},
});
const preset = useMemo(() => okData(presetQuery.data), [presetQuery.data]);
// Delete preset mutation
const deleteTemplateMutation = useDeleteV2DeleteAPreset({
mutation: {
onSuccess: () => {
toast({
title: `${templateOrTrigger} deleted successfully`,
variant: "default",
});
// Invalidate presets list
queryClient.invalidateQueries({
queryKey: getGetV2ListPresetsQueryKey({ graph_id: agent.graph_id }),
});
setIsDeleting(false);
},
onError: (error) => {
toast({
title: `Failed to delete ${templateOrTrigger.toLowerCase()}`,
description: String(error),
variant: "destructive",
});
setIsDeleting(false);
},
},
});
const doDeleteTemplate = async () => {
setIsDeleting(true);
deleteTemplateMutation.mutate({ presetId: presetID });
};
// Toggle trigger active status mutation
const toggleTriggerStatusMutation = usePatchV2UpdateAnExistingPreset({
mutation: {
onSuccess: (response) => {
if (response.status === 200) {
toast({
title: `Trigger ${preset?.is_active ? "disabled" : "enabled"} successfully`,
variant: "default",
});
// Invalidate preset queries to refresh data
queryClient.invalidateQueries({
queryKey: getGetV2ListPresetsQueryKey({ graph_id: agent.graph_id }),
});
queryClient.invalidateQueries({
queryKey: getGetV2GetASpecificPresetQueryKey(presetID),
});
}
},
onError: (error) => {
toast({
title: `Failed to ${preset?.is_active ? "disable" : "enable"} trigger`,
description: String(error),
variant: "destructive",
});
},
},
});
const doToggleTriggerStatus = () => {
if (!preset) return;
toggleTriggerStatusMutation.mutate({
presetId: presetID,
data: {
is_active: !preset.is_active,
},
});
};
const onSave = useCallback(() => {
// Invalidate preset queries to refresh data
queryClient.invalidateQueries({
queryKey: getGetV2ListPresetsQueryKey({ graph_id: agent.graph_id }),
});
queryClient.invalidateQueries({
queryKey: getGetV2GetASpecificPresetQueryKey(presetID),
});
}, [queryClient, agent.graph_id, presetID]);
const onCreateRun = useCallback(
(execution: GraphExecutionMeta) => {
// Invalidate runs list
queryClient.invalidateQueries({
queryKey: getGetV1ListGraphExecutionsQueryKey(agent.graph_id),
});
_onCreateRun?.(execution.id);
},
[queryClient, agent.graph_id, _onCreateRun],
);
const onCreateSchedule = useCallback(
(schedule: GraphExecutionJobInfo) => {
// Invalidate schedules list
queryClient.invalidateQueries({
queryKey: getGetV1ListExecutionSchedulesForAGraphQueryKey(
agent.graph_id,
),
});
_onCreateSchedule?.(schedule.id);
},
[queryClient, agent.graph_id, _onCreateSchedule],
);
const isLoading = presetQuery.isLoading;
const error = presetQuery.error;
if (error) {
return (
<ErrorCard
responseError={
error
? {
message: String(
(error as unknown as { message?: string })?.message ||
`Failed to load ${templateOrTrigger.toLowerCase()}`,
),
}
: undefined
}
httpError={
(error as any)?.status
? {
status: (error as any).status,
statusText: (error as any).statusText,
}
: undefined
}
context="template"
/>
);
}
if (isLoading && !preset) {
return (
<div className="flex-1 space-y-4">
<Skeleton className="h-8 w-full" />
<Skeleton className="h-12 w-full" />
<Skeleton className="h-64 w-full" />
<Skeleton className="h-32 w-full" />
</div>
);
}
return (
<div className="flex flex-col gap-6">
<div>
<div className="flex w-full items-center justify-between">
<div className="flex w-full flex-col gap-0">
<div className="flex flex-col gap-2">
<Text variant="h2" className="!text-2xl font-bold">
{preset?.name || "Loading..."}
</Text>
{/* <Text variant="body-medium" className="!text-zinc-500">
{templateOrTrigger} • {agent.name}
</Text> */}
</div>
</div>
{preset ? (
<div className="flex gap-2">
{!agent.has_external_trigger ? (
<RunAgentModal
triggerSlot={
<Button
variant="primary"
size="small"
leftIcon={<PlayIcon size={16} />}
>
Run {templateOrTrigger}
</Button>
}
agent={agent}
initialInputValues={preset.inputs || {}}
initialInputCredentials={preset.credentials || {}}
initialPresetName={preset.name}
initialPresetDescription={preset.description}
onRunCreated={onCreateRun}
onScheduleCreated={onCreateSchedule}
/>
) : null}
<RunAgentModal
triggerSlot={
<Button
variant="secondary"
size="small"
leftIcon={<PencilIcon size={16} />}
>
Edit
</Button>
}
agent={agent}
editMode={{
preset,
onSaved: onSave,
}}
/>
{/* Enable/Disable Trigger Button - only for triggered presets */}
{preset.webhook && (
<Button
variant={preset.is_active ? "destructive" : "primary"}
size="small"
onClick={doToggleTriggerStatus}
disabled={toggleTriggerStatusMutation.isPending}
leftIcon={
preset.is_active ? (
<StopIcon size={16} />
) : (
<PlayIcon size={16} />
)
}
>
{toggleTriggerStatusMutation.isPending
? preset.is_active
? "Disabling..."
: "Enabling..."
: preset.is_active
? "Disable Trigger"
: "Enable Trigger"}
</Button>
)}
<Button
// TODO: add confirmation modal before deleting
variant="destructive"
size="small"
onClick={() => {
doDeleteTemplate();
onDelete?.(presetID);
}}
disabled={isDeleting}
leftIcon={<TrashIcon size={16} />}
>
{isDeleting ? "Deleting..." : "Delete"}
</Button>
</div>
) : null}
</div>
</div>
<TabsLine defaultValue="input">
<TabsLineList>
<TabsLineTrigger value="input">Your input</TabsLineTrigger>
<TabsLineTrigger value="details">
{templateOrTrigger} details
</TabsLineTrigger>
</TabsLineList>
<TabsLineContent value="input">
<RunDetailCard>
<div className="relative">
<AgentInputsReadOnly
agent={agent}
inputs={preset?.inputs}
credentialInputs={preset?.credentials}
/>
</div>
</RunDetailCard>
</TabsLineContent>
<TabsLineContent value="details">
<RunDetailCard>
{isLoading || !preset ? (
<div className="text-neutral-500">Loading</div>
) : (
<div className="relative flex flex-col gap-8">
<div className="flex flex-col gap-1.5">
<Text variant="body-medium" className="!text-black">
Name
</Text>
<p className="text-sm text-zinc-600">{preset.name}</p>
</div>
<div className="flex flex-col gap-1.5">
<Text variant="body-medium" className="!text-black">
Description
</Text>
<p className="text-sm text-zinc-600">
{preset.description || "No description provided"}
</p>
</div>
<div className="flex flex-col gap-1.5">
<Text variant="body-medium" className="!text-black">
Created
</Text>
<p className="text-sm text-zinc-600">
{new Date(preset.created_at).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
</p>
</div>
<div className="flex flex-col gap-1.5">
<Text variant="body-medium" className="!text-black">
Last Updated
</Text>
<p className="text-sm text-zinc-600">
{new Date(preset.updated_at).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
</p>
</div>
</div>
)}
</RunDetailCard>
</TabsLineContent>
</TabsLine>
</div>
);
}

View File

@@ -0,0 +1,75 @@
"use client";
import React from "react";
import { Button } from "@/components/atoms/Button/Button";
import {
PlayIcon,
PencilIcon,
TrashIcon,
CalendarIcon,
} from "@phosphor-icons/react";
interface Props {
onEdit?: () => void;
onDelete?: () => void;
onRun?: () => void;
onCreateSchedule?: () => void;
isRunning?: boolean;
isDeleting?: boolean;
}
export function TemplateActions({
onEdit,
onDelete,
onRun,
onCreateSchedule,
isRunning = false,
isDeleting = false,
}: Props) {
return (
<div className="flex gap-2">
{onRun && (
<Button
variant="primary"
size="small"
onClick={onRun}
disabled={isRunning || isDeleting}
leftIcon={<PlayIcon size={16} />}
>
{isRunning ? "Running..." : "Run Template"}
</Button>
)}
{onEdit && (
<Button
variant="secondary"
size="small"
onClick={onEdit}
leftIcon={<PencilIcon size={16} />}
>
Edit
</Button>
)}
{onCreateSchedule && (
<Button
variant="secondary"
size="small"
onClick={onCreateSchedule}
leftIcon={<CalendarIcon size={16} />}
>
Schedule
</Button>
)}
{onDelete && (
<Button
variant="destructive"
size="small"
onClick={onDelete}
disabled={isRunning || isDeleting}
leftIcon={<TrashIcon size={16} />}
>
{isDeleting ? "Deleting..." : "Delete"}
</Button>
)}
</div>
);
}

View File

@@ -16,6 +16,7 @@ import { cn } from "@/lib/utils";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
import { RunListItem } from "./components/RunListItem";
import { ScheduleListItem } from "./components/ScheduleListItem";
import { TemplateListItem } from "./components/TemplateListItem";
import { useSidebarRunsList } from "./useSidebarRunsList";
interface Props {
@@ -27,6 +28,7 @@ interface Props {
onCountsChange?: (info: {
runsCount: number;
schedulesCount: number;
presetsCount: number;
loading?: boolean;
}) => void;
}
@@ -42,13 +44,18 @@ export function SidebarRunsList({
const {
runs,
schedules,
presets,
runsCount,
schedulesCount,
presetsCount,
error,
loading,
fetchMoreRuns,
hasMoreRuns,
fetchMoreRuns,
isFetchingMoreRuns,
hasMorePresets,
fetchMorePresets,
isFetchingMorePresets,
tabValue,
} = useSidebarRunsList({
graphId: agent.graph_id,
@@ -94,7 +101,11 @@ export function SidebarRunsList({
onClearSelectedRun?.();
}
} else if (value === "templates") {
onClearSelectedRun?.();
if (presets && presets.length) {
onSelectRun(`preset:${presets[0].id}`);
} else {
onClearSelectedRun?.();
}
}
}}
className="flex min-h-0 flex-col overflow-hidden"
@@ -107,7 +118,8 @@ export function SidebarRunsList({
Scheduled <span className="ml-3 inline-block">{schedulesCount}</span>
</TabsLineTrigger>
<TabsLineTrigger value="templates">
Templates <span className="ml-3 inline-block">0</span>
{agent.trigger_setup_info ? "Triggers" : "Templates"}{" "}
<span className="ml-3 inline-block">{presetsCount}</span>
</TabsLineTrigger>
</TabsLineList>
@@ -168,17 +180,29 @@ export function SidebarRunsList({
<TabsLineContent
value="templates"
className={cn(
"mt-0 flex min-h-0 flex-1 flex-col",
"flex min-h-0 flex-1 flex-col",
AGENT_LIBRARY_SECTION_PADDING_X,
)}
>
<div className="flex h-full flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300 lg:flex-col lg:gap-3 lg:overflow-y-auto lg:overflow-x-hidden">
<div className="flex min-h-[50vh] flex-col items-center justify-center">
<Text variant="large" className="text-zinc-700">
No templates saved
</Text>
</div>
</div>
<InfiniteList
items={presets}
hasMore={!!hasMorePresets}
isFetchingMore={isFetchingMorePresets}
onEndReached={fetchMorePresets}
className="flex max-h-[76vh] flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300 lg:flex-col lg:gap-3 lg:overflow-y-auto lg:overflow-x-hidden"
itemWrapperClassName="w-auto lg:w-full"
renderItem={(preset) => (
<div className="w-[15rem] lg:w-full">
<TemplateListItem
preset={preset}
selected={selectedRunId === `preset:${preset.id}`}
onClick={() =>
onSelectRun && onSelectRun(`preset:${preset.id}`)
}
/>
</div>
)}
/>
</TabsLineContent>
</>
</TabsLine>

View File

@@ -8,6 +8,7 @@ interface RunListItemProps {
title: string;
description?: string;
icon?: React.ReactNode;
statusBadge?: React.ReactNode;
selected?: boolean;
onClick?: () => void;
}
@@ -16,6 +17,7 @@ export function RunSidebarCard({
title,
description,
icon,
statusBadge,
selected,
onClick,
}: RunListItemProps) {
@@ -30,12 +32,15 @@ export function RunSidebarCard({
<div className="flex min-w-0 items-center justify-start gap-3">
{icon}
<div className="flex min-w-0 flex-1 flex-col items-start justify-between gap-0">
<Text
variant="body-medium"
className="block w-full truncate text-ellipsis"
>
{title}
</Text>
<div className="flex w-full items-center justify-between">
<Text
variant="body-medium"
className="block truncate text-ellipsis"
>
{title}
</Text>
{statusBadge}
</div>
<Text variant="body" className="leading-tight !text-zinc-500">
{description}
</Text>

View File

@@ -0,0 +1,69 @@
"use client";
import React from "react";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { RunSidebarCard } from "./RunSidebarCard";
import { IconWrapper } from "./RunIconWrapper";
import { LinkIcon, PushPinIcon } from "@phosphor-icons/react";
interface TemplateListItemProps {
preset: LibraryAgentPreset;
selected?: boolean;
onClick?: () => void;
}
export function TemplateListItem({
preset,
selected,
onClick,
}: TemplateListItemProps) {
const isTrigger = !!preset.webhook;
const isActive = preset.is_active ?? false;
return (
<RunSidebarCard
title={preset.name}
description={preset.description || "No description"}
onClick={onClick}
selected={selected}
icon={
<IconWrapper
className={
isTrigger
? isActive
? "border-green-50 bg-green-50"
: "border-gray-50 bg-gray-50"
: "border-blue-50 bg-blue-50"
}
>
{isTrigger ? (
<LinkIcon
size={16}
className={isActive ? "text-green-700" : "text-gray-700"}
weight="bold"
/>
) : (
<PushPinIcon size={16} className="text-blue-700" weight="bold" />
)}
</IconWrapper>
}
statusBadge={
isTrigger ? (
<span
className={`rounded-full px-2 py-0.5 text-xs ${
isActive
? "bg-green-100 text-green-800"
: "bg-gray-100 text-gray-600"
}`}
>
{isActive ? "Active" : "Inactive"}
</span>
) : (
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs text-blue-800">
Template
</span>
)
}
/>
);
}

View File

@@ -1,30 +1,21 @@
import type { GraphExecutionsPaginated } from "@/app/api/__generated__/models/graphExecutionsPaginated";
import { Pagination } from "@/app/api/__generated__/models/pagination";
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(
export function getPaginatedTotalCount(
infiniteData: InfiniteData<unknown> | undefined,
runsLength: number,
): number {
const lastPage = infiniteData?.pages.at(-1);
if (!hasValidExecutionsData(lastPage)) return runsLength;
if (!hasValidPaginationInfo(lastPage)) return runsLength;
return lastPage.data.pagination?.total_items || runsLength;
}
export function getNextRunsPageParam(lastPage: unknown): number | undefined {
if (!hasValidExecutionsData(lastPage)) return undefined;
export function getPaginationNextPageNumber(
lastPage:
| { data: { pagination?: Pagination; [key: string]: any } }
| undefined,
): number | undefined {
if (!hasValidPaginationInfo(lastPage)) return undefined;
const { pagination } = lastPage.data;
const hasMore =
@@ -32,13 +23,56 @@ export function getNextRunsPageParam(lastPage: unknown): number | undefined {
return hasMore ? pagination.current_page + 1 : undefined;
}
export function extractRunsFromPages(
infiniteData: InfiniteData<unknown> | undefined,
) {
export function unpaginate<
TItemData extends object,
TPageDataKey extends string,
>(
infiniteData: InfiniteData<{
status: number;
data: { [key in TPageDataKey]: TItemData[] } | Record<string, any>;
}>,
pageListKey: TPageDataKey &
keyof (typeof infiniteData)["pages"][number]["data"],
): TItemData[] {
return (
infiniteData?.pages.flatMap((page) => {
if (!hasValidExecutionsData(page)) return [];
return page.data.executions || [];
if (!hasValidListPage<TItemData, TPageDataKey>(page, pageListKey))
return [];
return page.data[pageListKey] || [];
}) || []
);
}
function hasValidListPage<TItemData extends object, TKey extends string>(
page: unknown,
pageListKey: TKey,
): page is { data: { [key in TKey]: TItemData[] } } {
return (
typeof page === "object" &&
page !== null &&
"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
);
}

View File

@@ -4,15 +4,15 @@ import { useEffect, useMemo } from "react";
import { useGetV1ListGraphExecutionsInfinite } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { useGetV1ListExecutionSchedulesForAGraph } from "@/app/api/__generated__/endpoints/schedules/schedules";
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { useGetV2ListPresetsInfinite } from "@/app/api/__generated__/endpoints/presets/presets";
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,
getPaginatedTotalCount,
getPaginationNextPageNumber,
unpaginate,
} from "./helpers";
function parseTab(value: string | null): "runs" | "scheduled" | "templates" {
@@ -28,6 +28,7 @@ type Args = {
onCountsChange?: (info: {
runsCount: number;
schedulesCount: number;
presetsCount: number;
loading?: boolean;
}) => void;
};
@@ -52,7 +53,18 @@ export function useSidebarRunsList({
query: {
enabled: !!graphId,
refetchOnWindowFocus: false,
getNextPageParam: getNextRunsPageParam,
getNextPageParam: getPaginationNextPageNumber,
},
},
);
const presetsQuery = useGetV2ListPresetsInfinite(
{ graph_id: graphId || null, page: 1, page_size: 100 },
{
query: {
enabled: !!graphId,
refetchOnWindowFocus: false,
getNextPageParam: getPaginationNextPageNumber,
},
},
);
@@ -62,21 +74,31 @@ export function useSidebarRunsList({
{
query: {
enabled: !!graphId,
select: (r) => okData<GraphExecutionJobInfo[]>(r) ?? [],
select: (r) => okData(r) ?? [],
},
},
);
const runs = useMemo(
() => extractRunsFromPages(runsQuery.data),
() => (runsQuery.data ? unpaginate(runsQuery.data, "executions") : []),
[runsQuery.data],
);
const presets = useMemo(
() => (presetsQuery.data ? unpaginate(presetsQuery.data, "presets") : []),
[presetsQuery.data],
);
const schedules = schedulesQuery.data || [];
const runsCount = computeRunsCount(runsQuery.data, runs.length);
const runsCount = getPaginatedTotalCount(runsQuery.data, runs.length);
const presetsCount = getPaginatedTotalCount(
presetsQuery.data,
presets.length,
);
const schedulesCount = schedules.length;
const loading = !schedulesQuery.isSuccess || !runsQuery.isSuccess;
const loading =
!schedulesQuery.isSuccess ||
!runsQuery.isSuccess ||
!presetsQuery.isSuccess;
// Update query cache when execution events arrive via websocket
useExecutionEvents({
@@ -94,9 +116,9 @@ export function useSidebarRunsList({
// Notify parent about counts and loading state
useEffect(() => {
if (onCountsChange) {
onCountsChange({ runsCount, schedulesCount, loading });
onCountsChange({ runsCount, schedulesCount, presetsCount, loading });
}
}, [runsCount, schedulesCount, loading, onCountsChange]);
}, [runsCount, schedulesCount, presetsCount, loading, onCountsChange]);
useEffect(() => {
if (runs.length > 0 && tabValue === "runs" && !activeItem) {
@@ -104,24 +126,34 @@ export function useSidebarRunsList({
}
}, [runs, activeItem, tabValue, onSelectRun]);
// If there are no runs but there are schedules, and nothing is selected, auto-select the first schedule
// If there are no runs but there are schedules or presets, auto-select the first available
useEffect(() => {
if (!activeItem && runs.length === 0 && schedules.length > 0) {
onSelectRun(schedules[0].id, "scheduled");
if (!activeItem && runs.length === 0) {
if (schedules.length > 0) {
onSelectRun(`schedule:${schedules[0].id}`);
} else if (presets.length > 0) {
onSelectRun(`preset:${presets[0].id}`);
}
}
}, [activeItem, runs.length, schedules, onSelectRun]);
return {
runs,
presets,
schedules,
error: schedulesQuery.error || runsQuery.error,
error: schedulesQuery.error || runsQuery.error || presetsQuery.error,
loading,
runsQuery,
presetsQuery,
tabValue,
runsCount,
presetsCount,
schedulesCount,
fetchMoreRuns: runsQuery.fetchNextPage,
hasMoreRuns: runsQuery.hasNextPage,
fetchMoreRuns: runsQuery.fetchNextPage,
isFetchingMoreRuns: runsQuery.isFetchingNextPage,
hasMorePresets: presetsQuery.hasNextPage,
fetchMorePresets: presetsQuery.fetchNextPage,
isFetchingMorePresets: presetsQuery.isFetchingNextPage,
};
}

View File

@@ -1,5 +1,4 @@
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, useQueryStates } from "nuqs";
@@ -17,14 +16,10 @@ export function useNewAgentLibraryView() {
const agentId = id as string;
const {
data: response,
data: agent,
isSuccess,
error,
} = useGetV2GetLibraryAgent(agentId, {
query: {
select: okData<LibraryAgent>,
},
});
} = useGetV2GetLibraryAgent(agentId, { query: { select: okData } });
const [{ activeItem, activeTab: activeTabRaw }, setQueryStates] =
useQueryStates({
@@ -45,6 +40,7 @@ export function useNewAgentLibraryView() {
const [sidebarCounts, setSidebarCounts] = useState({
runsCount: 0,
schedulesCount: 0,
presetsCount: 0,
});
const [sidebarLoading, setSidebarLoading] = useState(true);
@@ -52,7 +48,8 @@ export function useNewAgentLibraryView() {
const hasAnyItems = useMemo(
() =>
(sidebarCounts.runsCount ?? 0) > 0 ||
(sidebarCounts.schedulesCount ?? 0) > 0,
(sidebarCounts.schedulesCount ?? 0) > 0 ||
(sidebarCounts.presetsCount ?? 0) > 0,
[sidebarCounts],
);
@@ -60,10 +57,10 @@ export function useNewAgentLibraryView() {
const showSidebarLayout = sidebarLoading || hasAnyItems;
useEffect(() => {
if (response) {
document.title = `${response.name} - Library - AutoGPT Platform`;
if (agent) {
document.title = `${agent.name} - Library - AutoGPT Platform`;
}
}, [response]);
}, [agent]);
function handleSelectRun(id: string, tab?: "runs" | "scheduled") {
setQueryStates({
@@ -88,11 +85,13 @@ export function useNewAgentLibraryView() {
(counts: {
runsCount: number;
schedulesCount: number;
presetsCount: number;
loading?: boolean;
}) => {
setSidebarCounts({
runsCount: counts.runsCount,
schedulesCount: counts.schedulesCount,
presetsCount: counts.presetsCount,
});
if (counts.loading !== undefined) {
setSidebarLoading(counts.loading);
@@ -105,7 +104,7 @@ export function useNewAgentLibraryView() {
agentId: id,
ready: isSuccess,
error,
agent: response,
agent,
hasAnyItems,
showSidebarLayout,
activeItem,

View File

@@ -9,13 +9,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?: object }>(
res: TResponse | undefined,
): (TResponse & { status: 200 })["data"] | undefined {
if (!res || typeof res !== "object") return undefined;
// status must exist and be exactly 200
@@ -26,7 +28,7 @@ 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;
}
type ResponseWithData = { status: number; data: unknown };