Compare commits

...

1 Commits

Author SHA1 Message Date
Lluis Agusti
e34a055ed4 chore: fix modal wip 2025-08-30 00:39:57 +09:00
3 changed files with 259 additions and 202 deletions

View File

@@ -65,3 +65,125 @@ export function parseCronDescription(cron: string): string {
return cron; // Fallback to showing the raw cron return cron; // Fallback to showing the raw cron
} }
export function getMissingRequiredInputs(
inputSchema: any,
values: Record<string, any>,
): string[] {
if (!inputSchema || typeof inputSchema !== "object") return [];
const required: string[] = Array.isArray(inputSchema.required)
? inputSchema.required
: [];
const properties: Record<string, any> = inputSchema.properties || {};
return required.filter((key) => {
const field = properties[key];
if (field?.hidden) return false;
return isEmpty(values[key]);
});
}
export function getMissingCredentials(
credentialsProperties: Record<string, any> | undefined,
values: Record<string, any>,
): string[] {
const props = credentialsProperties || {};
return Object.keys(props).filter((key) => !(key in values));
}
type DeriveReadinessParams = {
inputSchema: any;
credentialsProperties?: Record<string, any>;
values: Record<string, any>;
credentialsValues: Record<string, any>;
};
export function deriveReadiness(params: DeriveReadinessParams): {
missingInputs: string[];
missingCredentials: string[];
credentialsRequired: boolean;
allRequiredInputsAreSet: boolean;
} {
const missingInputs = getMissingRequiredInputs(
params.inputSchema,
params.values,
);
const credentialsRequired =
Object.keys(params.credentialsProperties || {}).length > 0;
const missingCredentials = getMissingCredentials(
params.credentialsProperties,
params.credentialsValues,
);
const allRequiredInputsAreSet =
missingInputs.length === 0 &&
(!credentialsRequired || missingCredentials.length === 0);
return {
missingInputs,
missingCredentials,
credentialsRequired,
allRequiredInputsAreSet,
};
}
export function getVisibleInputFields(inputSchema: any): Record<string, any> {
if (
!inputSchema ||
typeof inputSchema !== "object" ||
!("properties" in inputSchema) ||
!inputSchema.properties
) {
return {} as Record<string, any>;
}
const properties = inputSchema.properties as Record<string, any>;
return Object.fromEntries(
Object.entries(properties).filter(([, subSchema]) => !subSchema?.hidden),
);
}
export function getCredentialFields(
credentialsInputSchema: any,
): Record<string, any> {
if (
!credentialsInputSchema ||
typeof credentialsInputSchema !== "object" ||
!("properties" in credentialsInputSchema) ||
!credentialsInputSchema.properties
) {
return {} as Record<string, any>;
}
return credentialsInputSchema.properties as Record<string, any>;
}
type CollectMissingFieldsOptions = {
needScheduleName?: boolean;
scheduleName: string;
missingInputs: string[];
credentialsRequired: boolean;
allCredentialsAreSet: boolean;
missingCredentials: string[];
};
export function collectMissingFields(
options: CollectMissingFieldsOptions,
): string[] {
const scheduleMissing =
options.needScheduleName && !options.scheduleName ? ["schedule_name"] : [];
const missingCreds =
options.credentialsRequired && !options.allCredentialsAreSet
? options.missingCredentials.map((k) => `credentials:${k}`)
: [];
return ([] as string[])
.concat(scheduleMissing)
.concat(options.missingInputs)
.concat(missingCreds);
}
export function getErrorMessage(error: unknown): string {
if (typeof error === "string") return error;
if (error && typeof error === "object" && "message" in error) {
const msg = (error as any).message;
if (typeof msg === "string" && msg.trim().length > 0) return msg;
}
return "An unexpected error occurred.";
}

View File

