fix(frontend): new agent run page design refinements (#10924)

## Changes 🏗️

Implements all the following changes...

1. The margins between the runs, on the left hand side.. reduced them
around `6px` ?
2. Make agent inputs full width
3. Make "Schedule setup" section displayed in a second modal
4. When an agent is running, we should not show the "Delete agent"
button
5. Copy changes around the actions for agent/runs
6. Large button height should be `52px`
7. Fix margins between + New Run button and the runs & scheduled menu
8. Make border white on cards

Also... 
- improve the naming of some components to reflect better their
context/usage
- show on the inputs section when an agent is using already API keys or
credentials
- fix runs/schedules not auto-selecting once created

## Checklist 📋

### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run the app locally with the new agent runs page enabled
  - [x] Test the above 

### For configuration changes:

None
This commit is contained in:
Ubbe
2025-09-16 23:34:52 +09:00
committed by GitHub
parent 17fcf68f2e
commit 1ddf92eed4
45 changed files with 568 additions and 401 deletions

View File

@@ -5,8 +5,8 @@ import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { useAgentRunsView } from "./useAgentRunsView";
import { AgentRunsLoading } from "./components/AgentRunsLoading";
import { RunsSidebar } from "./components/RunsSidebar/RunsSidebar";
import { RunDetails } from "./components/RunDetails/RunDetails";
import { ScheduleDetails } from "./components/ScheduleDetails/ScheduleDetails";
import { SelectedRunView } from "./components/SelectedRunView/SelectedRunView";
import { SelectedScheduleView } from "./components/SelectedScheduleView/SelectedScheduleView";
import { EmptyAgentRuns } from "./components/EmptyAgentRuns/EmptyAgentRuns";
import { Button } from "@/components/atoms/Button/Button";
import { RunAgentModal } from "./components/RunAgentModal/RunAgentModal";
@@ -45,12 +45,12 @@ export function AgentRunsView() {
<div
className={
showSidebarLayout
? "grid h-screen grid-cols-1 gap-0 pt-6 md:gap-4 lg:grid-cols-[25%_70%]"
: "grid h-screen grid-cols-1 gap-0 pt-6 md:gap-4"
? "grid h-screen grid-cols-1 gap-0 pt-3 md:gap-4 lg:grid-cols-[25%_70%]"
: "grid h-screen grid-cols-1 gap-0 pt-3 md:gap-4"
}
>
<div className={showSidebarLayout ? "p-4 pl-5" : "hidden p-4 pl-5"}>
<div className="mb-6">
<div className="mb-4">
<RunAgentModal
triggerSlot={
<Button variant="primary" size="large" className="w-full">
@@ -59,6 +59,10 @@ export function AgentRunsView() {
}
agent={agent}
agentId={agent.id.toString()}
onRunCreated={(execution) => handleSelectRun(execution.id)}
onScheduleCreated={(schedule) =>
handleSelectRun(`schedule:${schedule.id}`)
}
/>
</div>
<RunsSidebar
@@ -82,13 +86,13 @@ export function AgentRunsView() {
<div className="mt-1">
{selectedRun ? (
selectedRun.startsWith("schedule:") ? (
<ScheduleDetails
<SelectedScheduleView
agent={agent}
scheduleId={selectedRun.replace("schedule:", "")}
onClearSelectedRun={handleClearSelectedRun}
/>
) : (
<RunDetails
<SelectedRunView
agent={agent}
runId={selectedRun}
onSelectRun={handleSelectRun}

View File

@@ -103,7 +103,7 @@ export function AgentActionsDropdown({ agent }: Props) {
onClick={handleExport}
className="flex items-center gap-2"
>
<FileArrowDownIcon size={16} /> Export agent to file
<FileArrowDownIcon size={16} /> Export agent
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setShowDeleteDialog(true)}

View File

@@ -2,57 +2,88 @@
import React from "react";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
import { toDisplayName } from "@/components/integrations/helper";
import {
getAgentCredentialsFields,
getAgentInputFields,
getCredentialTypeDisplayName,
renderValue,
} from "./helpers";
type Props = {
agent: LibraryAgent;
inputs?: Record<string, any> | null;
credentialInputs?: Record<string, CredentialsMetaInput> | null;
};
function getAgentInputFields(agent: LibraryAgent): Record<string, any> {
const 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(
([, sub]) => !sub?.hidden,
);
return Object.fromEntries(visibleEntries);
}
function renderValue(value: any): string {
if (value === undefined || value === null) return "";
if (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean"
)
return String(value);
try {
return JSON.stringify(value, undefined, 2);
} catch {
return String(value);
}
}
export function AgentInputsReadOnly({ agent, inputs }: Props) {
export function AgentInputsReadOnly({
agent,
inputs,
credentialInputs,
}: Props) {
const fields = getAgentInputFields(agent);
const entries = Object.entries(fields);
const credentialFields = getAgentCredentialsFields(agent);
const inputEntries = Object.entries(fields);
const credentialEntries = Object.entries(credentialFields);
if (!inputs || entries.length === 0) {
const hasInputs = inputs && inputEntries.length > 0;
const hasCredentials = credentialInputs && credentialEntries.length > 0;
if (!hasInputs && !hasCredentials) {
return <div className="text-neutral-600">No input for this run.</div>;
}
return (
<div className="flex flex-col gap-4">
{entries.map(([key, sub]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">{sub?.title || key}</label>
<p className="whitespace-pre-wrap break-words text-sm text-neutral-700">
{renderValue((inputs as Record<string, any>)[key])}
</p>
<div className="flex flex-col gap-6">
{/* Regular inputs */}
{hasInputs && (
<div className="flex flex-col gap-4">
{inputEntries.map(([key, sub]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">{sub?.title || key}</label>
<p className="whitespace-pre-wrap break-words text-sm text-neutral-700">
{renderValue((inputs as Record<string, any>)[key])}
</p>
</div>
))}
</div>
))}
)}
{/* Credentials */}
{hasCredentials && (
<div className="flex flex-col gap-6">
{hasInputs && <div className="border-t border-neutral-200 pt-4" />}
{credentialEntries.map(([key, _sub]) => {
const credential = credentialInputs![key];
if (!credential) return null;
return (
<div key={key} className="flex flex-col gap-4">
<h3 className="text-lg font-medium text-neutral-900">
{toDisplayName(credential.provider)} credentials
</h3>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between text-sm">
<span className="text-neutral-600">Name</span>
<span className="text-neutral-600">
{getCredentialTypeDisplayName(credential.type)}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-neutral-900">
{credential.title || "Untitled"}
</span>
<span className="font-mono text-neutral-400">
{"*".repeat(25)}
</span>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,54 @@
import { CredentialsMetaResponseType } from "@/app/api/__generated__/models/credentialsMetaResponseType";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
export function getCredentialTypeDisplayName(type: string): string {
const typeDisplayMap: Record<CredentialsMetaResponseType, string> = {
[CredentialsMetaResponseType.api_key]: "API key",
[CredentialsMetaResponseType.oauth2]: "OAuth2",
[CredentialsMetaResponseType.user_password]: "Username/Password",
[CredentialsMetaResponseType.host_scoped]: "Host-Scoped",
};
return typeDisplayMap[type as CredentialsMetaResponseType] || type;
}
export function getAgentInputFields(agent: LibraryAgent): Record<string, any> {
const 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(
([, sub]) => !sub?.hidden,
);
return Object.fromEntries(visibleEntries);
}
export function getAgentCredentialsFields(
agent: LibraryAgent,
): Record<string, any> {
if (
!agent.credentials_input_schema ||
typeof agent.credentials_input_schema !== "object" ||
!("properties" in agent.credentials_input_schema) ||
!agent.credentials_input_schema.properties
) {
return {};
}
return agent.credentials_input_schema.properties as Record<string, any>;
}
export function renderValue(value: any): string {
if (value === undefined || value === null) return "";
if (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean"
)
return String(value);
try {
return JSON.stringify(value, undefined, 2);
} catch {
return String(value);
}
}

View File

@@ -1,9 +1,7 @@
"use client";
import React, { useState } from "react";
import { CopyIcon, CheckIcon } from "lucide-react";
import React from "react";
import { OutputRenderer, OutputMetadata } from "../types";
import { copyToClipboard } from "../utils/copy";
interface OutputItemProps {
value: any;
@@ -18,51 +16,13 @@ export function OutputItem({
renderer,
label,
}: OutputItemProps) {
const [showCopyButton, setShowCopyButton] = useState(false);
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
const copyContent = renderer.getCopyContent(value, metadata);
if (copyContent) {
try {
await copyToClipboard(copyContent);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error("Failed to copy:", error);
}
}
};
const canCopy = renderer.getCopyContent(value, metadata) !== null;
return (
<div
className="relative"
onMouseEnter={() => setShowCopyButton(true)}
onMouseLeave={() => setShowCopyButton(false)}
>
<div className="relative">
{label && (
<label className="mb-1.5 block text-sm font-medium">{label}</label>
)}
<div className="relative">
{renderer.render(value, metadata)}
{canCopy && showCopyButton && (
<button
onClick={handleCopy}
className="absolute right-2 top-2 rounded-md border border-gray-200 bg-background/80 p-1.5 backdrop-blur-sm transition-all duration-200 hover:bg-gray-100"
aria-label="Copy content"
>
{copied ? (
<CheckIcon className="h-4 w-4 text-green-600" />
) : (
<CopyIcon className="h-4 w-4 text-gray-600" />
)}
</button>
)}
</div>
<div className="relative">{renderer.render(value, metadata)}</div>
</div>
);
}

View File

@@ -228,5 +228,7 @@ export function RunAgentInputs({
);
}
return <div className="no-drag relative flex">{innerInputElement}</div>;
return (
<div className="no-drag relative flex w-full">{innerInputElement}</div>
);
}

View File

@@ -8,28 +8,34 @@ import { useAgentRunModal } from "./useAgentRunModal";
import { ModalHeader } from "./components/ModalHeader/ModalHeader";
import { AgentCostSection } from "./components/AgentCostSection/AgentCostSection";
import { AgentSectionHeader } from "./components/AgentSectionHeader/AgentSectionHeader";
import { ModalRunSection } from "./components/ModalRunSection/DefaultRunView";
import { ModalRunSection } from "./components/ModalRunSection/ModalRunSection";
import { RunAgentModalContextProvider } from "./context";
import { ModalScheduleSection } from "./components/ModalScheduleSection/ScheduleView";
import { AgentDetails } from "./components/AgentDetails/AgentDetails";
import { RunActions } from "./components/RunActions/RunActions";
import { ScheduleActions } from "./components/ScheduleActions/ScheduleActions";
import { Text } from "@/components/atoms/Text/Text";
import { AlarmIcon, TrashIcon } from "@phosphor-icons/react";
import { ScheduleAgentModal } from "../ScheduleAgentModal/ScheduleAgentModal";
import { AlarmIcon } from "@phosphor-icons/react";
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
interface Props {
triggerSlot: React.ReactNode;
agent: LibraryAgent;
agentId: string;
agentVersion?: number;
onRunCreated?: (execution: GraphExecutionMeta) => void;
onScheduleCreated?: (schedule: GraphExecutionJobInfo) => void;
}
export function RunAgentModal({ triggerSlot, agent }: Props) {
export function RunAgentModal({
triggerSlot,
agent,
onRunCreated,
onScheduleCreated,
}: Props) {
const {
// UI state
isOpen,
setIsOpen,
showScheduleView,
// Run mode
defaultRunType,
@@ -48,10 +54,6 @@ export function RunAgentModal({ triggerSlot, agent }: Props) {
setPresetName,
setPresetDescription,
// Scheduling
scheduleName,
cronExpression,
// Validation/readiness
allRequiredInputsAreSet,
@@ -61,19 +63,15 @@ export function RunAgentModal({ triggerSlot, agent }: Props) {
// Async states
isExecuting,
isCreatingSchedule,
isSettingUpTrigger,
// Actions
handleRun,
handleSchedule,
handleShowSchedule,
handleGoBack,
handleSetScheduleName,
handleSetCronExpression,
} = useAgentRunModal(agent);
} = useAgentRunModal(agent, {
onRun: onRunCreated,
});
const [isScheduleFormValid, setIsScheduleFormValid] = useState(true);
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
const hasAnySetupFields =
Object.keys(agentInputFields || {}).length > 0 ||
@@ -100,14 +98,20 @@ export function RunAgentModal({ triggerSlot, agent }: Props) {
function handleSetOpen(open: boolean) {
setIsOpen(open);
// Always reset to Run view when opening/closing
if (open || !open) handleGoBack();
}
function handleRemoveSchedule() {
handleGoBack();
handleSetScheduleName("");
handleSetCronExpression("");
function handleOpenScheduleModal() {
setIsScheduleModalOpen(true);
}
function handleCloseScheduleModal() {
setIsScheduleModalOpen(false);
}
function handleScheduleCreated(schedule: GraphExecutionJobInfo) {
handleCloseScheduleModal();
setIsOpen(false); // Close the main RunAgentModal
onScheduleCreated?.(schedule);
}
return (
@@ -154,61 +158,12 @@ export function RunAgentModal({ triggerSlot, agent }: Props) {
: "Agent Setup"
}
/>
<div>
<ModalRunSection />
</div>
<ModalRunSection />
</>
</RunAgentModalContextProvider>
) : null}
</div>
{/* Schedule Section - always visible */}
<div className="mt-4">
<AgentSectionHeader title="Schedule Setup" />
{showScheduleView ? (
<>
<div className="my-4 flex justify-start">
<Button
variant="secondary"
size="small"
onClick={handleRemoveSchedule}
>
<TrashIcon size={16} />
Remove schedule
</Button>
</div>
<ModalScheduleSection
scheduleName={scheduleName}
cronExpression={cronExpression}
recommendedScheduleCron={agent.recommended_schedule_cron}
onScheduleNameChange={handleSetScheduleName}
onCronExpressionChange={handleSetCronExpression}
onValidityChange={setIsScheduleFormValid}
/>
</>
) : (
<div className="mt-2 flex flex-col items-start gap-2">
<Text variant="body" className="mb-3 !text-zinc-500">
No schedule configured. Create a schedule to run this
agent automatically at a specific time.{" "}
{agent.recommended_schedule_cron && (
<span className="text-blue-600">
This agent has a recommended schedule.
</span>
)}
</Text>
<Button
variant="secondary"
size="small"
onClick={handleShowSchedule}
>
<AlarmIcon size={16} />
Create schedule
</Button>
</div>
)}
</div>
{/* Agent Details Section */}
<div className="mt-8">
<AgentSectionHeader title="Agent Details" />
@@ -220,25 +175,33 @@ export function RunAgentModal({ triggerSlot, agent }: Props) {
className="fixed bottom-1 left-0 z-10 w-full bg-white p-4"
style={{ boxShadow: "0px -8px 10px white" }}
>
{showScheduleView ? (
<ScheduleActions
onSchedule={handleSchedule}
isCreatingSchedule={isCreatingSchedule}
allRequiredInputsAreSet={
allRequiredInputsAreSet &&
!!scheduleName.trim() &&
isScheduleFormValid
<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}
allRequiredInputsAreSet={allRequiredInputsAreSet}
isRunReady={allRequiredInputsAreSet}
/>
)}
</div>
<ScheduleAgentModal
isOpen={isScheduleModalOpen}
onClose={handleCloseScheduleModal}
agent={agent}
inputValues={inputValues}
inputCredentials={inputCredentials}
onScheduleCreated={handleScheduleCreated}
/>
</Dialog.Footer>
</Dialog.Content>
</Dialog>

View File

@@ -6,7 +6,7 @@ interface Props {
export function AgentSectionHeader({ title }: Props) {
return (
<div className="border-t border-zinc-400 px-0 py-2">
<div className="border-t border-zinc-400 px-0 pb-2 pt-1">
<Text variant="label" className="!text-zinc-700">
{title}
</Text>

View File

@@ -4,6 +4,9 @@ import SchemaTooltip from "@/components/SchemaTooltip";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/CredentialsInputs/CredentialsInputs";
import { useRunAgentModalContext } from "../../context";
import { RunAgentInputs } from "../../../RunAgentInputs/RunAgentInputs";
import { Text } from "@/components/atoms/Text/Text";
import { toDisplayName } from "@/components/integrations/helper";
import { getCredentialTypeDisplayName } from "./helpers";
export function ModalRunSection() {
const {
@@ -22,7 +25,7 @@ export function ModalRunSection() {
} = useRunAgentModalContext();
return (
<div className="my-4">
<div className="mb-10 mt-4">
{defaultRunType === "automatic-trigger" && <WebhookTriggerBanner />}
{/* Preset/Trigger fields */}
@@ -82,7 +85,7 @@ export function ModalRunSection() {
{/* Regular inputs */}
{Object.entries(agentInputFields || {}).map(([key, inputSubSchema]) => (
<div key={key} className="flex flex-col gap-0 space-y-2">
<div key={key} className="flex w-full flex-col gap-0 space-y-2">
<label className="flex items-center gap-1 text-sm font-medium">
{inputSubSchema.title || key}
<SchemaTooltip description={inputSubSchema.description} />
@@ -97,6 +100,56 @@ export function ModalRunSection() {
/>
</div>
))}
{/* Selected Credentials Preview */}
{Object.keys(inputCredentials).length > 0 && (
<div className="mt-6 flex flex-col gap-6">
{Object.entries(agentCredentialsInputFields || {}).map(
([key, _sub]) => {
const credential = inputCredentials[key];
if (!credential) return null;
return (
<div key={key} className="flex flex-col gap-4">
<Text variant="body-medium" as="h3">
{toDisplayName(credential.provider)} credentials
</Text>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between text-sm">
<Text
variant="body"
as="span"
className="!text-neutral-600"
>
Name
</Text>
<Text
variant="body"
as="span"
className="!text-neutral-600"
>
{getCredentialTypeDisplayName(credential.type)}
</Text>
</div>
<div className="flex items-center justify-between text-sm">
<Text
variant="body"
as="span"
className="!text-neutral-900"
>
{credential.title || "Untitled"}
</Text>
<span className="font-mono text-neutral-400">
{"*".repeat(25)}
</span>
</div>
</div>
</div>
);
},
)}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,12 @@
import { CredentialsMetaResponseType } from "@/app/api/__generated__/models/credentialsMetaResponseType";
export function getCredentialTypeDisplayName(type: string): string {
const typeDisplayMap: Record<CredentialsMetaResponseType, string> = {
[CredentialsMetaResponseType.api_key]: "API key",
[CredentialsMetaResponseType.oauth2]: "OAuth2",
[CredentialsMetaResponseType.user_password]: "Username/Password",
[CredentialsMetaResponseType.host_scoped]: "Host-Scoped",
};
return typeDisplayMap[type as CredentialsMetaResponseType] || type;
}

View File

@@ -6,7 +6,7 @@ interface Props {
onRun: () => void;
isExecuting?: boolean;
isSettingUpTrigger?: boolean;
allRequiredInputsAreSet?: boolean;
isRunReady?: boolean;
}
export function RunActions({
@@ -14,14 +14,14 @@ export function RunActions({
onRun,
isExecuting = false,
isSettingUpTrigger = false,
allRequiredInputsAreSet = true,
isRunReady = true,
}: Props) {
return (
<div className="flex justify-end gap-3">
<Button
variant="primary"
onClick={onRun}
disabled={!allRequiredInputsAreSet || isExecuting || isSettingUpTrigger}
disabled={!isRunReady || isExecuting || isSettingUpTrigger}
loading={isExecuting || isSettingUpTrigger}
>
{defaultRunType === "automatic-trigger"

View File

@@ -1,26 +0,0 @@
import { Button } from "@/components/atoms/Button/Button";
interface Props {
onSchedule: () => void;
isCreatingSchedule?: boolean;
allRequiredInputsAreSet?: boolean;
}
export function ScheduleActions({
onSchedule,
isCreatingSchedule = false,
allRequiredInputsAreSet = true,
}: Props) {
return (
<div className="flex justify-end gap-3">
<Button
variant="primary"
onClick={onSchedule}
disabled={!allRequiredInputsAreSet || isCreatingSchedule}
loading={isCreatingSchedule}
>
Schedule Agent
</Button>
</div>
);
}

View File

@@ -7,15 +7,9 @@ import {
usePostV1ExecuteGraphAgent,
getGetV1ListGraphExecutionsInfiniteQueryOptions,
} from "@/app/api/__generated__/endpoints/graphs/graphs";
import {
usePostV1CreateExecutionSchedule as useCreateSchedule,
getGetV1ListExecutionSchedulesForAGraphQueryKey,
} from "@/app/api/__generated__/endpoints/schedules/schedules";
import { usePostV2SetupTrigger } 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";
import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth";
export type RunVariant =
| "manual"
@@ -25,7 +19,6 @@ export type RunVariant =
interface UseAgentRunModalCallbacks {
onRun?: (execution: GraphExecutionMeta) => void;
onCreateSchedule?: (schedule: GraphExecutionJobInfo) => void;
onSetupTrigger?: (preset: LibraryAgentPreset) => void;
}
@@ -36,26 +29,12 @@ export function useAgentRunModal(
const { toast } = useToast();
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 [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(
agent.recommended_schedule_cron || "0 9 * * 1",
);
// Get user timezone for scheduling
const { data: userTimezone } = useGetV1GetUserTimezone({
query: {
select: (res) => (res.status === 200 ? res.data.timezone : undefined),
},
});
// Determine the default run type based on agent capabilities
const defaultRunType: RunVariant = agent.has_external_trigger
? "automatic-trigger"
@@ -89,33 +68,6 @@ export function useAgentRunModal(
},
});
const createScheduleMutation = useCreateSchedule({
mutation: {
onSuccess: (response) => {
if (response.status === 200) {
toast({
title: "Schedule created",
});
callbacks?.onCreateSchedule?.(response.data);
// Invalidate schedules list for this graph
queryClient.invalidateQueries({
queryKey: getGetV1ListExecutionSchedulesForAGraphQueryKey(
agent.graph_id,
),
});
setIsOpen(false);
}
},
onError: (error: any) => {
toast({
title: "❌ Failed to create schedule",
description: error.message || "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
const setupTriggerMutation = usePostV2SetupTrigger({
mutation: {
onSuccess: (response: any) => {
@@ -210,33 +162,25 @@ export function useAgentRunModal(
[allRequiredInputsAreSetRaw, credentialsRequired, allCredentialsAreSet],
);
const notifyMissingRequirements = useCallback(
(needScheduleName: boolean = false) => {
const allMissingFields = (
needScheduleName && !scheduleName ? ["schedule_name"] : []
)
.concat(missingInputs)
.concat(
credentialsRequired && !allCredentialsAreSet
? missingCredentials.map((k) => `credentials:${k}`)
: [],
);
const notifyMissingRequirements = useCallback(() => {
const allMissingFields = missingInputs.concat(
credentialsRequired && !allCredentialsAreSet
? missingCredentials.map((k) => `credentials:${k}`)
: [],
);
toast({
title: "⚠️ Missing required inputs",
description: `Please provide: ${allMissingFields.map((k) => `"${k}"`).join(", ")}`,
variant: "destructive",
});
},
[
missingInputs,
scheduleName,
toast,
credentialsRequired,
allCredentialsAreSet,
missingCredentials,
],
);
toast({
title: "⚠️ Missing required inputs",
description: `Please provide: ${allMissingFields.map((k) => `"${k}"`).join(", ")}`,
variant: "destructive",
});
}, [
missingInputs,
toast,
credentialsRequired,
allCredentialsAreSet,
missingCredentials,
]);
// Action handlers
const handleRun = useCallback(() => {
@@ -247,7 +191,7 @@ export function useAgentRunModal(
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.",
@@ -258,7 +202,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,
@@ -280,7 +224,6 @@ export function useAgentRunModal(
}, [
allRequiredInputsAreSet,
defaultRunType,
scheduleName,
inputValues,
inputCredentials,
agent,
@@ -292,70 +235,6 @@ export function useAgentRunModal(
toast,
]);
const handleSchedule = useCallback(() => {
if (!allRequiredInputsAreSet) {
notifyMissingRequirements(true);
return;
}
if (!scheduleName.trim()) {
toast({
title: "⚠️ Schedule name required",
description: "Please provide a name for your schedule.",
variant: "destructive",
});
return;
}
createScheduleMutation.mutate({
graphId: agent.graph_id,
data: {
name: presetName || scheduleName,
cron: cronExpression,
inputs: inputValues,
graph_version: agent.graph_version,
credentials: inputCredentials,
timezone:
userTimezone && userTimezone !== "not-set" ? userTimezone : undefined,
},
});
}, [
allRequiredInputsAreSet,
scheduleName,
cronExpression,
inputValues,
inputCredentials,
agent,
notifyMissingRequirements,
createScheduleMutation,
toast,
userTimezone,
]);
function handleShowSchedule() {
// Initialize with sensible defaults when entering schedule view
setScheduleName((prev) => prev || defaultScheduleName);
setCronExpression(
(prev) => prev || agent.recommended_schedule_cron || "0 9 * * 1",
);
setShowScheduleView(true);
}
function handleGoBack() {
setShowScheduleView(false);
// Reset schedule fields on exit
setScheduleName(defaultScheduleName);
setCronExpression(agent.recommended_schedule_cron || "0 9 * * 1");
}
function handleSetScheduleName(name: string) {
setScheduleName(name);
}
function handleSetCronExpression(expression: string) {
setCronExpression(expression);
}
const hasInputFields = useMemo(() => {
return Object.keys(agentInputFields).length > 0;
}, [agentInputFields]);
@@ -364,7 +243,6 @@ export function useAgentRunModal(
// UI state
isOpen,
setIsOpen,
showScheduleView,
// Run mode
defaultRunType,
@@ -383,10 +261,6 @@ export function useAgentRunModal(
setPresetName,
setPresetDescription,
// Scheduling
scheduleName,
cronExpression,
// Validation/readiness
allRequiredInputsAreSet,
missingInputs,
@@ -398,15 +272,9 @@ export function useAgentRunModal(
// Async states
isExecuting: executeGraphMutation.isPending,
isCreatingSchedule: createScheduleMutation.isPending,
isSettingUpTrigger: setupTriggerMutation.isPending,
// Actions
handleRun,
handleSchedule,
handleShowSchedule,
handleGoBack,
handleSetScheduleName,
handleSetCronExpression,
};
}

View File

@@ -9,7 +9,7 @@ export function RunDetailCard({ children, className }: Props) {
return (
<div
className={cn(
"min-h-20 rounded-xlarge border border-slate-50/70 bg-white p-6",
"min-h-20 rounded-large border border-white bg-white p-6",
className,
)}
>

View File

@@ -1,5 +1,5 @@
import React from "react";
import { RunStatusBadge } from "../RunDetails/components/RunStatusBadge";
import { RunStatusBadge } from "../SelectedRunView/components/RunStatusBadge";
import { Text } from "@/components/atoms/Text/Text";
import { Button } from "@/components/atoms/Button/Button";
import {
@@ -34,6 +34,7 @@ export function RunDetailHeader({
canStop,
isStopping,
isDeleting,
isRunning,
isRunningAgain,
openInBuilderHref,
showDeleteDialog,
@@ -67,13 +68,15 @@ export function RunDetailHeader({
>
<PlayIcon size={16} /> Run again
</Button>
<Button
variant="secondary"
size="small"
onClick={() => handleShowDeleteDialog(true)}
>
<TrashIcon size={16} /> Delete
</Button>
{!isRunning ? (
<Button
variant="secondary"
size="small"
onClick={() => handleShowDeleteDialog(true)}
>
<TrashIcon size={16} /> Delete run
</Button>
) : null}
{openInBuilderHref ? (
<Button
variant="secondary"
@@ -82,7 +85,7 @@ export function RunDetailHeader({
href={openInBuilderHref}
target="_blank"
>
<ArrowSquareOutIcon size={16} /> Open in builder
<ArrowSquareOutIcon size={16} /> Edit run
</Button>
) : null}
{canStop ? (
@@ -92,7 +95,7 @@ export function RunDetailHeader({
onClick={handleStopRun}
disabled={isStopping}
>
<StopIcon size={14} /> Stop run
<StopIcon size={14} /> Stop agent
</Button>
) : null}
<AgentActionsDropdown agent={agent} />

View File

@@ -144,6 +144,7 @@ export function useRunDetailHeader(
canStop,
isStopping,
isDeleting,
isRunning: run?.status === "RUNNING",
isRunningAgain,
handleShowDeleteDialog,
handleDeleteRun,

View File

@@ -96,10 +96,10 @@ export function RunsSidebar({
hasMore={!!hasMoreRuns}
isFetchingMore={isFetchingMoreRuns}
onEndReached={fetchMoreRuns}
className="flex flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 lg:flex-col lg:gap-1 lg:overflow-x-hidden"
className="flex flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 lg:flex-col lg:gap-3 lg:overflow-x-hidden"
itemWrapperClassName="w-auto lg:w-full"
renderItem={(run) => (
<div className="mb-3 w-[15rem] lg:w-full">
<div className="w-[15rem] lg:w-full">
<RunListItem
run={run}
title={agent.name}
@@ -111,9 +111,9 @@ export function RunsSidebar({
/>
</TabsLineContent>
<TabsLineContent value="scheduled">
<div className="flex flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 lg:flex-col lg:gap-1 lg:overflow-x-hidden">
<div className="flex flex-nowrap items-center justify-start gap-4 overflow-x-scroll px-1 pb-4 pt-1 lg:flex-col lg:gap-3 lg:overflow-x-hidden">
{schedules.map((s: GraphExecutionJobInfo) => (
<div className="mb-3 w-[15rem] lg:w-full" key={s.id}>
<div className="w-[15rem] lg:w-full" key={s.id}>
<ScheduleListItem
schedule={s}
selected={selectedRunId === `schedule:${s.id}`}

View File

@@ -23,7 +23,7 @@ export function RunSidebarCard({
<button
className={cn(
"w-full rounded-large border border-slate-50/70 bg-white p-3 text-left transition-all duration-150 hover:scale-[1.01] hover:bg-slate-50/50",
selected ? "ring-2 ring-slate-800" : undefined,
selected ? "large ring-2 ring-slate-800" : undefined,
)}
onClick={onClick}
>

View File

@@ -0,0 +1,110 @@
"use client";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { Button } from "@/components/atoms/Button/Button";
import { useState } from "react";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { ModalScheduleSection } from "./components/ModalScheduleSection/ModalScheduleSection";
import { Text } from "@/components/atoms/Text/Text";
import { useScheduleAgentModal } from "./useScheduleAgentModal";
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
interface Props {
isOpen: boolean;
onClose: () => void;
agent: LibraryAgent;
inputValues: Record<string, any>;
inputCredentials: Record<string, any>;
onScheduleCreated?: (schedule: GraphExecutionJobInfo) => void;
}
export function ScheduleAgentModal({
isOpen,
onClose,
agent,
inputValues,
inputCredentials,
onScheduleCreated,
}: Props) {
const [isScheduleFormValid, setIsScheduleFormValid] = useState(true);
const {
scheduleName,
cronExpression,
isCreatingSchedule,
handleSchedule,
handleSetScheduleName,
handleSetCronExpression,
resetForm,
} = useScheduleAgentModal(agent, inputValues, inputCredentials, {
onCreateSchedule: (schedule) => {
onScheduleCreated?.(schedule);
},
onClose: onClose,
});
function handleClose() {
resetForm();
setIsScheduleFormValid(true);
onClose();
}
async function handleScheduleClick() {
if (!scheduleName.trim() || !isScheduleFormValid) return;
try {
await handleSchedule(scheduleName, cronExpression);
} catch (error) {
// Error handling is done in the hook
console.error("Failed to create schedule:", error);
}
}
const canSchedule = scheduleName.trim() && isScheduleFormValid;
return (
<Dialog
controlled={{ isOpen, set: handleClose }}
styling={{ maxWidth: "600px", maxHeight: "90vh" }}
>
<Dialog.Content>
<div className="flex h-full flex-col">
<Text variant="lead" as="h2" className="!font-medium !text-black">
Schedule run
</Text>
{/* Content */}
<div className="overflow-y-auto">
<ModalScheduleSection
scheduleName={scheduleName}
cronExpression={cronExpression}
recommendedScheduleCron={agent.recommended_schedule_cron}
onScheduleNameChange={handleSetScheduleName}
onCronExpressionChange={handleSetCronExpression}
onValidityChange={setIsScheduleFormValid}
/>
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-3 pt-6">
<Button
variant="secondary"
onClick={handleClose}
disabled={isCreatingSchedule}
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleScheduleClick}
loading={isCreatingSchedule}
disabled={!canSchedule}
>
Schedule
</Button>
</div>
</div>
</Dialog.Content>
</Dialog>
);
}

View File

@@ -51,9 +51,9 @@ export function TimeAt({
}
return (
<div className="flex items-end gap-2">
<div className="flex items-end gap-1">
<div className="relative">
<label className="mb-1 block text-xs font-medium text-zinc-700">
<label className="mb-0 block text-sm font-medium text-zinc-700">
At
</label>
<div className="flex items-center gap-2">

View File

@@ -0,0 +1,128 @@
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { useState, useCallback, useMemo } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useToast } from "@/components/molecules/Toast/use-toast";
import {
usePostV1CreateExecutionSchedule as useCreateSchedule,
getGetV1ListExecutionSchedulesForAGraphQueryKey,
} from "@/app/api/__generated__/endpoints/schedules/schedules";
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
interface UseScheduleAgentModalCallbacks {
onCreateSchedule?: (schedule: GraphExecutionJobInfo) => void;
onClose?: () => void;
}
export function useScheduleAgentModal(
agent: LibraryAgent,
inputValues: Record<string, any>,
inputCredentials: Record<string, any>,
callbacks?: UseScheduleAgentModalCallbacks,
) {
const { toast } = useToast();
const queryClient = useQueryClient();
const defaultScheduleName = useMemo(() => `Run ${agent.name}`, [agent.name]);
const [scheduleName, setScheduleName] = useState(defaultScheduleName);
const [cronExpression, setCronExpression] = useState(
agent.recommended_schedule_cron || "0 9 * * 1",
);
const createScheduleMutation = useCreateSchedule({
mutation: {
onSuccess: (response) => {
if (response.status === 200) {
toast({
title: "Schedule created",
});
callbacks?.onCreateSchedule?.(response.data);
// Invalidate schedules list for this graph
queryClient.invalidateQueries({
queryKey: getGetV1ListExecutionSchedulesForAGraphQueryKey(
agent.graph_id,
),
});
// Reset form
setScheduleName(defaultScheduleName);
setCronExpression(agent.recommended_schedule_cron || "0 9 * * 1");
callbacks?.onClose?.();
}
},
onError: (error: any) => {
toast({
title: "❌ Failed to create schedule",
description: error.message || "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
const handleSchedule = useCallback(
(scheduleName: string, cronExpression: string) => {
if (!scheduleName.trim()) {
toast({
title: "⚠️ Schedule name required",
description: "Please provide a name for your schedule.",
variant: "destructive",
});
return Promise.reject(new Error("Schedule name required"));
}
return new Promise<void>((resolve, reject) => {
createScheduleMutation.mutate(
{
graphId: agent.graph_id,
data: {
name: scheduleName,
cron: cronExpression,
inputs: inputValues,
graph_version: agent.graph_version,
credentials: inputCredentials,
},
},
{
onSuccess: () => resolve(),
onError: (error) => reject(error),
},
);
});
},
[
agent.graph_id,
agent.graph_version,
inputValues,
inputCredentials,
createScheduleMutation,
toast,
],
);
const handleSetScheduleName = useCallback((name: string) => {
setScheduleName(name);
}, []);
const handleSetCronExpression = useCallback((expression: string) => {
setCronExpression(expression);
}, []);
const resetForm = useCallback(() => {
setScheduleName(defaultScheduleName);
setCronExpression(agent.recommended_schedule_cron || "0 9 * * 1");
}, [defaultScheduleName, agent.recommended_schedule_cron]);
return {
// State
scheduleName,
cronExpression,
// Loading state
isCreatingSchedule: createScheduleMutation.isPending,
// Actions
handleSchedule,
handleSetScheduleName,
handleSetCronExpression,
resetForm,
};
}

View File

@@ -7,7 +7,7 @@ import {
TabsLineList,
TabsLineTrigger,
} from "@/components/molecules/TabsLine/TabsLine";
import { useRunDetails } from "./useRunDetails";
import { useSelectedRunView } from "./useSelectedRunView";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
@@ -16,20 +16,20 @@ import { AgentInputsReadOnly } from "../AgentInputsReadOnly/AgentInputsReadOnly"
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
import { RunOutputs } from "./components/RunOutputs";
interface RunDetailsProps {
interface Props {
agent: LibraryAgent;
runId: string;
onSelectRun?: (id: string) => void;
onClearSelectedRun?: () => void;
}
export function RunDetails({
export function SelectedRunView({
agent,
runId,
onSelectRun,
onClearSelectedRun,
}: RunDetailsProps) {
const { run, isLoading, responseError, httpError } = useRunDetails(
}: Props) {
const { run, isLoading, responseError, httpError } = useSelectedRunView(
agent.graph_id,
runId,
);
@@ -85,7 +85,11 @@ export function RunDetails({
<TabsLineContent value="input">
<RunDetailCard>
<AgentInputsReadOnly agent={agent} inputs={(run as any)?.inputs} />
<AgentInputsReadOnly
agent={agent}
inputs={(run as any)?.inputs}
credentialInputs={(run as any)?.credential_inputs}
/>
</RunDetailCard>
</TabsLineContent>
</TabsLine>

View File

@@ -4,7 +4,7 @@ import { useGetV1GetExecutionDetails } from "@/app/api/__generated__/endpoints/g
import type { GetV1GetExecutionDetails200 } from "@/app/api/__generated__/models/getV1GetExecutionDetails200";
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
export function useRunDetails(graphId: string, runId: string) {
export function useSelectedRunView(graphId: string, runId: string) {
const query = useGetV1GetExecutionDetails(graphId, runId, {
query: {
refetchInterval: (q) => {

View File

@@ -10,7 +10,7 @@ import {
TabsLineList,
TabsLineTrigger,
} from "@/components/molecules/TabsLine/TabsLine";
import { useScheduleDetails } from "./useScheduleDetails";
import { useSelectedScheduleView } from "./useSelectedScheduleView";
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader";
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
@@ -20,18 +20,18 @@ import { Skeleton } from "@/components/ui/skeleton";
import { AgentInputsReadOnly } from "../AgentInputsReadOnly/AgentInputsReadOnly";
import { ScheduleActions } from "./components/ScheduleActions";
interface ScheduleDetailsProps {
interface Props {
agent: LibraryAgent;
scheduleId: string;
onClearSelectedRun?: () => void;
}
export function ScheduleDetails({
export function SelectedScheduleView({
agent,
scheduleId,
onClearSelectedRun,
}: ScheduleDetailsProps) {
const { schedule, isLoading, error } = useScheduleDetails(
}: Props) {
const { schedule, isLoading, error } = useSelectedScheduleView(
agent.graph_id,
scheduleId,
);
@@ -119,6 +119,7 @@ export function ScheduleDetails({
<AgentInputsReadOnly
agent={agent}
inputs={schedule?.input_data}
credentialInputs={schedule?.input_credentials}
/>
</div>
</RunDetailCard>

View File

@@ -7,7 +7,7 @@ import { getGetV1ListGraphExecutionsInfiniteQueryOptions } from "@/app/api/__gen
import {
parseCronToForm,
validateSchedule,
} from "../../../RunAgentModal/components/ModalScheduleSection/helpers";
} from "../../../ScheduleAgentModal/components/ModalScheduleSection/helpers";
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { useToast } from "@/components/molecules/Toast/use-toast";

View File

@@ -4,7 +4,7 @@ import { useMemo } from "react";
import { useGetV1ListExecutionSchedulesForAGraph } from "@/app/api/__generated__/endpoints/schedules/schedules";
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
export function useScheduleDetails(graphId: string, scheduleId: string) {
export function useSelectedScheduleView(graphId: string, scheduleId: string) {
const query = useGetV1ListExecutionSchedulesForAGraph(graphId, {
query: {
enabled: !!graphId,

View File

@@ -21,7 +21,7 @@ export const extendedButtonVariants = cva(
},
size: {
small: "px-3 py-2 text-sm gap-1.5 h-[2.25rem]",
large: "px-4 py-3 text-sm gap-2",
large: "px-4 py-3 text-sm gap-2 h-[3.25rem]",
icon: "p-3 min-w-0",
},
},

View File

@@ -65,7 +65,7 @@ export function Input({
const baseStyles = cn(
// Base styles
"rounded-3xl border border-zinc-200 bg-white px-4 shadow-none",
"rounded-3xl border border-zinc-200 bg-white px-4 shadow-none w-full",
"font-normal text-black",
"placeholder:font-normal placeholder:text-zinc-400",
// Focus and hover states
@@ -83,7 +83,7 @@ export function Input({
className={cn(
baseStyles,
errorStyles,
"-mb-1 h-auto min-h-[2.875rem] w-full",
"-mb-1 h-auto min-h-[2.875rem]",
// Size variants for textarea
size === "small" && [
"min-h-[2.25rem]", // 36px minimum
@@ -136,7 +136,7 @@ export function Input({
};
const input = (
<div className={cn("relative", wrapperClassName)}>
<div className={cn("relative w-full", wrapperClassName)}>
{renderInput()}
{isPasswordType && (
<button
@@ -154,7 +154,7 @@ export function Input({
);
const inputWithError = (
<div className={cn("relative mb-6", wrapperClassName)}>
<div className={cn("relative mb-6 w-full", wrapperClassName)}>
{input}
<Text
variant="small-medium"