mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
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:
@@ -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}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -144,6 +144,7 @@ export function useRunDetailHeader(
|
||||
canStop,
|
||||
isStopping,
|
||||
isDeleting,
|
||||
isRunning: run?.status === "RUNNING",
|
||||
isRunningAgain,
|
||||
handleShowDeleteDialog,
|
||||
handleDeleteRun,
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
@@ -73,9 +73,8 @@ export function ModalScheduleSection({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 w-fit">
|
||||
<TimezoneNotice />
|
||||
</div>
|
||||
|
||||
<TimezoneNotice />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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) => {
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user