@@ -1,13 +1,19 @@
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { useState, useCallback, useMemo } from "react"; import { useState, useCallback, useMemo } from "react";
import { useToast } from "@/components/molecules/Toast/use-toast"; import { useToast } from "@/components/molecules/Toast/use-toast";
import { isEmpty } from "@/lib/utils";
import { usePostV1ExecuteGraphAgent } from "@/app/api/__generated__/endpoints/graphs/graphs"; import { usePostV1ExecuteGraphAgent } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { usePostV1CreateExecutionSchedule as useCreateSchedule } from "@/app/api/__generated__/endpoints/schedules/schedules"; import { usePostV1CreateExecutionSchedule as useCreateSchedule } from "@/app/api/__generated__/endpoints/schedules/schedules";
import { usePostV2SetupTrigger } from "@/app/api/__generated__/endpoints/presets/presets"; import { usePostV2SetupTrigger } from "@/app/api/__generated__/endpoints/presets/presets";
import { ExecuteGraphResponse } from "@/app/api/__generated__/models/executeGraphResponse"; import { ExecuteGraphResponse } from "@/app/api/__generated__/models/executeGraphResponse";
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo"; import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset"; import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import {
collectMissingFields,
getErrorMessage,
deriveReadiness,
getVisibleInputFields,
getCredentialFields,
} from "./helpers";
export type RunVariant = export type RunVariant =
| "manual" | "manual"
@@ -44,68 +50,9 @@ export function useAgentRunModal(
: "manual"; : "manual";
// API mutations // API mutations
const executeGraphMutation = usePostV1ExecuteGraphAgent({ const executeGraphMutation = usePostV1ExecuteGraphAgent();
mutation: { const createScheduleMutation = useCreateSchedule();
onSuccess: (response) => { const setupTriggerMutation = usePostV2SetupTrigger();
if (response.status === 200) {
toast({
title: "Agent execution started",
});
callbacks?.onRun?.(response.data);
setIsOpen(false);
}
},
onError: (error: any) => {
toast({
title: "❌ Failed to execute agent",
description: error.message || "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
const createScheduleMutation = useCreateSchedule({
mutation: {
onSuccess: (response) => {
if (response.status === 200) {
toast({
title: "Schedule created",
});
callbacks?.onCreateSchedule?.(response.data);
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) => {
if (response.status === 200) {
toast({
title: "Trigger setup complete",
});
callbacks?.onSetupTrigger?.(response.data);
setIsOpen(false);
}
},
onError: (error: any) => {
toast({
title: "❌ Failed to setup trigger",
description: error.message || "An unexpected error occurred.",
variant: "destructive",
});
},
},
});
// Input schema validation // Input schema validation
const agentInputSchema = useMemo( const agentInputSchema = useMemo(
@@ -113,84 +60,48 @@ export function useAgentRunModal(
[agent.input_schema], [agent.input_schema],
); );
const agentInputFields = useMemo(() => { const agentInputFields = useMemo(
if ( () => getVisibleInputFields(agentInputSchema),
!agentInputSchema || [agentInputSchema],
typeof agentInputSchema !== "object" ||
!("properties" in agentInputSchema) ||
!agentInputSchema.properties
) {
return {};
}
const properties = agentInputSchema.properties as Record<string, any>;
return Object.fromEntries(
Object.entries(properties).filter(
([_, subSchema]: [string, any]) => !subSchema.hidden,
),
);
}, [agentInputSchema]);
const agentCredentialsInputFields = useMemo(() => {
if (
!agent.credentials_input_schema ||
typeof agent.credentials_input_schema !== "object" ||
!("properties" in agent.credentials_input_schema) ||
!agent.credentials_input_schema.properties
) {
return {} as Record<string, any>;
}
return agent.credentials_input_schema.properties as Record<string, any>;
}, [agent.credentials_input_schema]);
// Validation logic
const [allRequiredInputsAreSetRaw, missingInputs] = useMemo(() => {
const nonEmptyInputs = new Set(
Object.keys(inputValues).filter((k) => !isEmpty(inputValues[k])),
);
const requiredInputs = new Set(
(agentInputSchema.required as string[]) || [],
);
const missing = [...requiredInputs].filter(
(input) => !nonEmptyInputs.has(input),
);
return [missing.length === 0, missing];
}, [agentInputSchema.required, inputValues]);
const [allCredentialsAreSet, missingCredentials] = useMemo(() => {
const availableCredentials = new Set(Object.keys(inputCredentials));
const allCredentials = new Set(
Object.keys(agentCredentialsInputFields || {}) ?? [],
);
const missing = [...allCredentials].filter(
(key) => !availableCredentials.has(key),
);
return [missing.length === 0, missing];
}, [agentCredentialsInputFields, inputCredentials]);
const credentialsRequired = useMemo(
() => Object.keys(agentCredentialsInputFields || {}).length > 0,
[agentCredentialsInputFields],
); );
// Final readiness flag combining inputs + credentials when credentials are shown const agentCredentialsInputFields = useMemo(
const allRequiredInputsAreSet = useMemo( () => getCredentialFields(agent.credentials_input_schema),
[agent.credentials_input_schema],
);
// Validation logic (presence checks derived from schemas)
const {
missingInputs,
missingCredentials,
credentialsRequired,
allRequiredInputsAreSet,
} = useMemo(
() => () =>
allRequiredInputsAreSetRaw && deriveReadiness({
(!credentialsRequired || allCredentialsAreSet), inputSchema: agentInputSchema,
[allRequiredInputsAreSetRaw, credentialsRequired, allCredentialsAreSet], credentialsProperties: agentCredentialsInputFields,
values: inputValues,
credentialsValues: inputCredentials,
}),
[
agentInputSchema,
agentCredentialsInputFields,
inputValues,
inputCredentials,
],
); );
const notifyMissingRequirements = useCallback( const notifyMissingRequirements = useCallback(
(needScheduleName: boolean = false) => { (needScheduleName: boolean = false) => {
const allMissingFields = ( const allMissingFields = collectMissingFields({
needScheduleName && !scheduleName ? ["schedule_name"] : [] needScheduleName,
) scheduleName,
.concat(missingInputs) missingInputs,
.concat( credentialsRequired,
credentialsRequired && !allCredentialsAreSet allCredentialsAreSet: missingCredentials.length === 0,
? missingCredentials.map((k) => `credentials:${k}`) missingCredentials,
: [], });
);
toast({ toast({
title: "⚠️ Missing required inputs", title: "⚠️ Missing required inputs",
@@ -203,21 +114,30 @@ export function useAgentRunModal(
scheduleName, scheduleName,
toast, toast,
credentialsRequired, credentialsRequired,
allCredentialsAreSet,
missingCredentials, missingCredentials,
], ],
); );
// Action handlers function showError(title: string, error: unknown) {
const handleRun = useCallback(() => { toast({
title,
description: getErrorMessage(error),
variant: "destructive",
});
}
async function handleRun() {
if (!allRequiredInputsAreSet) { if (!allRequiredInputsAreSet) {
notifyMissingRequirements(); notifyMissingRequirements();
return; return;
} }
if (defaultRunType === "automatic-trigger") { const shouldUseTrigger = defaultRunType === "automatic-trigger";
if (shouldUseTrigger) {
// Setup trigger // Setup trigger
if (!scheduleName.trim()) { const hasScheduleName = scheduleName.trim().length > 0;
if (!hasScheduleName) {
toast({ toast({
title: "⚠️ Trigger name required", title: "⚠️ Trigger name required",
description: "Please provide a name for your trigger.", description: "Please provide a name for your trigger.",
@@ -225,50 +145,63 @@ export function useAgentRunModal(
}); });
return; return;
} }
try {
setupTriggerMutation.mutate({ const nameToUse = presetName || scheduleName;
data: { const descriptionToUse =
name: presetName || scheduleName, presetDescription || `Trigger for ${agent.name}`;
description: presetDescription || `Trigger for ${agent.name}`, const response = await setupTriggerMutation.mutateAsync({
graph_id: agent.graph_id, data: {
graph_version: agent.graph_version, name: nameToUse,
trigger_config: inputValues, description: descriptionToUse,
agent_credentials: inputCredentials, graph_id: agent.graph_id,
}, graph_version: agent.graph_version,
}); trigger_config: inputValues,
agent_credentials: inputCredentials,
},
});
if (response.status === 200) {
toast({ title: "Trigger setup complete" });
callbacks?.onSetupTrigger?.(response.data);
setIsOpen(false);
} else {
throw new Error(JSON.stringify(response?.data?.detail));
}
} catch (error: any) {
showError("❌ Failed to setup trigger", error);
}
} else { } else {
// Manual execution // Manual execution
executeGraphMutation.mutate({ try {
graphId: agent.graph_id, const response = await executeGraphMutation.mutateAsync({
graphVersion: agent.graph_version, graphId: agent.graph_id,
data: { graphVersion: agent.graph_version,
inputs: inputValues, data: {
credentials_inputs: inputCredentials, inputs: inputValues,
}, credentials_inputs: inputCredentials,
}); },
} });
}, [
allRequiredInputsAreSet,
defaultRunType,
scheduleName,
inputValues,
inputCredentials,
agent,
presetName,
presetDescription,
notifyMissingRequirements,
setupTriggerMutation,
executeGraphMutation,
toast,
]);
const handleSchedule = useCallback(() => { if (response.status === 200) {
toast({ title: "Agent execution started" });
callbacks?.onRun?.(response.data);
setIsOpen(false);
} else {
throw new Error(JSON.stringify(response?.data?.detail));
}
} catch (error: any) {
showError("Failed to execute agent", error);
}
}
}
async function handleSchedule() {
if (!allRequiredInputsAreSet) { if (!allRequiredInputsAreSet) {
notifyMissingRequirements(true); notifyMissingRequirements(true);
return; return;
} }
if (!scheduleName.trim()) { const hasScheduleName = scheduleName.trim().length > 0;
if (!hasScheduleName) {
toast({ toast({
title: "⚠️ Schedule name required", title: "⚠️ Schedule name required",
description: "Please provide a name for your schedule.", description: "Please provide a name for your schedule.",
@@ -276,28 +209,27 @@ export function useAgentRunModal(
}); });
return; return;
} }
try {
createScheduleMutation.mutate({ const nameToUse = presetName || scheduleName;
graphId: agent.graph_id, const response = await createScheduleMutation.mutateAsync({
data: { graphId: agent.graph_id,
name: presetName || scheduleName, data: {
cron: cronExpression, name: nameToUse,
inputs: inputValues, cron: cronExpression,
graph_version: agent.graph_version, inputs: inputValues,
credentials: inputCredentials, graph_version: agent.graph_version,
}, credentials: inputCredentials,
}); },
}, [ });
allRequiredInputsAreSet, if (response.status === 200) {
scheduleName, toast({ title: "Schedule created" });
cronExpression, callbacks?.onCreateSchedule?.(response.data);
inputValues, setIsOpen(false);
inputCredentials, }
agent, } catch (error: any) {
notifyMissingRequirements, showError("❌ Failed to create schedule", error);
createScheduleMutation, }
toast, }
]);
function handleShowSchedule() { function handleShowSchedule() {
// Initialize with sensible defaults when entering schedule view // Initialize with sensible defaults when entering schedule view
@@ -342,10 +274,6 @@ export function useAgentRunModal(
cronExpression, cronExpression,
allRequiredInputsAreSet, allRequiredInputsAreSet,
missingInputs, missingInputs,
// Expose credential readiness for any UI hints if needed
// but enforcement is already applied in allRequiredInputsAreSet
// allCredentialsAreSet,
// missingCredentials,
agentInputFields, agentInputFields,
agentCredentialsInputFields, agentCredentialsInputFields,
hasInputFields, hasInputFields,

View File

@@ -11,9 +11,16 @@ const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
export function LaunchDarklyProvider({ children }: { children: ReactNode }) { export function LaunchDarklyProvider({ children }: { children: ReactNode }) {
const { user, isUserLoading } = useSupabase(); const { user, isUserLoading } = useSupabase();
const isCloud = getBehaveAs() === BehaveAs.CLOUD; const isCloud = true;
const isLaunchDarklyConfigured = isCloud && envEnabled && clientId; const isLaunchDarklyConfigured = isCloud && envEnabled && clientId;
console.log({
clientId,
envEnabled,
isCloud,
isLaunchDarklyConfigured,
});
const context = useMemo(() => { const context = useMemo(() => {
if (isUserLoading || !user) { if (isUserLoading || !user) {
return { return {