From 09aa94eee34fd150f13be27fd76b4b54efcf4338 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Mon, 13 Jul 2026 17:09:10 +0200 Subject: [PATCH 01/14] feat(agentic-workflow): add agentic workflow creation flow - Introduced new SVG icon for Jira integration. - Added routes for agentic workflow creation, configuration, and summary. - Implemented context and components for managing agentic workflow state and configuration. - Updated service list and new service components to include agentic workflow option based on feature flag. - Enhanced existing routing and navigation logic to accommodate new agentic workflow features. --- apps/console/public/assets/ai-tools/jira.svg | 1 + apps/console/src/routeTree.gen.ts | 113 ++ .../create/agentic-workflow/configuration.tsx | 15 + .../service/create/agentic-workflow/index.tsx | 19 + .../service/create/agentic-workflow/route.tsx | 44 + .../create/agentic-workflow/summary.tsx | 15 + .../organization/$organizationId/route.tsx | 1 + .../_authenticated/organization/route.tsx | 1 + libs/domains/services/feature/src/index.ts | 3 + .../agentic-workflow-configuration.tsx | 963 ++++++++++++++++++ .../agentic-workflow-context.tsx | 141 +++ .../agentic-workflow-summary.tsx | 183 ++++ .../src/lib/service-list/service-list.tsx | 35 +- .../src/lib/service-new/service-new.spec.tsx | 18 + .../src/lib/service-new/service-new.tsx | 23 +- 15 files changed, 1562 insertions(+), 13 deletions(-) create mode 100644 apps/console/public/assets/ai-tools/jira.svg create mode 100644 apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration.tsx create mode 100644 apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/index.tsx create mode 100644 apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/route.tsx create mode 100644 apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary.tsx create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.tsx create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx diff --git a/apps/console/public/assets/ai-tools/jira.svg b/apps/console/public/assets/ai-tools/jira.svg new file mode 100644 index 00000000000..35971c006a5 --- /dev/null +++ b/apps/console/public/assets/ai-tools/jira.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/console/src/routeTree.gen.ts b/apps/console/src/routeTree.gen.ts index 50660620478..f66a53ff5c0 100644 --- a/apps/console/src/routeTree.gen.ts +++ b/apps/console/src/routeTree.gen.ts @@ -118,6 +118,7 @@ import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnviron import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateHelmRouteRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm/route' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateDatabaseRouteRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database/route' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobRouteRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/route' +import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/route' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugRouteRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/route' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdVariablesRouteRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/variables/route' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdSettingsRouteRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/settings/route' @@ -127,6 +128,7 @@ import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnviron import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateHelmIndexRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm/index' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateDatabaseIndexRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database/index' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobIndexRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/index' +import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowIndexRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/index' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugIndexRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/index' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdVariablesIndexRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/variables/index' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdSettingsIndexRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/settings/index' @@ -155,6 +157,8 @@ import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnviron import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobResourcesRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/resources' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobGeneralRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/general' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobConfigureRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/configure' +import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowSummaryRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary' +import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowConfigurationRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugVariablesRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/variables' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugSummaryRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/summary' import { Route as AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugResourcesRouteImport } from './routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/resources' @@ -1047,6 +1051,14 @@ const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironm getParentRoute: () => AuthenticatedOrganizationOrganizationIdRouteRoute, } as any, ) +const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRoute = + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRouteImport.update( + { + id: '/project/$projectId/environment/$environmentId/service/create/agentic-workflow', + path: '/project/$projectId/environment/$environmentId/service/create/agentic-workflow', + getParentRoute: () => AuthenticatedOrganizationOrganizationIdRouteRoute, + } as any, + ) const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugRouteRoute = AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugRouteRouteImport.update( { @@ -1124,6 +1136,15 @@ const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironm AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobRouteRoute, } as any, ) +const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowIndexRoute = + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowIndexRouteImport.update( + { + id: '/', + path: '/', + getParentRoute: () => + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRoute, + } as any, + ) const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugIndexRoute = AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugIndexRouteImport.update( { @@ -1375,6 +1396,24 @@ const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironm AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobRouteRoute, } as any, ) +const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowSummaryRoute = + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowSummaryRouteImport.update( + { + id: '/summary', + path: '/summary', + getParentRoute: () => + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRoute, + } as any, + ) +const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowConfigurationRoute = + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowConfigurationRouteImport.update( + { + id: '/configuration', + path: '/configuration', + getParentRoute: () => + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRoute, + } as any, + ) const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugVariablesRoute = AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugVariablesRouteImport.update( { @@ -1824,6 +1863,7 @@ export interface FileRoutesByFullPath { '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/settings': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdSettingsRouteRouteWithChildren '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/variables': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdVariablesRouteRouteWithChildren '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugRouteRouteWithChildren + '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRouteWithChildren '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobRouteRouteWithChildren '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateDatabaseRouteRouteWithChildren '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateHelmRouteRouteWithChildren @@ -1865,6 +1905,8 @@ export interface FileRoutesByFullPath { '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/resources': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugResourcesRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/summary': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugSummaryRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/variables': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugVariablesRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowConfigurationRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowSummaryRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/configure': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobConfigureRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/general': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobGeneralRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/resources': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobResourcesRoute @@ -1893,6 +1935,7 @@ export interface FileRoutesByFullPath { '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/settings/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdSettingsIndexRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/variables/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdVariablesIndexRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugIndexRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowIndexRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobIndexRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateDatabaseIndexRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateHelmIndexRoute @@ -2031,6 +2074,8 @@ export interface FileRoutesByTo { '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/resources': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugResourcesRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/summary': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugSummaryRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/variables': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugVariablesRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowConfigurationRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowSummaryRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/configure': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobConfigureRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/general': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobGeneralRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/resources': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobResourcesRoute @@ -2059,6 +2104,7 @@ export interface FileRoutesByTo { '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/settings': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdSettingsIndexRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/variables': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdVariablesIndexRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugIndexRoute + '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowIndexRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobIndexRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateDatabaseIndexRoute '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateHelmIndexRoute @@ -2177,6 +2223,7 @@ export interface FileRoutesById { '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/settings': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdSettingsRouteRouteWithChildren '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/variables': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdVariablesRouteRouteWithChildren '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugRouteRouteWithChildren + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRouteWithChildren '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobRouteRouteWithChildren '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateDatabaseRouteRouteWithChildren '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateHelmRouteRouteWithChildren @@ -2218,6 +2265,8 @@ export interface FileRoutesById { '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/resources': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugResourcesRoute '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/summary': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugSummaryRoute '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/variables': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugVariablesRoute + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowConfigurationRoute + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowSummaryRoute '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/configure': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobConfigureRoute '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/general': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobGeneralRoute '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/resources': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobResourcesRoute @@ -2246,6 +2295,7 @@ export interface FileRoutesById { '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/settings/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdSettingsIndexRoute '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/variables/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdVariablesIndexRoute '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugIndexRoute + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowIndexRoute '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobIndexRoute '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateDatabaseIndexRoute '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm/': typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateHelmIndexRoute @@ -2365,6 +2415,7 @@ export interface FileRouteTypes { | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/settings' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/variables' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug' + | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm' @@ -2406,6 +2457,8 @@ export interface FileRouteTypes { | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/resources' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/summary' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/variables' + | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration' + | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/configure' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/general' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/resources' @@ -2434,6 +2487,7 @@ export interface FileRouteTypes { | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/settings/' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/variables/' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/' + | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database/' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm/' @@ -2572,6 +2626,8 @@ export interface FileRouteTypes { | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/resources' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/summary' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/variables' + | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration' + | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/configure' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/general' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/resources' @@ -2600,6 +2656,7 @@ export interface FileRouteTypes { | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/settings' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/variables' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug' + | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database' | '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm' @@ -2717,6 +2774,7 @@ export interface FileRouteTypes { | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/settings' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/variables' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug' + | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm' @@ -2758,6 +2816,8 @@ export interface FileRouteTypes { | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/resources' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/summary' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/variables' + | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration' + | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/configure' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/general' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/resources' @@ -2786,6 +2846,7 @@ export interface FileRouteTypes { | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/settings/' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/variables/' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/' + | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job/' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database/' | '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm/' @@ -3576,6 +3637,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobRouteRouteImport parentRoute: typeof AuthenticatedOrganizationOrganizationIdRouteRoute } + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow': { + id: '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow' + path: '/project/$projectId/environment/$environmentId/service/create/agentic-workflow' + fullPath: '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow' + preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRouteImport + parentRoute: typeof AuthenticatedOrganizationOrganizationIdRouteRoute + } '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug': { id: '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug' path: '/project/$projectId/environment/$environmentId/service/create/$slug' @@ -3639,6 +3707,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobIndexRouteImport parentRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobRouteRoute } + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/': { + id: '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/' + path: '/' + fullPath: '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/' + preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowIndexRouteImport + parentRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRoute + } '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/': { id: '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/' path: '/' @@ -3835,6 +3910,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobConfigureRouteImport parentRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobRouteRoute } + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary': { + id: '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary' + path: '/summary' + fullPath: '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary' + preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowSummaryRouteImport + parentRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRoute + } + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration': { + id: '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration' + path: '/configuration' + fullPath: '/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration' + preLoaderRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowConfigurationRouteImport + parentRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRoute + } '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/variables': { id: '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug/variables' path: '/variables' @@ -4531,6 +4620,27 @@ const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironm AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugRouteRouteChildren, ) +interface AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRouteChildren { + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowConfigurationRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowConfigurationRoute + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowSummaryRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowSummaryRoute + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowIndexRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowIndexRoute +} + +const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRouteChildren: AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRouteChildren = + { + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowConfigurationRoute: + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowConfigurationRoute, + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowSummaryRoute: + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowSummaryRoute, + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowIndexRoute: + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowIndexRoute, + } + +const AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRouteWithChildren = + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRoute._addFileChildren( + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRouteChildren, + ) + interface AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobRouteRouteChildren { AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobConfigureRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobConfigureRoute AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobGeneralRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobGeneralRoute @@ -4752,6 +4862,7 @@ interface AuthenticatedOrganizationOrganizationIdRouteRouteChildren { AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdSettingsRouteRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdSettingsRouteRouteWithChildren AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdVariablesRouteRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdVariablesRouteRouteWithChildren AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugRouteRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugRouteRouteWithChildren + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRouteWithChildren AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobRouteRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobRouteRouteWithChildren AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateDatabaseRouteRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateDatabaseRouteRouteWithChildren AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateHelmRouteRoute: typeof AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateHelmRouteRouteWithChildren @@ -4835,6 +4946,8 @@ const AuthenticatedOrganizationOrganizationIdRouteRouteChildren: AuthenticatedOr AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceServiceIdVariablesRouteRouteWithChildren, AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugRouteRoute: AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateSlugRouteRouteWithChildren, + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRoute: + AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateAgenticWorkflowRouteRouteWithChildren, AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobRouteRoute: AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateCronJobRouteRouteWithChildren, AuthenticatedOrganizationOrganizationIdProjectProjectIdEnvironmentEnvironmentIdServiceCreateDatabaseRouteRoute: diff --git a/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration.tsx b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration.tsx new file mode 100644 index 00000000000..836922c909b --- /dev/null +++ b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/react-router' +import { AgenticWorkflowConfiguration } from '@qovery/domains/services/feature' +import { useDocumentTitle } from '@qovery/shared/util-hooks' + +export const Route = createFileRoute( + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration' +)({ + component: RouteComponent, +}) + +function RouteComponent() { + useDocumentTitle('Agentic workflow configuration') + + return +} diff --git a/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/index.tsx b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/index.tsx new file mode 100644 index 00000000000..ab9b892b66c --- /dev/null +++ b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/index.tsx @@ -0,0 +1,19 @@ +import { Navigate, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute( + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/' +)({ + component: RouteComponent, +}) + +function RouteComponent() { + const { organizationId, projectId, environmentId } = Route.useParams() + + return ( + + ) +} diff --git a/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/route.tsx b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/route.tsx new file mode 100644 index 00000000000..177b183eda4 --- /dev/null +++ b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/route.tsx @@ -0,0 +1,44 @@ +import { Outlet, createFileRoute, useNavigate } from '@tanstack/react-router' +import { useFeatureFlagEnabled } from 'posthog-js/react' +import { useEffect } from 'react' +import { AgenticWorkflowCreationFlow } from '@qovery/domains/services/feature' +import { serviceCreateParamsSchema } from '@qovery/shared/router' + +export const Route = createFileRoute( + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow' +)({ + component: RouteComponent, + validateSearch: serviceCreateParamsSchema, +}) + +function RouteComponent() { + const { organizationId, projectId, environmentId } = Route.useParams() + const navigate = useNavigate() + const isAgenticWorkflowEnabled = Boolean(useFeatureFlagEnabled('argentic-workflow')) + const creationFlowUrl = `/organization/${organizationId}/project/${projectId}/environment/${environmentId}/service/create/agentic-workflow` + + useEffect(() => { + if (!isAgenticWorkflowEnabled) { + navigate({ + to: '/organization/$organizationId/project/$projectId/environment/$environmentId/service/new', + params: { organizationId, projectId, environmentId }, + }) + } + }, [environmentId, isAgenticWorkflowEnabled, navigate, organizationId, projectId]) + + if (!isAgenticWorkflowEnabled) return null + + return ( + + navigate({ + to: '/organization/$organizationId/project/$projectId/environment/$environmentId/service/new', + params: { organizationId, projectId, environmentId }, + }) + } + > + + + ) +} diff --git a/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary.tsx b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary.tsx new file mode 100644 index 00000000000..6bb8d520991 --- /dev/null +++ b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/react-router' +import { AgenticWorkflowSummary } from '@qovery/domains/services/feature' +import { useDocumentTitle } from '@qovery/shared/util-hooks' + +export const Route = createFileRoute( + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary' +)({ + component: RouteComponent, +}) + +function RouteComponent() { + useDocumentTitle('Summary - Create agentic workflow') + + return +} diff --git a/apps/console/src/routes/_authenticated/organization/$organizationId/route.tsx b/apps/console/src/routes/_authenticated/organization/$organizationId/route.tsx index 79cc12afcf5..a0b8eff01e9 100644 --- a/apps/console/src/routes/_authenticated/organization/$organizationId/route.tsx +++ b/apps/console/src/routes/_authenticated/organization/$organizationId/route.tsx @@ -51,6 +51,7 @@ const hiddenProgressCardRouteIds: FileRouteTypes['id'][] = [ '/_authenticated/organization/$organizationId/cluster/create/$slug', '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug', '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily', + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow', '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job', '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database', '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm', diff --git a/apps/console/src/routes/_authenticated/organization/route.tsx b/apps/console/src/routes/_authenticated/organization/route.tsx index dbb2c1271a2..28108223689 100644 --- a/apps/console/src/routes/_authenticated/organization/route.tsx +++ b/apps/console/src/routes/_authenticated/organization/route.tsx @@ -489,6 +489,7 @@ const bypassLayoutRouteIds: FileRouteTypes['id'][] = [ '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug', '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database', '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily', + '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow', '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm', '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/terraform', '/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/lifecycle-job', diff --git a/libs/domains/services/feature/src/index.ts b/libs/domains/services/feature/src/index.ts index c461f4b9f7e..b8f590dbedf 100644 --- a/libs/domains/services/feature/src/index.ts +++ b/libs/domains/services/feature/src/index.ts @@ -106,6 +106,9 @@ export * from './lib/service-creation-flow/database/database-summary-view/databa export * from './lib/service-creation-flow/database/step-summary/step-summary' export * from './lib/service-creation-flow/blueprint/blueprint-creation-flow' export * from './lib/service-update-flow/blueprint/blueprint-update-flow' +export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-context' +export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration' +export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-summary' export * from './lib/application-container-healthchecks/application-container-healthchecks-form/application-container-healthchecks-form' export * from './lib/application-container-healthchecks/healthchecks-utils' export * from './lib/application-container-healthchecks/step-healthchecks/step-healthchecks' diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx new file mode 100644 index 00000000000..d5d944846e5 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx @@ -0,0 +1,963 @@ +import { type IconName } from '@fortawesome/fontawesome-common-types' +import { useNavigate, useParams } from '@tanstack/react-router' +import { type GitProviderEnum, type GitRepository } from 'qovery-typescript-axios' +import { type ReactNode, useEffect } from 'react' +import { FormProvider, useForm } from 'react-hook-form' +import { GitBranchSettings, GitProviderSetting, GitRepositorySetting } from '@qovery/domains/organizations/feature' +import { IconEnum } from '@qovery/shared/enums' +import { Button, CodeEditor, FunnelFlowBody, Icon, InputText, InputTextArea, InputToggle } from '@qovery/shared/ui' +import { + type AgenticWorkflowConfigurationSection, + type AgenticWorkflowConnector, + type AgenticWorkflowGitRepository, + type AgenticWorkflowOutput, + DEFAULT_CONNECTOR_JSON, + useAgenticWorkflowCreateContext, +} from './agentic-workflow-context' + +const outputTypes: { iconName: IconName; label: AgenticWorkflowOutput['type'] }[] = [ + { label: 'Slack', iconName: 'slack' }, + { label: 'Jira', iconName: 'jira' }, + { label: 'GitHub', iconName: 'github' }, + { label: 'Other', iconName: 'ellipsis' }, +] + +const connectorTypes: { iconName: IconName; label: AgenticWorkflowConnector['type'] }[] = outputTypes + +const defaultMcpUrls: Record, string> = { + Slack: 'https://mcp.slack.com/mcp', + Jira: 'https://mcp.atlassian.com/v1/sse', + GitHub: 'https://api.githubcopilot.com/mcp/', +} + +function getDefaultIntegrationName(type: AgenticWorkflowConnector['type'] | AgenticWorkflowOutput['type']) { + return type +} + +function getNextIntegrationName( + currentName: string, + previousType: AgenticWorkflowConnector['type'] | AgenticWorkflowOutput['type'], + nextType: AgenticWorkflowConnector['type'] | AgenticWorkflowOutput['type'] +) { + const trimmedName = currentName.trim() + + if (!trimmedName || trimmedName === getDefaultIntegrationName(previousType)) { + return getDefaultIntegrationName(nextType) + } + + return currentName +} + +const sectionOrder: AgenticWorkflowConfigurationSection[] = [ + 'service-information', + 'ai-model', + 'connectors', + 'git-repositories', + 'governance', + 'docker-fragment', + 'outputs', + 'agent-prompt', +] + +function getJsonError(value: string, required = false) { + if (!value.trim()) return required ? 'Please enter a valid JSON configuration.' : undefined + + try { + JSON.parse(value) + return undefined + } catch { + return 'Invalid JSON format.' + } +} + +function isSectionCompleted( + section: AgenticWorkflowConfigurationSection, + activeSection: AgenticWorkflowConfigurationSection +) { + return sectionOrder.indexOf(section) < sectionOrder.indexOf(activeSection) +} + +function getSectionTitle(section: AgenticWorkflowConfigurationSection) { + const titles: Record = { + 'service-information': 'Service information', + 'ai-model': 'AI model', + connectors: 'Connectors', + 'git-repositories': 'Git repositories', + governance: 'Governance', + 'docker-fragment': 'Docker fragment', + outputs: 'Outputs', + 'agent-prompt': 'Agent prompt', + } + + return titles[section] +} + +function isGitRepositoryComplete(repository: AgenticWorkflowGitRepository) { + return Boolean(repository.gitTokenId && repository.repository.trim() && repository.branch.trim()) +} + +function isOutputComplete(output: AgenticWorkflowOutput) { + return output.type === 'Other' + ? Boolean(output.prompt.trim()) + : Boolean(output.url.trim() && output.authentication.trim()) +} + +function isConnectorComplete(connector: AgenticWorkflowConnector) { + return Boolean(connector.url.trim()) +} + +function AgenticWorkflowSection({ + children, + headerAction, + iconName, + invalid, + section, +}: { + children: ReactNode + headerAction?: ReactNode + iconName: IconName + invalid?: boolean + section: AgenticWorkflowConfigurationSection +}) { + const { activeSection, setActiveSection } = useAgenticWorkflowCreateContext() + const active = activeSection === section + const completed = isSectionCompleted(section, activeSection) + const showStatus = completed && !active + const title = getSectionTitle(section) + const headerContent = ( + <> +
+ +

+ {title} +

+
+
+ {showStatus && + (invalid ? ( + + ) : ( + + ))} +
+ + ) + + return ( +
+ {active ? ( +
+ {headerContent} + {headerAction &&
{headerAction}
} +
+ ) : ( + + )} + {active &&
{children}
} +
+ ) +} + +function CodeEditorField({ + error, + height = '180px', + hint, + label, + language, + name, + onChange, + placeholder, + value, +}: { + error?: string + height?: string + hint?: ReactNode + label: string + language: string + name: string + onChange: (value: string) => void + placeholder?: string + value: string +}) { + return ( +
+ +
+ {placeholder && !value.trim() && ( +
+ {placeholder} +
+ )} + onChange(nextValue ?? '')} + options={{ + scrollbar: { alwaysConsumeMouseWheel: false }, + scrollBeyondLastLine: false, + wordWrap: 'on', + }} + /> +
+ {hint && !error &&

{hint}

} + {error &&

{error}

} +
+ ) +} + +function AIModelCards() { + const { form } = useAgenticWorkflowCreateContext() + const selectedModel = form.watch('aiModel') + + return ( +
+ + +
+ ) +} + +function IntegrationTypeIcon({ + iconName, + label, + selected, +}: { + iconName: IconName + label: AgenticWorkflowConnector['type'] | AgenticWorkflowOutput['type'] + selected: boolean +}) { + if (label === 'Slack') { + return + } + + if (label === 'GitHub') { + return + } + + if (label === 'Jira') { + return + } + + return +} + +function ConnectorTypeCards({ + onChange, + value, +}: { + onChange: (value: AgenticWorkflowConnector['type']) => void + value: AgenticWorkflowConnector['type'] +}) { + return ( +
+ {connectorTypes.map((connectorType) => { + const selected = value === connectorType.label + + return ( + + ) + })} +
+ ) +} + +function OutputTypeCards({ + onChange, + value, +}: { + onChange: (value: AgenticWorkflowOutput['type']) => void + value: AgenticWorkflowOutput['type'] +}) { + return ( +
+ {outputTypes.map((outputType) => { + const selected = value === outputType.label + + return ( + + ) + })} +
+ ) +} + +function GitRepositoryCard({ + index, + onChange, + onRemove, + repository, +}: { + index: number + onChange: (repository: AgenticWorkflowGitRepository) => void + onRemove: () => void + repository: AgenticWorkflowGitRepository +}) { + const { organizationId = '' } = useParams({ strict: false }) + const methods = useForm<{ + provider?: keyof typeof GitProviderEnum | string | null + is_public_repository?: boolean + repository: string + branch: string + git_token_name?: string | null + git_token_id?: string | null + git_repository?: GitRepository + }>({ + defaultValues: { + provider: repository.provider, + is_public_repository: repository.isPublicRepository, + repository: repository.repository, + branch: repository.branch, + git_token_name: repository.gitTokenName, + git_token_id: repository.gitTokenId, + git_repository: repository.gitRepository, + }, + mode: 'onChange', + }) + const provider = methods.watch('provider') as keyof typeof GitProviderEnum | undefined + const watchedRepository = methods.watch('repository') + const gitTokenId = methods.watch('git_token_id') ?? undefined + const isPublicRepository = methods.watch('is_public_repository') + + useEffect(() => { + const subscription = methods.watch((values) => { + onChange({ + provider: values.provider, + gitTokenId: values.git_token_id, + gitTokenName: values.git_token_name, + isPublicRepository: values.is_public_repository, + repository: values.repository ?? '', + gitRepository: values.git_repository as GitRepository | undefined, + branch: values.branch ?? '', + }) + }) + + return () => subscription.unsubscribe() + }, [methods, onChange]) + + return ( +
+
+ Repository {index + 1} + +
+ +
+ + {isPublicRepository ? null : ( + <> + {provider && ( + + )} + {provider && watchedRepository && ( + + )} + + )} +
+
+
+ ) +} + +export function AgenticWorkflowConfiguration() { + const navigate = useNavigate() + const { activeSection, creationFlowUrl, form, setActiveSection, setCurrentStep } = useAgenticWorkflowCreateContext() + const values = form.watch() + const { dirtyFields } = form.formState + const connectorErrors = values.connectors.map((connector) => getJsonError(connector.mcpServersJson, true)) + const connectorHeadersErrors = values.connectors.map((connector) => getJsonError(connector.headersJson)) + const connectorsValid = values.connectors.every(isConnectorComplete) + const modelSettingsJsonError = getJsonError(values.modelSettingsJson, true) + const claudeConfigJsonError = getJsonError(values.claudeConfigJson, true) + const gitRepositoriesValid = values.gitRepositories.every(isGitRepositoryComplete) + const outputsValid = values.outputs.every(isOutputComplete) + const showNameError = Boolean(dirtyFields.name) && !values.name.trim() + const showModelApiKeyError = Boolean(dirtyFields.modelApiKey) && !values.modelApiKey.trim() + const sectionInvalid: Record = { + 'service-information': !values.name.trim(), + 'ai-model': !values.modelApiKey.trim() || Boolean(modelSettingsJsonError), + connectors: !connectorsValid || connectorErrors.some(Boolean) || connectorHeadersErrors.some(Boolean), + 'git-repositories': !gitRepositoriesValid, + governance: Boolean(claudeConfigJsonError), + 'docker-fragment': false, + outputs: !outputsValid, + 'agent-prompt': !values.agentPrompt.trim(), + } + const isValid = + Boolean(values.name.trim()) && + Boolean(values.modelApiKey.trim()) && + Boolean(values.agentPrompt.trim()) && + gitRepositoriesValid && + outputsValid && + connectorsValid && + connectorErrors.every((error) => !error) && + connectorHeadersErrors.every((error) => !error) && + !modelSettingsJsonError && + !claudeConfigJsonError + + useEffect(() => { + setCurrentStep(1) + }, [setCurrentStep]) + + const goToNextSection = () => { + const nextSection = sectionOrder[sectionOrder.indexOf(activeSection) + 1] + + if (nextSection) { + setActiveSection(nextSection) + return + } + + navigate({ to: `${creationFlowUrl}/summary` }) + } + + const addConnector = () => + form.setValue( + 'connectors', + [ + ...values.connectors, + { + type: 'Slack', + name: getDefaultIntegrationName('Slack'), + url: defaultMcpUrls.Slack, + mcpServersJson: DEFAULT_CONNECTOR_JSON, + headersJson: '{}', + }, + ], + { + shouldDirty: true, + } + ) + + const addRepository = () => + form.setValue( + 'gitRepositories', + [ + ...values.gitRepositories, + { + provider: undefined, + gitTokenId: undefined, + gitTokenName: undefined, + isPublicRepository: false, + repository: '', + gitRepository: undefined, + branch: '', + }, + ], + { shouldDirty: true } + ) + + const addOutput = () => + form.setValue( + 'outputs', + [ + ...values.outputs, + { + type: 'Slack', + name: getDefaultIntegrationName('Slack'), + url: defaultMcpUrls.Slack, + authentication: '', + prompt: '', + }, + ], + { + shouldDirty: true, + } + ) + + return ( + +
+

Create agentic workflow

+

+ Configure the inputs, tools, model, governance, and outputs used by your workflow. +

+
+ +
+ + form.setValue('name', event.currentTarget.value, { shouldDirty: true })} + /> + form.setValue('description', event.currentTarget.value, { shouldDirty: true })} + /> + form.setValue('workflowEnabled', value, { shouldDirty: true })} + /> + + + + + + form.setValue('modelApiKey', event.currentTarget.value, { shouldDirty: true })} + /> + form.setValue('modelSettingsJson', value, { shouldDirty: true })} + /> + + + + + + Add connector + + } + > + {values.connectors.map((connector, index) => ( +
+
+ Connector {index + 1} + +
+
+ { + const connectors = [...values.connectors] + connectors[index] = { + ...connector, + type, + name: getNextIntegrationName(connector.name, connector.type, type), + url: type === 'Other' ? '' : defaultMcpUrls[type], + } + form.setValue('connectors', connectors, { shouldDirty: true }) + }} + /> + { + const connectors = [...values.connectors] + connectors[index] = { ...connector, name: event.currentTarget.value } + form.setValue('connectors', connectors, { shouldDirty: true }) + }} + /> + { + const connectors = [...values.connectors] + connectors[index] = { ...connector, url: event.currentTarget.value } + form.setValue('connectors', connectors, { shouldDirty: true }) + }} + /> + { + const connectors = [...values.connectors] + connectors[index] = { ...connector, headersJson: value } + form.setValue('connectors', connectors, { shouldDirty: true }) + }} + /> + { + const connectors = [...values.connectors] + connectors[index] = { ...connector, mcpServersJson: value } + form.setValue('connectors', connectors, { shouldDirty: true }) + }} + /> +
+
+ ))} + {!connectorsValid && ( +

Enter an MCP URL for each configured connector.

+ )} + +
+ + + + Add repository + + } + > + {values.gitRepositories.map((repository, index) => ( + { + const gitRepositories = [...values.gitRepositories] + gitRepositories[index] = nextRepository + form.setValue('gitRepositories', gitRepositories, { shouldDirty: true }) + }} + onRemove={() => + form.setValue( + 'gitRepositories', + values.gitRepositories.filter((_, repositoryIndex) => repositoryIndex !== index), + { shouldDirty: true } + ) + } + /> + ))} + {!gitRepositoriesValid && ( +

+ Select a Git account, repository, and branch for each repository. +

+ )} + +
+ + + form.setValue('whitelistHosts', event.currentTarget.value, { shouldDirty: true })} + /> + + Need help with permissions and allowed routes? Use{' '} + + Claude settings generator + + . + + } + onChange={(value) => form.setValue('claudeConfigJson', value, { shouldDirty: true })} + /> + + + + + form.setValue('dockerFragment', value, { shouldDirty: true })} + /> + + + + + + Add output + + } + > + {values.outputs.map((output, index) => ( +
+
+ Output {index + 1} + +
+
+ { + const outputs = [...values.outputs] + outputs[index] = { + ...output, + type, + name: getNextIntegrationName(output.name, output.type, type), + url: type === 'Other' ? '' : defaultMcpUrls[type], + } + form.setValue('outputs', outputs, { shouldDirty: true }) + }} + /> + { + const outputs = [...values.outputs] + outputs[index] = { ...output, name: event.currentTarget.value } + form.setValue('outputs', outputs, { shouldDirty: true }) + }} + /> + {output.type !== 'Other' && ( + <> + { + const outputs = [...values.outputs] + outputs[index] = { ...output, url: event.currentTarget.value } + form.setValue('outputs', outputs, { shouldDirty: true }) + }} + /> + { + const outputs = [...values.outputs] + outputs[index] = { ...output, authentication: event.currentTarget.value } + form.setValue('outputs', outputs, { shouldDirty: true }) + }} + /> + + )} + { + const outputs = [...values.outputs] + outputs[index] = { ...output, prompt: event.currentTarget.value } + form.setValue('outputs', outputs, { shouldDirty: true }) + }} + /> +
+
+ ))} + {!outputsValid && ( +

+ Enter a URL and authentication value for each configured output. For Other, describe the expected output. +

+ )} + +
+ + + form.setValue('agentPrompt', event.currentTarget.value, { shouldDirty: true })} + /> + + +
+ +
+
+ +
+
+
+ ) +} + +function ContinueButton({ disabled, onClick }: { disabled?: boolean; onClick: () => void }) { + return ( + + ) +} diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.tsx new file mode 100644 index 00000000000..86dcf1bd1e9 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.tsx @@ -0,0 +1,141 @@ +import { type GitProviderEnum, type GitRepository } from 'qovery-typescript-axios' +import { type PropsWithChildren, createContext, useContext, useState } from 'react' +import { FormProvider, type UseFormReturn, useForm } from 'react-hook-form' +import { FunnelFlow } from '@qovery/shared/ui' + +const DEFAULT_MODEL_SETTINGS = `{ + "provider": "anthropic", + "model": "claude-sonnet-4", + "temperature": 0.2 +}` + +const DEFAULT_CLAUDE_CONFIG = `{ + "tools": [], + "commands": [] +}` + +export const DEFAULT_CONNECTOR_JSON = `{ + "mcpServers": {} +}` + +export const agenticWorkflowCreationSteps: { title: string }[] = [{ title: 'Configuration' }, { title: 'Summary' }] + +export type AgenticWorkflowConfigurationSection = + | 'service-information' + | 'ai-model' + | 'connectors' + | 'git-repositories' + | 'governance' + | 'docker-fragment' + | 'outputs' + | 'agent-prompt' + +export interface AgenticWorkflowOutput { + type: 'Slack' | 'Jira' | 'GitHub' | 'Other' + name: string + url: string + authentication: string + prompt: string +} + +export interface AgenticWorkflowGitRepository { + provider?: keyof typeof GitProviderEnum | string | null + gitTokenId?: string | null + gitTokenName?: string | null + isPublicRepository?: boolean + repository: string + gitRepository?: GitRepository + branch: string +} + +export interface AgenticWorkflowConnector { + type: 'Slack' | 'Jira' | 'GitHub' | 'Other' + name: string + url: string + mcpServersJson: string + headersJson: string +} + +export interface AgenticWorkflowFormData { + name: string + description: string + workflowEnabled: boolean + aiModel: 'Claude' | 'Codex' + webhookEnabled: boolean + connectors: AgenticWorkflowConnector[] + gitRepositories: AgenticWorkflowGitRepository[] + modelApiKey: string + modelSettingsJson: string + whitelistHosts: string + claudeConfigJson: string + dockerFragment: string + outputs: AgenticWorkflowOutput[] + agentPrompt: string +} + +export interface AgenticWorkflowCreateContextInterface { + activeSection: AgenticWorkflowConfigurationSection + creationFlowUrl: string + currentStep: number + form: UseFormReturn + setActiveSection: (section: AgenticWorkflowConfigurationSection) => void + setCurrentStep: (step: number) => void +} + +const AgenticWorkflowCreateContext = createContext(undefined) + +export function useAgenticWorkflowCreateContext() { + const context = useContext(AgenticWorkflowCreateContext) + + if (!context) { + throw new Error('useAgenticWorkflowCreateContext must be used within AgenticWorkflowCreationFlow') + } + + return context +} + +export interface AgenticWorkflowCreationFlowProps extends PropsWithChildren { + creationFlowUrl: string + onExit: () => void +} + +export function AgenticWorkflowCreationFlow({ children, creationFlowUrl, onExit }: AgenticWorkflowCreationFlowProps) { + const [currentStep, setCurrentStep] = useState(1) + const [activeSection, setActiveSection] = useState('service-information') + const form = useForm({ + defaultValues: { + name: '', + description: '', + workflowEnabled: true, + aiModel: 'Claude', + webhookEnabled: true, + connectors: [], + gitRepositories: [], + modelApiKey: '', + modelSettingsJson: DEFAULT_MODEL_SETTINGS, + whitelistHosts: '', + claudeConfigJson: DEFAULT_CLAUDE_CONFIG, + dockerFragment: '', + outputs: [], + agentPrompt: '', + }, + mode: 'onChange', + }) + + return ( + + + + {children} + + + + ) +} diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx new file mode 100644 index 00000000000..84b5317f92f --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx @@ -0,0 +1,183 @@ +import { useNavigate, useParams } from '@tanstack/react-router' +import posthog from 'posthog-js' +import { type ReactNode, useEffect, useState } from 'react' +import { Button, FunnelFlowBody, Heading, Icon, Section, SummaryValue, toast } from '@qovery/shared/ui' +import { + type AgenticWorkflowConfigurationSection, + type AgenticWorkflowFormData, + useAgenticWorkflowCreateContext, +} from './agentic-workflow-context' + +function generateToken(prefix: string) { + const randomPart = Math.random().toString(36).slice(2, 10) + const timePart = Date.now().toString(36).slice(-6) + + return `${prefix}_${timePart}${randomPart}` +} + +function generateWebhookDetails() { + const id = generateToken('awf') + + return { + webhookUrl: `https://hooks.qovery.local/agentic-workflows/${id}`, + webhookSecret: generateToken('qvy_whsec'), + } +} + +function truncateSummary(value: string) { + if (!value.trim()) return '-' + + return value.length > 160 ? `${value.slice(0, 157)}...` : value +} + +function formatCount(count: number, singular: string) { + if (count === 0) return 'None' + + return `${count} ${singular}${count > 1 ? 's' : ''}` +} + +function maskValue(value: string) { + return value.trim() ? '••••••••••••' : '-' +} + +function hasIncompleteGitRepository(values: AgenticWorkflowFormData) { + return values.gitRepositories.some( + (repository) => !repository.gitTokenId || !repository.repository.trim() || !repository.branch.trim() + ) +} + +function hasIncompleteOutput(values: AgenticWorkflowFormData) { + return values.outputs.some((output) => + output.type === 'Other' ? !output.prompt.trim() : !output.url.trim() || !output.authentication.trim() + ) +} + +function SummarySection({ children, onEdit, title }: { children: ReactNode; onEdit: () => void; title: string }) { + return ( +
+
+ {title} + +
+
    {children}
+
+ ) +} + +export function AgenticWorkflowSummary() { + const navigate = useNavigate() + const { environmentId = '', organizationId = '', projectId = '' } = useParams({ strict: false }) + const { creationFlowUrl, form, setActiveSection, setCurrentStep } = useAgenticWorkflowCreateContext() + const [isCreating, setIsCreating] = useState(false) + const values = form.watch() + + useEffect(() => { + setCurrentStep(2) + }, [setCurrentStep]) + + useEffect(() => { + if ( + !values.name.trim() || + !values.modelApiKey.trim() || + !values.agentPrompt.trim() || + hasIncompleteGitRepository(values) || + hasIncompleteOutput(values) + ) { + navigate({ to: `${creationFlowUrl}/configuration` }) + } + }, [creationFlowUrl, navigate, values]) + + const handleEditSection = (section: AgenticWorkflowConfigurationSection) => { + setActiveSection(section) + navigate({ to: `${creationFlowUrl}/configuration` }) + } + + const handleCreate = () => { + setIsCreating(true) + generateWebhookDetails() + posthog.capture('create-service', { + selectedServiceType: 'agentic-workflow', + }) + toast('success', 'Your agentic workflow has been created') + navigate({ + to: '/organization/$organizationId/project/$projectId/environment/$environmentId/overview', + params: { organizationId, projectId, environmentId }, + }) + } + + return ( + +
+
+ Ready to create your agentic workflow +

+ Review the workflow configuration before creating it. Webhook details will be generated after creation. +

+
+ +
+ handleEditSection('service-information')}> + + + + + + handleEditSection('ai-model')}> + + + + + + handleEditSection('connectors')}> + + + + handleEditSection('git-repositories')}> + + + + handleEditSection('governance')}> + + + + + handleEditSection('docker-fragment')}> + + + + handleEditSection('outputs')}> + + + + handleEditSection('agent-prompt')}> + + +
+ +
+ + +
+
+
+ ) +} diff --git a/libs/domains/services/feature/src/lib/service-list/service-list.tsx b/libs/domains/services/feature/src/lib/service-list/service-list.tsx index 6519267dc6b..a25149fe45d 100644 --- a/libs/domains/services/feature/src/lib/service-list/service-list.tsx +++ b/libs/domains/services/feature/src/lib/service-list/service-list.tsx @@ -150,8 +150,12 @@ export function ServiceList({ className, containerClassName, environment, ...pro }) }, [actualServices, skippedServicesMap]) + const tableData = useMemo(() => { + return sortedServices.map((service) => ({ ...service, kind: 'service' as const })) + }, [sortedServices]) + const columns = useMemo(() => { - const columnHelper = createColumnHelper<(typeof actualServices)[number]>() + const columnHelper = createColumnHelper<(typeof tableData)[number]>() return [ columnHelper.display({ id: 'select', @@ -245,13 +249,15 @@ export function ServiceList({ className, containerClassName, environment, ...pro header: 'Last operation', enableColumnFilter: false, enableSorting: false, - cell: (info) => ( - - ), + cell: (info) => { + return ( + + ) + }, }), columnHelper.accessor('version', { header: 'Target version', @@ -277,10 +283,10 @@ export function ServiceList({ className, containerClassName, environment, ...pro }, }), ] - }, [environment, organizationId, projectId]) + }, [environment, organizationId, projectId, tableData]) const table = useReactTable({ - data: sortedServices, + data: tableData, columns, state: { sorting, @@ -427,9 +433,14 @@ export function ServiceList({ className, containerClassName, environment, ...pro {table.getRowModel().rows.map((row) => ( handleNavigateToService(row.original.id)} + onClick={() => { + handleNavigateToService(row.original.id) + }} onKeyDown={(e) => { if (e.key === 'Enter') handleNavigateToService(row.original.id) }} diff --git a/libs/domains/services/feature/src/lib/service-new/service-new.spec.tsx b/libs/domains/services/feature/src/lib/service-new/service-new.spec.tsx index dc625b2e853..449f201b49f 100644 --- a/libs/domains/services/feature/src/lib/service-new/service-new.spec.tsx +++ b/libs/domains/services/feature/src/lib/service-new/service-new.spec.tsx @@ -131,6 +131,24 @@ describe('ServiceNew', () => { expect(screen.getByText('Cron Job')).toBeInTheDocument() expect(screen.getByText('Helm')).toBeInTheDocument() expect(screen.getAllByText('Terraform').length).toBeGreaterThanOrEqual(1) + expect(screen.queryByText('Agentic workflow')).not.toBeInTheDocument() + }) + + it('should render agentic workflow entry when feature flag is enabled', () => { + mockUseFeatureFlagEnabled.mockImplementation((flag: string) => flag === 'argentic-workflow') + + renderWithProviders( + + ) + + expect(screen.getByText('Agentic workflow')).toBeInTheDocument() + expect( + screen.getByText('Run an AI workflow with webhooks, MCP connectors, governance, and configured outputs.') + ).toBeInTheDocument() + expect(screen.getByRole('link', { name: /Agentic workflow/i })).toHaveAttribute( + 'href', + '/organization/org-1/project/project-1/environment/env-1/service/create/agentic-workflow' + ) }) it('should render template sections by tag', () => { diff --git a/libs/domains/services/feature/src/lib/service-new/service-new.tsx b/libs/domains/services/feature/src/lib/service-new/service-new.tsx index 964dfba5fa3..737cb44a448 100644 --- a/libs/domains/services/feature/src/lib/service-new/service-new.tsx +++ b/libs/domains/services/feature/src/lib/service-new/service-new.tsx @@ -152,10 +152,23 @@ export function ServiceNew({ }: ServiceNewProps) { const isTerraformFeatureFlag = Boolean(useFeatureFlagEnabled('terraform')) const isServiceCatalogEnabled = Boolean(useFeatureFlagEnabled('service-catalog')) + const isAgenticWorkflowEnabled = Boolean(useFeatureFlagEnabled('argentic-workflow')) const { showPylonForm } = useSupportChat() const serviceEmpty: ServiceBlock[] = useMemo( () => [ + ...(isAgenticWorkflowEnabled + ? [ + { + title: 'Agentic workflow', + description: 'Run an AI workflow with webhooks, MCP connectors, governance, and configured outputs.', + icon: , + link: getServicesPath(organizationId, projectId, environmentId, '/service/create/agentic-workflow'), + cloud_provider: cloudProvider, + badge: 'BETA', + }, + ] + : []), { title: 'Application', description: 'Deploy a long running service running from Git or a Container Registry.', @@ -223,7 +236,15 @@ export function ServiceNew({ }, ]), ], - [cloudProvider, organizationId, projectId, environmentId, isTerraformFeatureFlag, showPylonForm] + [ + cloudProvider, + environmentId, + isAgenticWorkflowEnabled, + isTerraformFeatureFlag, + organizationId, + projectId, + showPylonForm, + ] ) const [searchInput, setSearchInput] = useState('') From a70a6578fe30802747d9b74853fa5be1d89dd171 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Wed, 15 Jul 2026 09:51:51 +0200 Subject: [PATCH 02/14] fix(agentic-workflow): align model options with api --- .../agentic-workflow/agentic-workflow-configuration.tsx | 4 ++-- .../agentic-workflow/agentic-workflow-context.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx index d5d944846e5..14b8747e39c 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx @@ -249,8 +249,8 @@ function AIModelCards() { disabled > - - Codex + + Bedrock Coming later. diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.tsx index 86dcf1bd1e9..2734544fd6e 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.tsx @@ -60,7 +60,7 @@ export interface AgenticWorkflowFormData { name: string description: string workflowEnabled: boolean - aiModel: 'Claude' | 'Codex' + aiModel: 'Claude' | 'Bedrock' webhookEnabled: boolean connectors: AgenticWorkflowConnector[] gitRepositories: AgenticWorkflowGitRepository[] From 1c3750228fa6ae09dc71d1f8f4d7f90963bc71ce Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Thu, 16 Jul 2026 11:31:56 +0200 Subject: [PATCH 03/14] feat(agentic-workflow): use generated api client --- .../src/lib/domains-services-data-access.ts | 12 +++ libs/domains/services/feature/src/index.ts | 1 + .../agentic-workflow-configuration.tsx | 2 +- .../agentic-workflow-summary.tsx | 32 ++----- .../use-create-agentic-workflow.ts | 84 +++++++++++++++++++ 5 files changed, 107 insertions(+), 24 deletions(-) create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow.ts diff --git a/libs/domains/services/data-access/src/lib/domains-services-data-access.ts b/libs/domains/services/data-access/src/lib/domains-services-data-access.ts index 835ee0ff981..c172df4c729 100644 --- a/libs/domains/services/data-access/src/lib/domains-services-data-access.ts +++ b/libs/domains/services/data-access/src/lib/domains-services-data-access.ts @@ -1,5 +1,7 @@ import { createQueryKeys, type inferQueryKeys } from '@lukemorales/query-key-factory' import { + type AgenticWorkflowRequest, + AgenticWorkflowsApi, ApplicationActionsApi, type ApplicationAdvancedSettings, ApplicationConfigurationApi, @@ -104,6 +106,7 @@ const helmsApi = new HelmsApi() const terraformsApi = new TerraformsApi() const blueprintApi = new BlueprintMainCallsApi() const blueprintCatalogApi = new BlueprintCatalogApi() +const agenticWorkflowsApi = new AgenticWorkflowsApi() const applicationMainCallsApi = new ApplicationMainCallsApi() const containerMainCallsApi = new ContainerMainCallsApi() @@ -780,6 +783,11 @@ type UpdateBlueprintRequest = { payload: BlueprintUpdateRequest } +type CreateAgenticWorkflowRequest = { + environmentId: string + payload: AgenticWorkflowRequest +} + type EditServiceRequest = { serviceId: string payload: @@ -1061,6 +1069,10 @@ export const mutations = { const response = await blueprintApi.updateBlueprint(blueprintId, payload) return response.data }, + async createAgenticWorkflow({ environmentId, payload }: CreateAgenticWorkflowRequest) { + const response = await agenticWorkflowsApi.createAgenticWorkflow(environmentId, payload) + return response.data + }, async editService({ serviceId, payload }: EditServiceRequest) { const { mutation } = match(payload) .with({ serviceType: 'APPLICATION' }, ({ serviceType, ...payload }) => ({ diff --git a/libs/domains/services/feature/src/index.ts b/libs/domains/services/feature/src/index.ts index b8f590dbedf..17946de424e 100644 --- a/libs/domains/services/feature/src/index.ts +++ b/libs/domains/services/feature/src/index.ts @@ -109,6 +109,7 @@ export * from './lib/service-update-flow/blueprint/blueprint-update-flow' export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-context' export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration' export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-summary' +export * from './lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow' export * from './lib/application-container-healthchecks/application-container-healthchecks-form/application-container-healthchecks-form' export * from './lib/application-container-healthchecks/healthchecks-utils' export * from './lib/application-container-healthchecks/step-healthchecks/step-healthchecks' diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx index 14b8747e39c..4295f30d1d2 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx @@ -872,7 +872,7 @@ export function AgenticWorkflowConfiguration() { <> { const outputs = [...values.outputs] diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx index 84b5317f92f..ba2b819201d 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx @@ -1,28 +1,13 @@ import { useNavigate, useParams } from '@tanstack/react-router' import posthog from 'posthog-js' -import { type ReactNode, useEffect, useState } from 'react' -import { Button, FunnelFlowBody, Heading, Icon, Section, SummaryValue, toast } from '@qovery/shared/ui' +import { type ReactNode, useEffect } from 'react' +import { Button, FunnelFlowBody, Heading, Icon, Section, SummaryValue } from '@qovery/shared/ui' import { type AgenticWorkflowConfigurationSection, type AgenticWorkflowFormData, useAgenticWorkflowCreateContext, } from './agentic-workflow-context' - -function generateToken(prefix: string) { - const randomPart = Math.random().toString(36).slice(2, 10) - const timePart = Date.now().toString(36).slice(-6) - - return `${prefix}_${timePart}${randomPart}` -} - -function generateWebhookDetails() { - const id = generateToken('awf') - - return { - webhookUrl: `https://hooks.qovery.local/agentic-workflows/${id}`, - webhookSecret: generateToken('qvy_whsec'), - } -} +import { formatAgenticWorkflowRequest, useCreateAgenticWorkflow } from './use-create-agentic-workflow' function truncateSummary(value: string) { if (!value.trim()) return '-' @@ -78,7 +63,7 @@ export function AgenticWorkflowSummary() { const navigate = useNavigate() const { environmentId = '', organizationId = '', projectId = '' } = useParams({ strict: false }) const { creationFlowUrl, form, setActiveSection, setCurrentStep } = useAgenticWorkflowCreateContext() - const [isCreating, setIsCreating] = useState(false) + const { isLoading: isCreating, mutateAsync: createAgenticWorkflow } = useCreateAgenticWorkflow({ environmentId }) const values = form.watch() useEffect(() => { @@ -102,13 +87,14 @@ export function AgenticWorkflowSummary() { navigate({ to: `${creationFlowUrl}/configuration` }) } - const handleCreate = () => { - setIsCreating(true) - generateWebhookDetails() + const handleCreate = async () => { + await createAgenticWorkflow({ + environmentId, + payload: formatAgenticWorkflowRequest(form.getValues()), + }) posthog.capture('create-service', { selectedServiceType: 'agentic-workflow', }) - toast('success', 'Your agentic workflow has been created') navigate({ to: '/organization/$organizationId/project/$projectId/environment/$environmentId/overview', params: { organizationId, projectId, environmentId }, diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow.ts b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow.ts new file mode 100644 index 00000000000..2fd351949d2 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow.ts @@ -0,0 +1,84 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { type AgenticWorkflowHeader, AgenticWorkflowModel, type AgenticWorkflowRequest } from 'qovery-typescript-axios' +import { mutations } from '@qovery/domains/services/data-access' +import { queries } from '@qovery/state/util-queries' +import { type AgenticWorkflowFormData } from './agentic-workflow-context' + +function parseHeaders(headersJson: string): AgenticWorkflowHeader[] { + if (!headersJson.trim()) { + return [] + } + + const parsedValue = JSON.parse(headersJson) + + if (!parsedValue || typeof parsedValue !== 'object' || Array.isArray(parsedValue)) { + return [] + } + + return Object.entries(parsedValue) + .filter((entry): entry is [string, string] => typeof entry[1] === 'string') + .map(([name, value]) => ({ name, value })) +} + +function formatWhitelistHosts(value: string) { + return value + .split(',') + .map((host) => host.trim()) + .filter(Boolean) +} + +function formatOutputHeaders(authentication: string): AgenticWorkflowHeader[] { + if (!authentication.trim()) { + return [] + } + + return [{ name: 'Authorization', value: authentication.trim() }] +} + +export function formatAgenticWorkflowRequest(values: AgenticWorkflowFormData): AgenticWorkflowRequest { + return { + name: values.name, + description: values.description, + whitelist_hosts: formatWhitelistHosts(values.whitelistHosts), + model_settings: values.modelSettingsJson, + docker_fragment: values.dockerFragment, + enabled: values.workflowEnabled, + mcp_connectors: values.connectors.map((connector) => ({ + name: connector.name, + url: connector.url, + headers: parseHeaders(connector.headersJson), + instructions: connector.mcpServersJson, + })), + outputs: values.outputs.map((output) => ({ + name: output.name, + url: output.type === 'Other' ? null : output.url, + headers: output.type === 'Other' ? [] : formatOutputHeaders(output.authentication), + instructions: output.prompt, + })), + model: values.aiModel === 'Claude' ? AgenticWorkflowModel.CLAUDE : AgenticWorkflowModel.BEDROCK, + project_repositories: values.gitRepositories.map((repository) => ({ + url: repository.repository, + branch: repository.branch, + root_path: '', + git_token_id: repository.gitTokenId ?? '', + })), + } +} + +export function useCreateAgenticWorkflow({ environmentId }: { environmentId: string }) { + const queryClient = useQueryClient() + + return useMutation(mutations.createAgenticWorkflow, { + onSuccess() { + queryClient.invalidateQueries({ + queryKey: queries.services.list(environmentId).queryKey, + }) + }, + meta: { + notifyOnSuccess: { + title: 'Your agentic workflow has been created', + }, + notifyOnError: true, + }, + }) +} From 835398d57c8ba52523a4684fdb92f30b9b375359 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Fri, 17 Jul 2026 10:31:55 +0200 Subject: [PATCH 04/14] refactor(agentic-workflow): reuse summary helpers --- .../agentic-workflow/agentic-workflow-summary.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx index ba2b819201d..c14530782f7 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx @@ -1,7 +1,8 @@ import { useNavigate, useParams } from '@tanstack/react-router' import posthog from 'posthog-js' import { type ReactNode, useEffect } from 'react' -import { Button, FunnelFlowBody, Heading, Icon, Section, SummaryValue } from '@qovery/shared/ui' +import { Button, FunnelFlowBody, Heading, Icon, Section, SummaryValue, truncateText } from '@qovery/shared/ui' +import { pluralize } from '@qovery/shared/util-js' import { type AgenticWorkflowConfigurationSection, type AgenticWorkflowFormData, @@ -12,17 +13,17 @@ import { formatAgenticWorkflowRequest, useCreateAgenticWorkflow } from './use-cr function truncateSummary(value: string) { if (!value.trim()) return '-' - return value.length > 160 ? `${value.slice(0, 157)}...` : value + return value.length > 160 ? `${truncateText(value, 157)}…` : value } function formatCount(count: number, singular: string) { if (count === 0) return 'None' - return `${count} ${singular}${count > 1 ? 's' : ''}` + return `${count} ${pluralize(count, singular)}` } function maskValue(value: string) { - return value.trim() ? '••••••••••••' : '-' + return value.trim() ? '********' : '-' } function hasIncompleteGitRepository(values: AgenticWorkflowFormData) { From 79e60e90aeafc95588d7447af59ffa96c0ce78d2 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Fri, 17 Jul 2026 10:54:21 +0200 Subject: [PATCH 05/14] test(services): update service list snapshot --- .../service-list/__snapshots__/service-list.spec.tsx.snap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/domains/services/feature/src/lib/service-list/__snapshots__/service-list.spec.tsx.snap b/libs/domains/services/feature/src/lib/service-list/__snapshots__/service-list.spec.tsx.snap index b7c5ca58d0e..beb95efda18 100644 --- a/libs/domains/services/feature/src/lib/service-list/__snapshots__/service-list.spec.tsx.snap +++ b/libs/domains/services/feature/src/lib/service-list/__snapshots__/service-list.spec.tsx.snap @@ -81,7 +81,7 @@ exports[`ServiceList should match snapshot 1`] = ` class="divide-y divide-neutral" > Date: Fri, 17 Jul 2026 15:33:20 +0200 Subject: [PATCH 06/14] fix(agentic-workflow): rename governance field to IP allow list --- .../agentic-workflow-configuration.tsx | 10 +++++----- .../agentic-workflow/agentic-workflow-context.tsx | 4 ++-- .../agentic-workflow/agentic-workflow-summary.tsx | 2 +- .../agentic-workflow/use-create-agentic-workflow.ts | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx index 4295f30d1d2..d372baa8f03 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx @@ -764,11 +764,11 @@ export function AgenticWorkflowConfiguration() { form.setValue('whitelistHosts', event.currentTarget.value, { shouldDirty: true })} + name="ip-allow-list" + label="IP allow list" + value={values.ipAllowlist} + hint="Enter IPs or CIDR ranges separated by commas. Example: 203.0.113.10, 198.51.100.0/24." + onChange={(event) => form.setValue('ipAllowlist', event.currentTarget.value, { shouldDirty: true })} /> handleEditSection('governance')}> - + diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow.ts b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow.ts index 2fd351949d2..1ca94cc28ed 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow.ts +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow.ts @@ -20,10 +20,10 @@ function parseHeaders(headersJson: string): AgenticWorkflowHeader[] { .map(([name, value]) => ({ name, value })) } -function formatWhitelistHosts(value: string) { +function formatIpAllowlist(value: string) { return value .split(',') - .map((host) => host.trim()) + .map((entry) => entry.trim()) .filter(Boolean) } @@ -39,7 +39,7 @@ export function formatAgenticWorkflowRequest(values: AgenticWorkflowFormData): A return { name: values.name, description: values.description, - whitelist_hosts: formatWhitelistHosts(values.whitelistHosts), + whitelist_hosts: formatIpAllowlist(values.ipAllowlist), model_settings: values.modelSettingsJson, docker_fragment: values.dockerFragment, enabled: values.workflowEnabled, From 4c17730a8ef6e34516cb9a5de8bb8348faf6168f Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Mon, 20 Jul 2026 10:55:46 +0200 Subject: [PATCH 07/14] feat(services): adjust agentic workflow creation --- .../agentic-workflow-configuration.tsx | 455 ++++++------------ .../agentic-workflow-context.tsx | 78 ++- .../agentic-workflow-summary.tsx | 29 +- .../use-create-agentic-workflow.ts | 42 +- 4 files changed, 245 insertions(+), 359 deletions(-) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx index d372baa8f03..e835d768f15 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx @@ -1,7 +1,7 @@ import { type IconName } from '@fortawesome/fontawesome-common-types' import { useNavigate, useParams } from '@tanstack/react-router' import { type GitProviderEnum, type GitRepository } from 'qovery-typescript-axios' -import { type ReactNode, useEffect } from 'react' +import { type ReactNode, useEffect, useRef } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { GitBranchSettings, GitProviderSetting, GitRepositorySetting } from '@qovery/domains/organizations/feature' import { IconEnum } from '@qovery/shared/enums' @@ -12,42 +12,10 @@ import { type AgenticWorkflowGitRepository, type AgenticWorkflowOutput, DEFAULT_CONNECTOR_JSON, + MCP_CONNECTOR_JSON_EXAMPLE, useAgenticWorkflowCreateContext, } from './agentic-workflow-context' -const outputTypes: { iconName: IconName; label: AgenticWorkflowOutput['type'] }[] = [ - { label: 'Slack', iconName: 'slack' }, - { label: 'Jira', iconName: 'jira' }, - { label: 'GitHub', iconName: 'github' }, - { label: 'Other', iconName: 'ellipsis' }, -] - -const connectorTypes: { iconName: IconName; label: AgenticWorkflowConnector['type'] }[] = outputTypes - -const defaultMcpUrls: Record, string> = { - Slack: 'https://mcp.slack.com/mcp', - Jira: 'https://mcp.atlassian.com/v1/sse', - GitHub: 'https://api.githubcopilot.com/mcp/', -} - -function getDefaultIntegrationName(type: AgenticWorkflowConnector['type'] | AgenticWorkflowOutput['type']) { - return type -} - -function getNextIntegrationName( - currentName: string, - previousType: AgenticWorkflowConnector['type'] | AgenticWorkflowOutput['type'], - nextType: AgenticWorkflowConnector['type'] | AgenticWorkflowOutput['type'] -) { - const trimmedName = currentName.trim() - - if (!trimmedName || trimmedName === getDefaultIntegrationName(previousType)) { - return getDefaultIntegrationName(nextType) - } - - return currentName -} - const sectionOrder: AgenticWorkflowConfigurationSection[] = [ 'service-information', 'ai-model', @@ -81,11 +49,11 @@ function getSectionTitle(section: AgenticWorkflowConfigurationSection) { const titles: Record = { 'service-information': 'Service information', 'ai-model': 'AI model', - connectors: 'Connectors', + connectors: 'MCPs', 'git-repositories': 'Git repositories', governance: 'Governance', 'docker-fragment': 'Docker fragment', - outputs: 'Outputs', + outputs: 'Output webhooks', 'agent-prompt': 'Agent prompt', } @@ -93,17 +61,13 @@ function getSectionTitle(section: AgenticWorkflowConfigurationSection) { } function isGitRepositoryComplete(repository: AgenticWorkflowGitRepository) { - return Boolean(repository.gitTokenId && repository.repository.trim() && repository.branch.trim()) + return Boolean( + repository.gitTokenId && repository.repository.trim() && repository.branch.trim() && (repository.rootPath || '/').trim() + ) } function isOutputComplete(output: AgenticWorkflowOutput) { - return output.type === 'Other' - ? Boolean(output.prompt.trim()) - : Boolean(output.url.trim() && output.authentication.trim()) -} - -function isConnectorComplete(connector: AgenticWorkflowConnector) { - return Boolean(connector.url.trim()) + return Boolean(output.url.trim()) } function AgenticWorkflowSection({ @@ -216,7 +180,7 @@ function CodeEditorField({ }} /> - {hint && !error &&

{hint}

} + {hint && !error &&
{hint}
} {error &&

{error}

} ) @@ -258,98 +222,6 @@ function AIModelCards() { ) } -function IntegrationTypeIcon({ - iconName, - label, - selected, -}: { - iconName: IconName - label: AgenticWorkflowConnector['type'] | AgenticWorkflowOutput['type'] - selected: boolean -}) { - if (label === 'Slack') { - return - } - - if (label === 'GitHub') { - return - } - - if (label === 'Jira') { - return - } - - return -} - -function ConnectorTypeCards({ - onChange, - value, -}: { - onChange: (value: AgenticWorkflowConnector['type']) => void - value: AgenticWorkflowConnector['type'] -}) { - return ( -
- {connectorTypes.map((connectorType) => { - const selected = value === connectorType.label - - return ( - - ) - })} -
- ) -} - -function OutputTypeCards({ - onChange, - value, -}: { - onChange: (value: AgenticWorkflowOutput['type']) => void - value: AgenticWorkflowOutput['type'] -}) { - return ( -
- {outputTypes.map((outputType) => { - const selected = value === outputType.label - - return ( - - ) - })} -
- ) -} - function GitRepositoryCard({ index, onChange, @@ -367,6 +239,7 @@ function GitRepositoryCard({ is_public_repository?: boolean repository: string branch: string + root_path: string git_token_name?: string | null git_token_id?: string | null git_repository?: GitRepository @@ -376,6 +249,7 @@ function GitRepositoryCard({ is_public_repository: repository.isPublicRepository, repository: repository.repository, branch: repository.branch, + root_path: repository.rootPath || '/', git_token_name: repository.gitTokenName, git_token_id: repository.gitTokenId, git_repository: repository.gitRepository, @@ -397,6 +271,7 @@ function GitRepositoryCard({ repository: values.repository ?? '', gitRepository: values.git_repository as GitRepository | undefined, branch: values.branch ?? '', + rootPath: values.root_path ?? '/', }) }) @@ -424,7 +299,8 @@ function GitRepositoryCard({ organizationId={organizationId} gitProvider={provider} gitTokenId={gitTokenId} - hideRootPath + rootPathLabel="Repository folder path" + rootPathHint="Folder inside the repository the workflow should use. Use / for the repository root." /> )} @@ -438,13 +314,14 @@ function GitRepositoryCard({ export function AgenticWorkflowConfiguration() { const navigate = useNavigate() const { activeSection, creationFlowUrl, form, setActiveSection, setCurrentStep } = useAgenticWorkflowCreateContext() + const modelApiKeyInputRef = useRef(null) + const whitelistHostsTextareaRef = useRef(null) + const agentPromptTextareaRef = useRef(null) const values = form.watch() const { dirtyFields } = form.formState const connectorErrors = values.connectors.map((connector) => getJsonError(connector.mcpServersJson, true)) - const connectorHeadersErrors = values.connectors.map((connector) => getJsonError(connector.headersJson)) - const connectorsValid = values.connectors.every(isConnectorComplete) + const outputHeadersErrors = values.outputs.map((output) => getJsonError(output.headersJson)) const modelSettingsJsonError = getJsonError(values.modelSettingsJson, true) - const claudeConfigJsonError = getJsonError(values.claudeConfigJson, true) const gitRepositoriesValid = values.gitRepositories.every(isGitRepositoryComplete) const outputsValid = values.outputs.every(isOutputComplete) const showNameError = Boolean(dirtyFields.name) && !values.name.trim() @@ -452,11 +329,11 @@ export function AgenticWorkflowConfiguration() { const sectionInvalid: Record = { 'service-information': !values.name.trim(), 'ai-model': !values.modelApiKey.trim() || Boolean(modelSettingsJsonError), - connectors: !connectorsValid || connectorErrors.some(Boolean) || connectorHeadersErrors.some(Boolean), + connectors: connectorErrors.some(Boolean), 'git-repositories': !gitRepositoriesValid, - governance: Boolean(claudeConfigJsonError), + governance: false, 'docker-fragment': false, - outputs: !outputsValid, + outputs: !outputsValid || outputHeadersErrors.some(Boolean), 'agent-prompt': !values.agentPrompt.trim(), } const isValid = @@ -465,16 +342,24 @@ export function AgenticWorkflowConfiguration() { Boolean(values.agentPrompt.trim()) && gitRepositoriesValid && outputsValid && - connectorsValid && connectorErrors.every((error) => !error) && - connectorHeadersErrors.every((error) => !error) && - !modelSettingsJsonError && - !claudeConfigJsonError + outputHeadersErrors.every((error) => !error) && + !modelSettingsJsonError useEffect(() => { setCurrentStep(1) }, [setCurrentStep]) + useEffect(() => { + const inputBySection: Partial> = { + 'ai-model': modelApiKeyInputRef.current, + governance: whitelistHostsTextareaRef.current, + 'agent-prompt': agentPromptTextareaRef.current, + } + + window.requestAnimationFrame(() => inputBySection[activeSection]?.focus()) + }, [activeSection]) + const goToNextSection = () => { const nextSection = sectionOrder[sectionOrder.indexOf(activeSection) + 1] @@ -492,11 +377,7 @@ export function AgenticWorkflowConfiguration() { [ ...values.connectors, { - type: 'Slack', - name: getDefaultIntegrationName('Slack'), - url: defaultMcpUrls.Slack, mcpServersJson: DEFAULT_CONNECTOR_JSON, - headersJson: '{}', }, ], { @@ -517,6 +398,7 @@ export function AgenticWorkflowConfiguration() { repository: '', gitRepository: undefined, branch: '', + rootPath: '/', }, ], { shouldDirty: true } @@ -528,10 +410,10 @@ export function AgenticWorkflowConfiguration() { [ ...values.outputs, { - type: 'Slack', - name: getDefaultIntegrationName('Slack'), - url: defaultMcpUrls.Slack, - authentication: '', + url: '', + headersJson: `{ + "Authorization": "Bearer {{TOKEN}}" +}`, prompt: '', }, ], @@ -569,6 +451,29 @@ export function AgenticWorkflowConfiguration() { value={values.description} onChange={(event) => form.setValue('description', event.currentTarget.value, { shouldDirty: true })} /> +
+ form.setValue('cpu', event.currentTarget.value, { shouldDirty: true })} + /> + form.setValue('memory', event.currentTarget.value, { shouldDirty: true })} + /> + form.setValue('storage', event.currentTarget.value, { shouldDirty: true })} + /> +
+ Need help configuring Claude Code settings? Read the{' '} + + Claude Code settings documentation + + . + + } onChange={(value) => form.setValue('modelSettingsJson', value, { shouldDirty: true })} /> - Add connector + Add MCP } > {values.connectors.map((connector, index) => (
- Connector {index + 1} + MCP {index + 1}
-
- { - const connectors = [...values.connectors] - connectors[index] = { - ...connector, - type, - name: getNextIntegrationName(connector.name, connector.type, type), - url: type === 'Other' ? '' : defaultMcpUrls[type], - } - form.setValue('connectors', connectors, { shouldDirty: true }) - }} - /> - { - const connectors = [...values.connectors] - connectors[index] = { ...connector, name: event.currentTarget.value } - form.setValue('connectors', connectors, { shouldDirty: true }) - }} - /> - { - const connectors = [...values.connectors] - connectors[index] = { ...connector, url: event.currentTarget.value } - form.setValue('connectors', connectors, { shouldDirty: true }) - }} - /> - { - const connectors = [...values.connectors] - connectors[index] = { ...connector, headersJson: value } - form.setValue('connectors', connectors, { shouldDirty: true }) - }} - /> - { - const connectors = [...values.connectors] - connectors[index] = { ...connector, mcpServersJson: value } - form.setValue('connectors', connectors, { shouldDirty: true }) - }} - /> -
+ + + See Claude Code docs for{' '} + + remote HTTP + {' '} + and{' '} + + local stdio + {' '} + servers. + + Example: +
+                      {MCP_CONNECTOR_JSON_EXAMPLE}
+                    
+ + } + onChange={(value) => { + const connectors = [...values.connectors] + connectors[index] = { ...connector, mcpServersJson: value } + form.setValue('connectors', connectors, { shouldDirty: true }) + }} + />
))} - {!connectorsValid && ( -

Enter an MCP URL for each configured connector.

- )} - +
form.setValue('ipAllowlist', event.currentTarget.value, { shouldDirty: true })} - /> - - Need help with permissions and allowed routes? Use{' '} - - Claude settings generator - - . - - } - onChange={(value) => form.setValue('claudeConfigJson', value, { shouldDirty: true })} + ref={whitelistHostsTextareaRef} + name="whitelist-hosts" + label="Domain allowlist" + value={values.whitelistHosts} + hint="Use * to allow all domains, or enter hostnames separated by commas. Example: api.github.com, jira.company.com." + onChange={(event) => form.setValue('whitelistHosts', event.currentTarget.value, { shouldDirty: true })} /> @@ -801,6 +672,7 @@ export function AgenticWorkflowConfiguration() { label="Dockerfile fragment" language="dockerfile" value={values.dockerFragment} + hint="Use this to install CLI or binary. Example: RUN npm install -g @modelcontextprotocol/server-github." onChange={(value) => form.setValue('dockerFragment', value, { shouldDirty: true })} /> @@ -820,14 +692,14 @@ export function AgenticWorkflowConfiguration() { onClick={addOutput} > - Add output + Add webhook } > {values.outputs.map((output, index) => (
- Output {index + 1} + Webhook {index + 1}
- { + { const outputs = [...values.outputs] - outputs[index] = { - ...output, - type, - name: getNextIntegrationName(output.name, output.type, type), - url: type === 'Other' ? '' : defaultMcpUrls[type], - } + outputs[index] = { ...output, url: event.currentTarget.value } form.setValue('outputs', outputs, { shouldDirty: true }) }} /> - { + { const outputs = [...values.outputs] - outputs[index] = { ...output, name: event.currentTarget.value } + outputs[index] = { ...output, headersJson: value } form.setValue('outputs', outputs, { shouldDirty: true }) }} /> - {output.type !== 'Other' && ( - <> - { - const outputs = [...values.outputs] - outputs[index] = { ...output, url: event.currentTarget.value } - form.setValue('outputs', outputs, { shouldDirty: true }) - }} - /> - { - const outputs = [...values.outputs] - outputs[index] = { ...output, authentication: event.currentTarget.value } - form.setValue('outputs', outputs, { shouldDirty: true }) - }} - /> - - )} { const outputs = [...values.outputs] @@ -913,10 +757,10 @@ export function AgenticWorkflowConfiguration() { ))} {!outputsValid && (

- Enter a URL and authentication value for each configured output. For Other, describe the expected output. + Enter a webhook URL for each configured output webhook.

)} - + - output.type === 'Other' ? !output.prompt.trim() : !output.url.trim() || !output.authentication.trim() - ) + return values.outputs.some((output) => { + if (!output.url.trim()) return true + + if (!output.headersJson.trim()) return false + + try { + JSON.parse(output.headersJson) + return false + } catch { + return true + } + }) } function SummarySection({ children, onEdit, title }: { children: ReactNode; onEdit: () => void; title: string }) { @@ -116,6 +125,9 @@ export function AgenticWorkflowSummary() { handleEditSection('service-information')}> + + + @@ -125,8 +137,8 @@ export function AgenticWorkflowSummary() { - handleEditSection('connectors')}> - + handleEditSection('connectors')}> + handleEditSection('git-repositories')}> @@ -134,16 +146,15 @@ export function AgenticWorkflowSummary() { handleEditSection('governance')}> - - + handleEditSection('docker-fragment')}> - handleEditSection('outputs')}> - + handleEditSection('outputs')}> + handleEditSection('agent-prompt')}> diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow.ts b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow.ts index 1ca94cc28ed..de054700c07 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow.ts +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow.ts @@ -4,6 +4,13 @@ import { mutations } from '@qovery/domains/services/data-access' import { queries } from '@qovery/state/util-queries' import { type AgenticWorkflowFormData } from './agentic-workflow-context' +function formatWhitelistHosts(value: string) { + return value + .split(',') + .map((host) => host.trim()) + .filter(Boolean) +} + function parseHeaders(headersJson: string): AgenticWorkflowHeader[] { if (!headersJson.trim()) { return [] @@ -20,46 +27,31 @@ function parseHeaders(headersJson: string): AgenticWorkflowHeader[] { .map(([name, value]) => ({ name, value })) } -function formatIpAllowlist(value: string) { - return value - .split(',') - .map((entry) => entry.trim()) - .filter(Boolean) -} - -function formatOutputHeaders(authentication: string): AgenticWorkflowHeader[] { - if (!authentication.trim()) { - return [] - } - - return [{ name: 'Authorization', value: authentication.trim() }] -} - export function formatAgenticWorkflowRequest(values: AgenticWorkflowFormData): AgenticWorkflowRequest { return { name: values.name, description: values.description, - whitelist_hosts: formatIpAllowlist(values.ipAllowlist), + whitelist_hosts: formatWhitelistHosts(values.whitelistHosts), model_settings: values.modelSettingsJson, docker_fragment: values.dockerFragment, enabled: values.workflowEnabled, - mcp_connectors: values.connectors.map((connector) => ({ - name: connector.name, - url: connector.url, - headers: parseHeaders(connector.headersJson), + mcp_connectors: values.connectors.map((connector, index) => ({ + name: `MCP ${index + 1}`, + url: '', + headers: [], instructions: connector.mcpServersJson, })), - outputs: values.outputs.map((output) => ({ - name: output.name, - url: output.type === 'Other' ? null : output.url, - headers: output.type === 'Other' ? [] : formatOutputHeaders(output.authentication), + outputs: values.outputs.map((output, index) => ({ + name: `Output ${index + 1}`, + url: output.url, + headers: parseHeaders(output.headersJson), instructions: output.prompt, })), model: values.aiModel === 'Claude' ? AgenticWorkflowModel.CLAUDE : AgenticWorkflowModel.BEDROCK, project_repositories: values.gitRepositories.map((repository) => ({ url: repository.repository, branch: repository.branch, - root_path: '', + root_path: repository.rootPath || '/', git_token_id: repository.gitTokenId ?? '', })), } From 7d10527be64d7388fd7d05aa3ed5cac3ef6319c3 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Mon, 20 Jul 2026 11:13:24 +0200 Subject: [PATCH 08/14] test(services): cover agentic workflow creation --- libs/domains/services/feature/src/index.ts | 2 +- .../agentic-workflow-configuration.tsx | 809 +----------------- .../agentic-workflow-configuration.spec.tsx | 35 + .../agentic-workflow-configuration.tsx | 680 +++++++++++++++ .../ai-model-cards.spec.tsx | 37 + .../ai-model-cards.tsx | 39 + .../git-repository-card.spec.tsx | 60 ++ .../git-repository-card.tsx | 96 +++ .../agentic-workflow-configuration/index.ts | 1 + .../agentic-workflow-context.spec.tsx | 33 + .../agentic-workflow-summary.spec.tsx | 115 +++ .../agentic-workflow-summary.tsx | 2 +- .../use-create-agentic-workflow/index.ts | 1 + .../use-create-agentic-workflow.spec.ts | 96 +++ .../use-create-agentic-workflow.ts | 2 +- 15 files changed, 1197 insertions(+), 811 deletions(-) create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.spec.tsx create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.spec.tsx create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.tsx create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/git-repository-card.spec.tsx create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/git-repository-card.tsx create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/index.ts create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.spec.tsx create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/index.ts create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/use-create-agentic-workflow.spec.ts rename libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/{ => hooks/use-create-agentic-workflow}/use-create-agentic-workflow.ts (96%) diff --git a/libs/domains/services/feature/src/index.ts b/libs/domains/services/feature/src/index.ts index 17946de424e..dd85e3886f9 100644 --- a/libs/domains/services/feature/src/index.ts +++ b/libs/domains/services/feature/src/index.ts @@ -109,7 +109,7 @@ export * from './lib/service-update-flow/blueprint/blueprint-update-flow' export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-context' export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration' export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-summary' -export * from './lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow' +export * from './lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow' export * from './lib/application-container-healthchecks/application-container-healthchecks-form/application-container-healthchecks-form' export * from './lib/application-container-healthchecks/healthchecks-utils' export * from './lib/application-container-healthchecks/step-healthchecks/step-healthchecks' diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx index e835d768f15..e131edd6eb6 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx @@ -1,808 +1 @@ -import { type IconName } from '@fortawesome/fontawesome-common-types' -import { useNavigate, useParams } from '@tanstack/react-router' -import { type GitProviderEnum, type GitRepository } from 'qovery-typescript-axios' -import { type ReactNode, useEffect, useRef } from 'react' -import { FormProvider, useForm } from 'react-hook-form' -import { GitBranchSettings, GitProviderSetting, GitRepositorySetting } from '@qovery/domains/organizations/feature' -import { IconEnum } from '@qovery/shared/enums' -import { Button, CodeEditor, FunnelFlowBody, Icon, InputText, InputTextArea, InputToggle } from '@qovery/shared/ui' -import { - type AgenticWorkflowConfigurationSection, - type AgenticWorkflowConnector, - type AgenticWorkflowGitRepository, - type AgenticWorkflowOutput, - DEFAULT_CONNECTOR_JSON, - MCP_CONNECTOR_JSON_EXAMPLE, - useAgenticWorkflowCreateContext, -} from './agentic-workflow-context' - -const sectionOrder: AgenticWorkflowConfigurationSection[] = [ - 'service-information', - 'ai-model', - 'connectors', - 'git-repositories', - 'governance', - 'docker-fragment', - 'outputs', - 'agent-prompt', -] - -function getJsonError(value: string, required = false) { - if (!value.trim()) return required ? 'Please enter a valid JSON configuration.' : undefined - - try { - JSON.parse(value) - return undefined - } catch { - return 'Invalid JSON format.' - } -} - -function isSectionCompleted( - section: AgenticWorkflowConfigurationSection, - activeSection: AgenticWorkflowConfigurationSection -) { - return sectionOrder.indexOf(section) < sectionOrder.indexOf(activeSection) -} - -function getSectionTitle(section: AgenticWorkflowConfigurationSection) { - const titles: Record = { - 'service-information': 'Service information', - 'ai-model': 'AI model', - connectors: 'MCPs', - 'git-repositories': 'Git repositories', - governance: 'Governance', - 'docker-fragment': 'Docker fragment', - outputs: 'Output webhooks', - 'agent-prompt': 'Agent prompt', - } - - return titles[section] -} - -function isGitRepositoryComplete(repository: AgenticWorkflowGitRepository) { - return Boolean( - repository.gitTokenId && repository.repository.trim() && repository.branch.trim() && (repository.rootPath || '/').trim() - ) -} - -function isOutputComplete(output: AgenticWorkflowOutput) { - return Boolean(output.url.trim()) -} - -function AgenticWorkflowSection({ - children, - headerAction, - iconName, - invalid, - section, -}: { - children: ReactNode - headerAction?: ReactNode - iconName: IconName - invalid?: boolean - section: AgenticWorkflowConfigurationSection -}) { - const { activeSection, setActiveSection } = useAgenticWorkflowCreateContext() - const active = activeSection === section - const completed = isSectionCompleted(section, activeSection) - const showStatus = completed && !active - const title = getSectionTitle(section) - const headerContent = ( - <> -
- -

- {title} -

-
-
- {showStatus && - (invalid ? ( - - ) : ( - - ))} -
- - ) - - return ( -
- {active ? ( -
- {headerContent} - {headerAction &&
{headerAction}
} -
- ) : ( - - )} - {active &&
{children}
} -
- ) -} - -function CodeEditorField({ - error, - height = '180px', - hint, - label, - language, - name, - onChange, - placeholder, - value, -}: { - error?: string - height?: string - hint?: ReactNode - label: string - language: string - name: string - onChange: (value: string) => void - placeholder?: string - value: string -}) { - return ( -
- -
- {placeholder && !value.trim() && ( -
- {placeholder} -
- )} - onChange(nextValue ?? '')} - options={{ - scrollbar: { alwaysConsumeMouseWheel: false }, - scrollBeyondLastLine: false, - wordWrap: 'on', - }} - /> -
- {hint && !error &&
{hint}
} - {error &&

{error}

} -
- ) -} - -function AIModelCards() { - const { form } = useAgenticWorkflowCreateContext() - const selectedModel = form.watch('aiModel') - - return ( -
- - -
- ) -} - -function GitRepositoryCard({ - index, - onChange, - onRemove, - repository, -}: { - index: number - onChange: (repository: AgenticWorkflowGitRepository) => void - onRemove: () => void - repository: AgenticWorkflowGitRepository -}) { - const { organizationId = '' } = useParams({ strict: false }) - const methods = useForm<{ - provider?: keyof typeof GitProviderEnum | string | null - is_public_repository?: boolean - repository: string - branch: string - root_path: string - git_token_name?: string | null - git_token_id?: string | null - git_repository?: GitRepository - }>({ - defaultValues: { - provider: repository.provider, - is_public_repository: repository.isPublicRepository, - repository: repository.repository, - branch: repository.branch, - root_path: repository.rootPath || '/', - git_token_name: repository.gitTokenName, - git_token_id: repository.gitTokenId, - git_repository: repository.gitRepository, - }, - mode: 'onChange', - }) - const provider = methods.watch('provider') as keyof typeof GitProviderEnum | undefined - const watchedRepository = methods.watch('repository') - const gitTokenId = methods.watch('git_token_id') ?? undefined - const isPublicRepository = methods.watch('is_public_repository') - - useEffect(() => { - const subscription = methods.watch((values) => { - onChange({ - provider: values.provider, - gitTokenId: values.git_token_id, - gitTokenName: values.git_token_name, - isPublicRepository: values.is_public_repository, - repository: values.repository ?? '', - gitRepository: values.git_repository as GitRepository | undefined, - branch: values.branch ?? '', - rootPath: values.root_path ?? '/', - }) - }) - - return () => subscription.unsubscribe() - }, [methods, onChange]) - - return ( -
-
- Repository {index + 1} - -
- -
- - {isPublicRepository ? null : ( - <> - {provider && ( - - )} - {provider && watchedRepository && ( - - )} - - )} -
-
-
- ) -} - -export function AgenticWorkflowConfiguration() { - const navigate = useNavigate() - const { activeSection, creationFlowUrl, form, setActiveSection, setCurrentStep } = useAgenticWorkflowCreateContext() - const modelApiKeyInputRef = useRef(null) - const whitelistHostsTextareaRef = useRef(null) - const agentPromptTextareaRef = useRef(null) - const values = form.watch() - const { dirtyFields } = form.formState - const connectorErrors = values.connectors.map((connector) => getJsonError(connector.mcpServersJson, true)) - const outputHeadersErrors = values.outputs.map((output) => getJsonError(output.headersJson)) - const modelSettingsJsonError = getJsonError(values.modelSettingsJson, true) - const gitRepositoriesValid = values.gitRepositories.every(isGitRepositoryComplete) - const outputsValid = values.outputs.every(isOutputComplete) - const showNameError = Boolean(dirtyFields.name) && !values.name.trim() - const showModelApiKeyError = Boolean(dirtyFields.modelApiKey) && !values.modelApiKey.trim() - const sectionInvalid: Record = { - 'service-information': !values.name.trim(), - 'ai-model': !values.modelApiKey.trim() || Boolean(modelSettingsJsonError), - connectors: connectorErrors.some(Boolean), - 'git-repositories': !gitRepositoriesValid, - governance: false, - 'docker-fragment': false, - outputs: !outputsValid || outputHeadersErrors.some(Boolean), - 'agent-prompt': !values.agentPrompt.trim(), - } - const isValid = - Boolean(values.name.trim()) && - Boolean(values.modelApiKey.trim()) && - Boolean(values.agentPrompt.trim()) && - gitRepositoriesValid && - outputsValid && - connectorErrors.every((error) => !error) && - outputHeadersErrors.every((error) => !error) && - !modelSettingsJsonError - - useEffect(() => { - setCurrentStep(1) - }, [setCurrentStep]) - - useEffect(() => { - const inputBySection: Partial> = { - 'ai-model': modelApiKeyInputRef.current, - governance: whitelistHostsTextareaRef.current, - 'agent-prompt': agentPromptTextareaRef.current, - } - - window.requestAnimationFrame(() => inputBySection[activeSection]?.focus()) - }, [activeSection]) - - const goToNextSection = () => { - const nextSection = sectionOrder[sectionOrder.indexOf(activeSection) + 1] - - if (nextSection) { - setActiveSection(nextSection) - return - } - - navigate({ to: `${creationFlowUrl}/summary` }) - } - - const addConnector = () => - form.setValue( - 'connectors', - [ - ...values.connectors, - { - mcpServersJson: DEFAULT_CONNECTOR_JSON, - }, - ], - { - shouldDirty: true, - } - ) - - const addRepository = () => - form.setValue( - 'gitRepositories', - [ - ...values.gitRepositories, - { - provider: undefined, - gitTokenId: undefined, - gitTokenName: undefined, - isPublicRepository: false, - repository: '', - gitRepository: undefined, - branch: '', - rootPath: '/', - }, - ], - { shouldDirty: true } - ) - - const addOutput = () => - form.setValue( - 'outputs', - [ - ...values.outputs, - { - url: '', - headersJson: `{ - "Authorization": "Bearer {{TOKEN}}" -}`, - prompt: '', - }, - ], - { - shouldDirty: true, - } - ) - - return ( - -
-

Create agentic workflow

-

- Configure the inputs, tools, model, governance, and outputs used by your workflow. -

-
- -
- - form.setValue('name', event.currentTarget.value, { shouldDirty: true })} - /> - form.setValue('description', event.currentTarget.value, { shouldDirty: true })} - /> -
- form.setValue('cpu', event.currentTarget.value, { shouldDirty: true })} - /> - form.setValue('memory', event.currentTarget.value, { shouldDirty: true })} - /> - form.setValue('storage', event.currentTarget.value, { shouldDirty: true })} - /> -
- form.setValue('workflowEnabled', value, { shouldDirty: true })} - /> - -
- - - - form.setValue('modelApiKey', event.currentTarget.value, { shouldDirty: true })} - /> - - Need help configuring Claude Code settings? Read the{' '} - - Claude Code settings documentation - - . - - } - onChange={(value) => form.setValue('modelSettingsJson', value, { shouldDirty: true })} - /> - - - - - - Add MCP - - } - > - {values.connectors.map((connector, index) => ( -
-
- MCP {index + 1} - -
- - - See Claude Code docs for{' '} - - remote HTTP - {' '} - and{' '} - - local stdio - {' '} - servers. - - Example: -
-                      {MCP_CONNECTOR_JSON_EXAMPLE}
-                    
- - } - onChange={(value) => { - const connectors = [...values.connectors] - connectors[index] = { ...connector, mcpServersJson: value } - form.setValue('connectors', connectors, { shouldDirty: true }) - }} - /> -
- ))} - -
- - - - Add repository - - } - > - {values.gitRepositories.map((repository, index) => ( - { - const gitRepositories = [...values.gitRepositories] - gitRepositories[index] = nextRepository - form.setValue('gitRepositories', gitRepositories, { shouldDirty: true }) - }} - onRemove={() => - form.setValue( - 'gitRepositories', - values.gitRepositories.filter((_, repositoryIndex) => repositoryIndex !== index), - { shouldDirty: true } - ) - } - /> - ))} - {!gitRepositoriesValid && ( -

- Select a Git account, repository, and branch for each repository. -

- )} - -
- - - form.setValue('whitelistHosts', event.currentTarget.value, { shouldDirty: true })} - /> - - - - - form.setValue('dockerFragment', value, { shouldDirty: true })} - /> - - - - - - Add webhook - - } - > - {values.outputs.map((output, index) => ( -
-
- Webhook {index + 1} - -
-
- { - const outputs = [...values.outputs] - outputs[index] = { ...output, url: event.currentTarget.value } - form.setValue('outputs', outputs, { shouldDirty: true }) - }} - /> - { - const outputs = [...values.outputs] - outputs[index] = { ...output, headersJson: value } - form.setValue('outputs', outputs, { shouldDirty: true }) - }} - /> - { - const outputs = [...values.outputs] - outputs[index] = { ...output, prompt: event.currentTarget.value } - form.setValue('outputs', outputs, { shouldDirty: true }) - }} - /> -
-
- ))} - {!outputsValid && ( -

- Enter a webhook URL for each configured output webhook. -

- )} - -
- - - form.setValue('agentPrompt', event.currentTarget.value, { shouldDirty: true })} - /> - - -
- -
-
- -
-
-
- ) -} - -function ContinueButton({ disabled, onClick }: { disabled?: boolean; onClick: () => void }) { - return ( - - ) -} +export * from './agentic-workflow-configuration/agentic-workflow-configuration' diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.spec.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.spec.tsx new file mode 100644 index 00000000000..52fad832c03 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.spec.tsx @@ -0,0 +1,35 @@ +import { getJsonError, isGitRepositoryComplete, isOutputComplete } from './agentic-workflow-configuration' + +describe('AgenticWorkflowConfiguration validation', () => { + it('should require valid JSON only when a required JSON field is empty or invalid', () => { + expect(getJsonError('', false)).toBeUndefined() + expect(getJsonError('', true)).toBe('Please enter a valid JSON configuration.') + expect(getJsonError('{"mcpServers":{}}', true)).toBeUndefined() + expect(getJsonError('{invalid', true)).toBe('Invalid JSON format.') + }) + + it('should require token, repository, branch, and root path for configured repositories', () => { + expect( + isGitRepositoryComplete({ + gitTokenId: 'token-1', + repository: 'https://github.com/qovery/console', + branch: 'main', + rootPath: '/', + }) + ).toBe(true) + + expect( + isGitRepositoryComplete({ + gitTokenId: 'token-1', + repository: 'https://github.com/qovery/console', + branch: '', + rootPath: '/', + }) + ).toBe(false) + }) + + it('should require a webhook URL for configured output webhooks', () => { + expect(isOutputComplete({ url: 'https://hooks.example.com/workflow', headersJson: '{}', prompt: '' })).toBe(true) + expect(isOutputComplete({ url: '', headersJson: '{}', prompt: 'Notify the team.' })).toBe(false) + }) +}) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx new file mode 100644 index 00000000000..1de17637610 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx @@ -0,0 +1,680 @@ +import { type IconName } from '@fortawesome/fontawesome-common-types' +import { useNavigate } from '@tanstack/react-router' +import { type ReactNode, useEffect, useRef } from 'react' +import { Button, CodeEditor, FunnelFlowBody, Icon, InputText, InputTextArea, InputToggle } from '@qovery/shared/ui' +import { + type AgenticWorkflowConfigurationSection, + type AgenticWorkflowGitRepository, + type AgenticWorkflowOutput, + DEFAULT_CONNECTOR_JSON, + MCP_CONNECTOR_JSON_EXAMPLE, + useAgenticWorkflowCreateContext, +} from '../agentic-workflow-context' +import { AIModelCards } from './ai-model-cards' +import { GitRepositoryCard } from './git-repository-card' + +const sectionOrder: AgenticWorkflowConfigurationSection[] = [ + 'service-information', + 'ai-model', + 'connectors', + 'git-repositories', + 'governance', + 'docker-fragment', + 'outputs', + 'agent-prompt', +] + +export function getJsonError(value: string, required = false) { + if (!value.trim()) return required ? 'Please enter a valid JSON configuration.' : undefined + + try { + JSON.parse(value) + return undefined + } catch { + return 'Invalid JSON format.' + } +} + +function isSectionCompleted( + section: AgenticWorkflowConfigurationSection, + activeSection: AgenticWorkflowConfigurationSection +) { + return sectionOrder.indexOf(section) < sectionOrder.indexOf(activeSection) +} + +function getSectionTitle(section: AgenticWorkflowConfigurationSection) { + const titles: Record = { + 'service-information': 'Service information', + 'ai-model': 'AI model', + connectors: 'MCPs', + 'git-repositories': 'Git repositories', + governance: 'Governance', + 'docker-fragment': 'Docker fragment', + outputs: 'Output webhooks', + 'agent-prompt': 'Agent prompt', + } + + return titles[section] +} + +export function isGitRepositoryComplete(repository: AgenticWorkflowGitRepository) { + return Boolean( + repository.gitTokenId && repository.repository.trim() && repository.branch.trim() && (repository.rootPath || '/').trim() + ) +} + +export function isOutputComplete(output: AgenticWorkflowOutput) { + return Boolean(output.url.trim()) +} + +function AgenticWorkflowSection({ + children, + headerAction, + iconName, + invalid, + section, +}: { + children: ReactNode + headerAction?: ReactNode + iconName: IconName + invalid?: boolean + section: AgenticWorkflowConfigurationSection +}) { + const { activeSection, setActiveSection } = useAgenticWorkflowCreateContext() + const active = activeSection === section + const completed = isSectionCompleted(section, activeSection) + const showStatus = completed && !active + const title = getSectionTitle(section) + const headerContent = ( + <> +
+ +

+ {title} +

+
+
+ {showStatus && + (invalid ? ( + + ) : ( + + ))} +
+ + ) + + return ( +
+ {active ? ( +
+ {headerContent} + {headerAction &&
{headerAction}
} +
+ ) : ( + + )} + {active &&
{children}
} +
+ ) +} + +function CodeEditorField({ + error, + height = '180px', + hint, + label, + language, + name, + onChange, + placeholder, + value, +}: { + error?: string + height?: string + hint?: ReactNode + label: string + language: string + name: string + onChange: (value: string) => void + placeholder?: string + value: string +}) { + return ( +
+ +
+ {placeholder && !value.trim() && ( +
+ {placeholder} +
+ )} + onChange(nextValue ?? '')} + options={{ + scrollbar: { alwaysConsumeMouseWheel: false }, + scrollBeyondLastLine: false, + wordWrap: 'on', + }} + /> +
+ {hint && !error &&
{hint}
} + {error &&

{error}

} +
+ ) +} + +export function AgenticWorkflowConfiguration() { + const navigate = useNavigate() + const { activeSection, creationFlowUrl, form, setActiveSection, setCurrentStep } = useAgenticWorkflowCreateContext() + const modelApiKeyInputRef = useRef(null) + const whitelistHostsTextareaRef = useRef(null) + const agentPromptTextareaRef = useRef(null) + const values = form.watch() + const { dirtyFields } = form.formState + const connectorErrors = values.connectors.map((connector) => getJsonError(connector.mcpServersJson, true)) + const outputHeadersErrors = values.outputs.map((output) => getJsonError(output.headersJson)) + const modelSettingsJsonError = getJsonError(values.modelSettingsJson, true) + const gitRepositoriesValid = values.gitRepositories.every(isGitRepositoryComplete) + const outputsValid = values.outputs.every(isOutputComplete) + const showNameError = Boolean(dirtyFields.name) && !values.name.trim() + const showModelApiKeyError = Boolean(dirtyFields.modelApiKey) && !values.modelApiKey.trim() + const sectionInvalid: Record = { + 'service-information': !values.name.trim(), + 'ai-model': !values.modelApiKey.trim() || Boolean(modelSettingsJsonError), + connectors: connectorErrors.some(Boolean), + 'git-repositories': !gitRepositoriesValid, + governance: false, + 'docker-fragment': false, + outputs: !outputsValid || outputHeadersErrors.some(Boolean), + 'agent-prompt': !values.agentPrompt.trim(), + } + const isValid = + Boolean(values.name.trim()) && + Boolean(values.modelApiKey.trim()) && + Boolean(values.agentPrompt.trim()) && + gitRepositoriesValid && + outputsValid && + connectorErrors.every((error) => !error) && + outputHeadersErrors.every((error) => !error) && + !modelSettingsJsonError + + useEffect(() => { + setCurrentStep(1) + }, [setCurrentStep]) + + useEffect(() => { + const inputBySection: Partial> = { + 'ai-model': modelApiKeyInputRef.current, + governance: whitelistHostsTextareaRef.current, + 'agent-prompt': agentPromptTextareaRef.current, + } + + window.requestAnimationFrame(() => inputBySection[activeSection]?.focus()) + }, [activeSection]) + + const goToNextSection = () => { + const nextSection = sectionOrder[sectionOrder.indexOf(activeSection) + 1] + + if (nextSection) { + setActiveSection(nextSection) + return + } + + navigate({ to: `${creationFlowUrl}/summary` }) + } + + const addConnector = () => + form.setValue( + 'connectors', + [ + ...values.connectors, + { + mcpServersJson: DEFAULT_CONNECTOR_JSON, + }, + ], + { + shouldDirty: true, + } + ) + + const addRepository = () => + form.setValue( + 'gitRepositories', + [ + ...values.gitRepositories, + { + provider: undefined, + gitTokenId: undefined, + gitTokenName: undefined, + isPublicRepository: false, + repository: '', + gitRepository: undefined, + branch: '', + rootPath: '/', + }, + ], + { shouldDirty: true } + ) + + const addOutput = () => + form.setValue( + 'outputs', + [ + ...values.outputs, + { + url: '', + headersJson: `{ + "Authorization": "Bearer {{TOKEN}}" +}`, + prompt: '', + }, + ], + { + shouldDirty: true, + } + ) + + return ( + +
+

Create agentic workflow

+

+ Configure the inputs, tools, model, governance, and outputs used by your workflow. +

+
+ +
+ + form.setValue('name', event.currentTarget.value, { shouldDirty: true })} + /> + form.setValue('description', event.currentTarget.value, { shouldDirty: true })} + /> +
+ form.setValue('cpu', event.currentTarget.value, { shouldDirty: true })} + /> + form.setValue('memory', event.currentTarget.value, { shouldDirty: true })} + /> + form.setValue('storage', event.currentTarget.value, { shouldDirty: true })} + /> +
+ form.setValue('workflowEnabled', value, { shouldDirty: true })} + /> + +
+ + + + form.setValue('modelApiKey', event.currentTarget.value, { shouldDirty: true })} + /> + + Need help configuring Claude Code settings? Read the{' '} + + Claude Code settings documentation + + . + + } + onChange={(value) => form.setValue('modelSettingsJson', value, { shouldDirty: true })} + /> + + + + + + Add MCP + + } + > + {values.connectors.map((connector, index) => ( +
+
+ MCP {index + 1} + +
+ + + See Claude Code docs for{' '} + + remote HTTP + {' '} + and{' '} + + local stdio + {' '} + servers. + + Example: +
+                      {MCP_CONNECTOR_JSON_EXAMPLE}
+                    
+ + } + onChange={(value) => { + const connectors = [...values.connectors] + connectors[index] = { ...connector, mcpServersJson: value } + form.setValue('connectors', connectors, { shouldDirty: true }) + }} + /> +
+ ))} + +
+ + + + Add repository + + } + > + {values.gitRepositories.map((repository, index) => ( + { + const gitRepositories = [...values.gitRepositories] + gitRepositories[index] = nextRepository + form.setValue('gitRepositories', gitRepositories, { shouldDirty: true }) + }} + onRemove={() => + form.setValue( + 'gitRepositories', + values.gitRepositories.filter((_, repositoryIndex) => repositoryIndex !== index), + { shouldDirty: true } + ) + } + /> + ))} + {!gitRepositoriesValid && ( +

+ Select a Git account, repository, and branch for each repository. +

+ )} + +
+ + + form.setValue('whitelistHosts', event.currentTarget.value, { shouldDirty: true })} + /> + + + + + form.setValue('dockerFragment', value, { shouldDirty: true })} + /> + + + + + + Add webhook + + } + > + {values.outputs.map((output, index) => ( +
+
+ Webhook {index + 1} + +
+
+ { + const outputs = [...values.outputs] + outputs[index] = { ...output, url: event.currentTarget.value } + form.setValue('outputs', outputs, { shouldDirty: true }) + }} + /> + { + const outputs = [...values.outputs] + outputs[index] = { ...output, headersJson: value } + form.setValue('outputs', outputs, { shouldDirty: true }) + }} + /> + { + const outputs = [...values.outputs] + outputs[index] = { ...output, prompt: event.currentTarget.value } + form.setValue('outputs', outputs, { shouldDirty: true }) + }} + /> +
+
+ ))} + {!outputsValid && ( +

+ Enter a webhook URL for each configured output webhook. +

+ )} + +
+ + + form.setValue('agentPrompt', event.currentTarget.value, { shouldDirty: true })} + /> + + +
+ +
+
+ +
+
+
+ ) +} + +function ContinueButton({ disabled, onClick }: { disabled?: boolean; onClick: () => void }) { + return ( + + ) +} diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.spec.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.spec.tsx new file mode 100644 index 00000000000..f1a3e3556a0 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.spec.tsx @@ -0,0 +1,37 @@ +import { renderWithProviders, screen } from '@qovery/shared/util-tests' +import { AgenticWorkflowCreationFlow, useAgenticWorkflowCreateContext } from '../agentic-workflow-context' +import { AIModelCards } from './ai-model-cards' + +function SelectedModel() { + const { form } = useAgenticWorkflowCreateContext() + + return {form.watch('aiModel')} +} + +describe('AIModelCards', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + it('should keep Claude selectable and Bedrock disabled', async () => { + const { userEvent } = renderWithProviders( + + <> + + + + + ) + + expect(screen.getByTestId('selected-model')).toHaveTextContent('Claude') + expect(screen.getByRole('button', { name: /bedrock/i })).toBeDisabled() + + await userEvent.click(screen.getByRole('button', { name: /claude/i })) + + expect(screen.getByTestId('selected-model')).toHaveTextContent('Claude') + }) +}) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.tsx new file mode 100644 index 00000000000..cd2c5f663ec --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.tsx @@ -0,0 +1,39 @@ +import { IconEnum } from '@qovery/shared/enums' +import { Icon } from '@qovery/shared/ui' +import { useAgenticWorkflowCreateContext } from '../agentic-workflow-context' + +export function AIModelCards() { + const { form } = useAgenticWorkflowCreateContext() + const selectedModel = form.watch('aiModel') + + return ( +
+ + +
+ ) +} diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/git-repository-card.spec.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/git-repository-card.spec.tsx new file mode 100644 index 00000000000..4a9c9f9e3a6 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/git-repository-card.spec.tsx @@ -0,0 +1,60 @@ +import { renderWithProviders, screen } from '@qovery/shared/util-tests' +import { GitRepositoryCard } from './git-repository-card' + +jest.mock('@tanstack/react-router', () => ({ + ...jest.requireActual('@tanstack/react-router'), + useParams: () => ({ organizationId: 'org-1' }), +})) + +jest.mock('@qovery/domains/organizations/feature', () => ({ + GitBranchSettings: ({ organizationId, gitTokenId }: { organizationId: string; gitTokenId?: string }) => ( +
+ {organizationId}:{gitTokenId} +
+ ), + GitProviderSetting: ({ organizationId }: { organizationId: string }) => ( +
{organizationId}
+ ), + GitRepositorySetting: ({ organizationId, gitTokenId }: { organizationId: string; gitTokenId?: string }) => ( +
+ {organizationId}:{gitTokenId} +
+ ), +})) + +describe('GitRepositoryCard', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + it('should render repository settings and remove the repository', async () => { + const onRemove = jest.fn() + const { userEvent } = renderWithProviders( + + ) + + expect(screen.getByText('Repository 1')).toBeInTheDocument() + expect(screen.getByTestId('git-provider-setting')).toHaveTextContent('org-1') + expect(screen.getByTestId('git-repository-setting')).toHaveTextContent('org-1:token-1') + expect(screen.getByTestId('git-branch-settings')).toHaveTextContent('org-1:token-1') + + await userEvent.click(screen.getByRole('button', { name: 'Remove' })) + + expect(onRemove).toHaveBeenCalledTimes(1) + }) +}) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/git-repository-card.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/git-repository-card.tsx new file mode 100644 index 00000000000..1553b141bf4 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/git-repository-card.tsx @@ -0,0 +1,96 @@ +import { useParams } from '@tanstack/react-router' +import { type GitProviderEnum, type GitRepository } from 'qovery-typescript-axios' +import { useEffect } from 'react' +import { FormProvider, useForm } from 'react-hook-form' +import { GitBranchSettings, GitProviderSetting, GitRepositorySetting } from '@qovery/domains/organizations/feature' +import { Button } from '@qovery/shared/ui' +import { type AgenticWorkflowGitRepository } from '../agentic-workflow-context' + +export function GitRepositoryCard({ + index, + onChange, + onRemove, + repository, +}: { + index: number + onChange: (repository: AgenticWorkflowGitRepository) => void + onRemove: () => void + repository: AgenticWorkflowGitRepository +}) { + const { organizationId = '' } = useParams({ strict: false }) + const methods = useForm<{ + provider?: keyof typeof GitProviderEnum | string | null + is_public_repository?: boolean + repository: string + branch: string + root_path: string + git_token_name?: string | null + git_token_id?: string | null + git_repository?: GitRepository + }>({ + defaultValues: { + provider: repository.provider, + is_public_repository: repository.isPublicRepository, + repository: repository.repository, + branch: repository.branch, + root_path: repository.rootPath || '/', + git_token_name: repository.gitTokenName, + git_token_id: repository.gitTokenId, + git_repository: repository.gitRepository, + }, + mode: 'onChange', + }) + const provider = methods.watch('provider') as keyof typeof GitProviderEnum | undefined + const watchedRepository = methods.watch('repository') + const gitTokenId = methods.watch('git_token_id') ?? undefined + const isPublicRepository = methods.watch('is_public_repository') + + useEffect(() => { + const subscription = methods.watch((values) => { + onChange({ + provider: values.provider, + gitTokenId: values.git_token_id, + gitTokenName: values.git_token_name, + isPublicRepository: values.is_public_repository, + repository: values.repository ?? '', + gitRepository: values.git_repository as GitRepository | undefined, + branch: values.branch ?? '', + rootPath: values.root_path ?? '/', + }) + }) + + return () => subscription.unsubscribe() + }, [methods, onChange]) + + return ( +
+
+ Repository {index + 1} + +
+ +
+ + {isPublicRepository ? null : ( + <> + {provider && ( + + )} + {provider && watchedRepository && ( + + )} + + )} +
+
+
+ ) +} diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/index.ts b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/index.ts new file mode 100644 index 00000000000..c02f5d22ac0 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/index.ts @@ -0,0 +1 @@ +export * from './agentic-workflow-configuration' diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.spec.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.spec.tsx new file mode 100644 index 00000000000..9bb1252a8f3 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.spec.tsx @@ -0,0 +1,33 @@ +import { renderWithProviders, screen } from '@qovery/shared/util-tests' +import { AgenticWorkflowCreationFlow, useAgenticWorkflowCreateContext } from './agentic-workflow-context' + +function FormDefaults() { + const { form } = useAgenticWorkflowCreateContext() + const values = form.getValues() + + return ( + <> + {values.cpu} + {values.memory} + {values.storage} + {values.whitelistHosts} + {String(values.workflowEnabled)} + + ) +} + +describe('AgenticWorkflowCreationFlow', () => { + it('should initialize the form with MVP defaults', () => { + renderWithProviders( + + + + ) + + expect(screen.getByTestId('cpu')).toHaveTextContent('2000') + expect(screen.getByTestId('memory')).toHaveTextContent('2048') + expect(screen.getByTestId('storage')).toHaveTextContent('10') + expect(screen.getByTestId('whitelist-hosts')).toHaveTextContent('*') + expect(screen.getByTestId('workflow-enabled')).toHaveTextContent('true') + }) +}) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx new file mode 100644 index 00000000000..cbca1b22759 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx @@ -0,0 +1,115 @@ +import { useLayoutEffect, useState } from 'react' +import { renderWithProviders, screen } from '@qovery/shared/util-tests' +import { AgenticWorkflowCreationFlow, type AgenticWorkflowFormData, useAgenticWorkflowCreateContext } from './agentic-workflow-context' +import { AgenticWorkflowSummary } from './agentic-workflow-summary' + +const mockNavigate = jest.fn() +const mockCreateAgenticWorkflow = jest.fn() + +jest.mock('@tanstack/react-router', () => ({ + ...jest.requireActual('@tanstack/react-router'), + useNavigate: () => mockNavigate, + useParams: () => ({ + organizationId: 'org-1', + projectId: 'project-1', + environmentId: 'environment-1', + }), +})) + +jest.mock('posthog-js', () => ({ + capture: jest.fn(), +})) + +jest.mock('./hooks/use-create-agentic-workflow', () => ({ + ...jest.requireActual('./hooks/use-create-agentic-workflow'), + useCreateAgenticWorkflow: () => ({ + isLoading: false, + mutateAsync: mockCreateAgenticWorkflow, + }), +})) + +const validValues: Partial = { + name: 'review-agent', + description: 'Reviews incoming changes', + modelApiKey: 'sk-ant-test', + agentPrompt: 'Review payloads and open pull requests.', + whitelistHosts: '*', + outputs: [{ url: 'https://hooks.example.com/workflow', headersJson: '{}', prompt: 'Notify the team.' }], +} + +function WithFormValues({ children, values = validValues }: { children: JSX.Element; values?: Partial }) { + const { form } = useAgenticWorkflowCreateContext() + const [ready, setReady] = useState(false) + + useLayoutEffect(() => { + form.reset({ + ...form.getValues(), + ...values, + }) + setReady(true) + }, [form, values]) + + if (!ready) { + return null + } + + return children +} + +function renderSummary(values?: Partial) { + return renderWithProviders( + + + + + + ) +} + +describe('AgenticWorkflowSummary', () => { + beforeEach(() => { + jest.useFakeTimers() + jest.clearAllMocks() + mockCreateAgenticWorkflow.mockResolvedValue({ id: 'workflow-1' }) + }) + + afterEach(() => { + jest.useRealTimers() + }) + + it('should render configured values from the configuration form', () => { + renderSummary() + + expect(screen.getByText('review-agent')).toBeInTheDocument() + expect(screen.getByText('Reviews incoming changes')).toBeInTheDocument() + expect(screen.getByText('********')).toBeInTheDocument() + expect(screen.getByText('*')).toBeInTheDocument() + expect(screen.getByText('1 webhook')).toBeInTheDocument() + }) + + it('should navigate back to the edited section from summary', async () => { + const { userEvent } = renderSummary() + + await userEvent.click(screen.getByRole('button', { name: 'Edit AI model' })) + + expect(mockNavigate).toHaveBeenCalledWith({ to: '/create/agentic-workflow/configuration' }) + }) + + it('should create the agentic workflow and navigate back to the environment overview', async () => { + const { userEvent } = renderSummary() + + await userEvent.click(screen.getByRole('button', { name: 'Create' })) + + expect(mockCreateAgenticWorkflow).toHaveBeenCalledWith({ + environmentId: 'environment-1', + payload: expect.objectContaining({ + name: 'review-agent', + outputs: [expect.objectContaining({ url: 'https://hooks.example.com/workflow' })], + }), + }) + expect(mockNavigate).toHaveBeenCalledWith({ + to: '/organization/$organizationId/project/$projectId/environment/$environmentId/overview', + params: { organizationId: 'org-1', projectId: 'project-1', environmentId: 'environment-1' }, + }) + }) +}) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx index 755fecf6646..80407baa074 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx @@ -8,7 +8,7 @@ import { type AgenticWorkflowFormData, useAgenticWorkflowCreateContext, } from './agentic-workflow-context' -import { formatAgenticWorkflowRequest, useCreateAgenticWorkflow } from './use-create-agentic-workflow' +import { formatAgenticWorkflowRequest, useCreateAgenticWorkflow } from './hooks/use-create-agentic-workflow' function truncateSummary(value: string) { if (!value.trim()) return '-' diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/index.ts b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/index.ts new file mode 100644 index 00000000000..b047f10b40f --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/index.ts @@ -0,0 +1 @@ +export * from './use-create-agentic-workflow' diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/use-create-agentic-workflow.spec.ts b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/use-create-agentic-workflow.spec.ts new file mode 100644 index 00000000000..d5a26a308cd --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/use-create-agentic-workflow.spec.ts @@ -0,0 +1,96 @@ +import { AgenticWorkflowModel } from 'qovery-typescript-axios' +import { type AgenticWorkflowFormData } from '../../agentic-workflow-context' +import { formatAgenticWorkflowRequest } from './use-create-agentic-workflow' + +const formValues: AgenticWorkflowFormData = { + name: 'review-agent', + description: 'Reviews incoming changes', + cpu: '2000', + memory: '2048', + storage: '10', + workflowEnabled: false, + aiModel: 'Claude', + webhookEnabled: true, + connectors: [ + { + mcpServersJson: '{"mcpServers":{"github":{"type":"http","url":"https://api.githubcopilot.com/mcp/"}}}', + }, + ], + gitRepositories: [ + { + provider: 'GITHUB', + gitTokenId: 'token-1', + gitTokenName: 'GitHub', + repository: 'https://github.com/qovery/console', + branch: 'main', + rootPath: '/apps/console', + }, + ], + modelApiKey: 'sk-ant-test', + modelSettingsJson: '{"provider":"anthropic","models":[{"name":"claude-sonnet-4"}]}', + whitelistHosts: 'api.github.com, jira.company.com', + dockerFragment: 'RUN npm install -g @modelcontextprotocol/server-github', + outputs: [ + { + url: 'https://hooks.slack.com/services/team/channel/token', + headersJson: '{"Authorization":"Bearer {{SLACK_TOKEN}}","Ignored":42}', + prompt: 'Send a summary.', + }, + ], + agentPrompt: 'Review the payload and open a PR when needed.', +} + +describe('formatAgenticWorkflowRequest', () => { + it('should map the creation form to the API payload', () => { + expect(formatAgenticWorkflowRequest(formValues)).toEqual({ + name: 'review-agent', + description: 'Reviews incoming changes', + whitelist_hosts: ['api.github.com', 'jira.company.com'], + model_settings: '{"provider":"anthropic","models":[{"name":"claude-sonnet-4"}]}', + docker_fragment: 'RUN npm install -g @modelcontextprotocol/server-github', + enabled: false, + mcp_connectors: [ + { + name: 'MCP 1', + url: '', + headers: [], + instructions: '{"mcpServers":{"github":{"type":"http","url":"https://api.githubcopilot.com/mcp/"}}}', + }, + ], + outputs: [ + { + name: 'Output 1', + url: 'https://hooks.slack.com/services/team/channel/token', + headers: [{ name: 'Authorization', value: 'Bearer {{SLACK_TOKEN}}' }], + instructions: 'Send a summary.', + }, + ], + model: AgenticWorkflowModel.CLAUDE, + project_repositories: [ + { + url: 'https://github.com/qovery/console', + branch: 'main', + root_path: '/apps/console', + git_token_id: 'token-1', + }, + ], + }) + }) + + it('should fallback to the repository root path and Bedrock model when selected', () => { + expect( + formatAgenticWorkflowRequest({ + ...formValues, + aiModel: 'Bedrock', + whitelistHosts: ' * ', + outputs: [{ url: 'https://example.com/webhook', headersJson: '', prompt: '' }], + gitRepositories: [{ ...formValues.gitRepositories[0], rootPath: '' }], + }) + ).toMatchObject({ + whitelist_hosts: ['*'], + model: AgenticWorkflowModel.BEDROCK, + outputs: [{ headers: [] }], + project_repositories: [{ root_path: '/' }], + }) + }) +}) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow.ts b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/use-create-agentic-workflow.ts similarity index 96% rename from libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow.ts rename to libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/use-create-agentic-workflow.ts index de054700c07..02848009ff0 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/use-create-agentic-workflow.ts +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/use-create-agentic-workflow.ts @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import { type AgenticWorkflowHeader, AgenticWorkflowModel, type AgenticWorkflowRequest } from 'qovery-typescript-axios' import { mutations } from '@qovery/domains/services/data-access' import { queries } from '@qovery/state/util-queries' -import { type AgenticWorkflowFormData } from './agentic-workflow-context' +import { type AgenticWorkflowFormData } from '../../agentic-workflow-context' function formatWhitelistHosts(value: string) { return value From ca1a98e2028728dd17404eb71b7b7e153ae17ad7 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Mon, 20 Jul 2026 11:17:29 +0200 Subject: [PATCH 09/14] chore(services): simplify agentic workflow exports --- libs/domains/services/feature/src/index.ts | 4 +- .../agentic-workflow-configuration.tsx | 1 - .../agentic-workflow-configuration/index.ts | 1 - .../agentic-workflow-summary.spec.tsx | 4 +- .../agentic-workflow-summary.tsx | 2 +- .../use-create-agentic-workflow/index.ts | 1 - .../use-create-agentic-workflow.spec.ts | 96 ------------------- 7 files changed, 5 insertions(+), 104 deletions(-) delete mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx delete mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/index.ts delete mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/index.ts delete mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/use-create-agentic-workflow.spec.ts diff --git a/libs/domains/services/feature/src/index.ts b/libs/domains/services/feature/src/index.ts index dd85e3886f9..b68d800324c 100644 --- a/libs/domains/services/feature/src/index.ts +++ b/libs/domains/services/feature/src/index.ts @@ -107,9 +107,9 @@ export * from './lib/service-creation-flow/database/step-summary/step-summary' export * from './lib/service-creation-flow/blueprint/blueprint-creation-flow' export * from './lib/service-update-flow/blueprint/blueprint-update-flow' export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-context' -export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration' +export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration' export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-summary' -export * from './lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow' +export * from './lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/use-create-agentic-workflow' export * from './lib/application-container-healthchecks/application-container-healthchecks-form/application-container-healthchecks-form' export * from './lib/application-container-healthchecks/healthchecks-utils' export * from './lib/application-container-healthchecks/step-healthchecks/step-healthchecks' diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx deleted file mode 100644 index e131edd6eb6..00000000000 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './agentic-workflow-configuration/agentic-workflow-configuration' diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/index.ts b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/index.ts deleted file mode 100644 index c02f5d22ac0..00000000000 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './agentic-workflow-configuration' diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx index cbca1b22759..dde8acac6dc 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx @@ -20,8 +20,8 @@ jest.mock('posthog-js', () => ({ capture: jest.fn(), })) -jest.mock('./hooks/use-create-agentic-workflow', () => ({ - ...jest.requireActual('./hooks/use-create-agentic-workflow'), +jest.mock('./hooks/use-create-agentic-workflow/use-create-agentic-workflow', () => ({ + ...jest.requireActual('./hooks/use-create-agentic-workflow/use-create-agentic-workflow'), useCreateAgenticWorkflow: () => ({ isLoading: false, mutateAsync: mockCreateAgenticWorkflow, diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx index 80407baa074..36b29eec180 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx @@ -8,7 +8,7 @@ import { type AgenticWorkflowFormData, useAgenticWorkflowCreateContext, } from './agentic-workflow-context' -import { formatAgenticWorkflowRequest, useCreateAgenticWorkflow } from './hooks/use-create-agentic-workflow' +import { formatAgenticWorkflowRequest, useCreateAgenticWorkflow } from './hooks/use-create-agentic-workflow/use-create-agentic-workflow' function truncateSummary(value: string) { if (!value.trim()) return '-' diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/index.ts b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/index.ts deleted file mode 100644 index b047f10b40f..00000000000 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './use-create-agentic-workflow' diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/use-create-agentic-workflow.spec.ts b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/use-create-agentic-workflow.spec.ts deleted file mode 100644 index d5a26a308cd..00000000000 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/hooks/use-create-agentic-workflow/use-create-agentic-workflow.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { AgenticWorkflowModel } from 'qovery-typescript-axios' -import { type AgenticWorkflowFormData } from '../../agentic-workflow-context' -import { formatAgenticWorkflowRequest } from './use-create-agentic-workflow' - -const formValues: AgenticWorkflowFormData = { - name: 'review-agent', - description: 'Reviews incoming changes', - cpu: '2000', - memory: '2048', - storage: '10', - workflowEnabled: false, - aiModel: 'Claude', - webhookEnabled: true, - connectors: [ - { - mcpServersJson: '{"mcpServers":{"github":{"type":"http","url":"https://api.githubcopilot.com/mcp/"}}}', - }, - ], - gitRepositories: [ - { - provider: 'GITHUB', - gitTokenId: 'token-1', - gitTokenName: 'GitHub', - repository: 'https://github.com/qovery/console', - branch: 'main', - rootPath: '/apps/console', - }, - ], - modelApiKey: 'sk-ant-test', - modelSettingsJson: '{"provider":"anthropic","models":[{"name":"claude-sonnet-4"}]}', - whitelistHosts: 'api.github.com, jira.company.com', - dockerFragment: 'RUN npm install -g @modelcontextprotocol/server-github', - outputs: [ - { - url: 'https://hooks.slack.com/services/team/channel/token', - headersJson: '{"Authorization":"Bearer {{SLACK_TOKEN}}","Ignored":42}', - prompt: 'Send a summary.', - }, - ], - agentPrompt: 'Review the payload and open a PR when needed.', -} - -describe('formatAgenticWorkflowRequest', () => { - it('should map the creation form to the API payload', () => { - expect(formatAgenticWorkflowRequest(formValues)).toEqual({ - name: 'review-agent', - description: 'Reviews incoming changes', - whitelist_hosts: ['api.github.com', 'jira.company.com'], - model_settings: '{"provider":"anthropic","models":[{"name":"claude-sonnet-4"}]}', - docker_fragment: 'RUN npm install -g @modelcontextprotocol/server-github', - enabled: false, - mcp_connectors: [ - { - name: 'MCP 1', - url: '', - headers: [], - instructions: '{"mcpServers":{"github":{"type":"http","url":"https://api.githubcopilot.com/mcp/"}}}', - }, - ], - outputs: [ - { - name: 'Output 1', - url: 'https://hooks.slack.com/services/team/channel/token', - headers: [{ name: 'Authorization', value: 'Bearer {{SLACK_TOKEN}}' }], - instructions: 'Send a summary.', - }, - ], - model: AgenticWorkflowModel.CLAUDE, - project_repositories: [ - { - url: 'https://github.com/qovery/console', - branch: 'main', - root_path: '/apps/console', - git_token_id: 'token-1', - }, - ], - }) - }) - - it('should fallback to the repository root path and Bedrock model when selected', () => { - expect( - formatAgenticWorkflowRequest({ - ...formValues, - aiModel: 'Bedrock', - whitelistHosts: ' * ', - outputs: [{ url: 'https://example.com/webhook', headersJson: '', prompt: '' }], - gitRepositories: [{ ...formValues.gitRepositories[0], rootPath: '' }], - }) - ).toMatchObject({ - whitelist_hosts: ['*'], - model: AgenticWorkflowModel.BEDROCK, - outputs: [{ headers: [] }], - project_repositories: [{ root_path: '/' }], - }) - }) -}) From f4a2a68896f6812b53d23606f0788b2be177cf22 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Mon, 20 Jul 2026 12:30:04 +0200 Subject: [PATCH 10/14] chore(services): format agentic workflow files --- .../agentic-workflow-configuration.tsx | 5 ++++- .../agentic-workflow-summary.spec.tsx | 14 ++++++++++++-- .../agentic-workflow/agentic-workflow-summary.tsx | 5 ++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx index 1de17637610..de7f2b11d55 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx @@ -59,7 +59,10 @@ function getSectionTitle(section: AgenticWorkflowConfigurationSection) { export function isGitRepositoryComplete(repository: AgenticWorkflowGitRepository) { return Boolean( - repository.gitTokenId && repository.repository.trim() && repository.branch.trim() && (repository.rootPath || '/').trim() + repository.gitTokenId && + repository.repository.trim() && + repository.branch.trim() && + (repository.rootPath || '/').trim() ) } diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx index dde8acac6dc..38f5301d3fe 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx @@ -1,6 +1,10 @@ import { useLayoutEffect, useState } from 'react' import { renderWithProviders, screen } from '@qovery/shared/util-tests' -import { AgenticWorkflowCreationFlow, type AgenticWorkflowFormData, useAgenticWorkflowCreateContext } from './agentic-workflow-context' +import { + AgenticWorkflowCreationFlow, + type AgenticWorkflowFormData, + useAgenticWorkflowCreateContext, +} from './agentic-workflow-context' import { AgenticWorkflowSummary } from './agentic-workflow-summary' const mockNavigate = jest.fn() @@ -37,7 +41,13 @@ const validValues: Partial = { outputs: [{ url: 'https://hooks.example.com/workflow', headersJson: '{}', prompt: 'Notify the team.' }], } -function WithFormValues({ children, values = validValues }: { children: JSX.Element; values?: Partial }) { +function WithFormValues({ + children, + values = validValues, +}: { + children: JSX.Element + values?: Partial +}) { const { form } = useAgenticWorkflowCreateContext() const [ready, setReady] = useState(false) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx index 36b29eec180..45977f02c4f 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx @@ -8,7 +8,10 @@ import { type AgenticWorkflowFormData, useAgenticWorkflowCreateContext, } from './agentic-workflow-context' -import { formatAgenticWorkflowRequest, useCreateAgenticWorkflow } from './hooks/use-create-agentic-workflow/use-create-agentic-workflow' +import { + formatAgenticWorkflowRequest, + useCreateAgenticWorkflow, +} from './hooks/use-create-agentic-workflow/use-create-agentic-workflow' function truncateSummary(value: string) { if (!value.trim()) return '-' From 5df8b836b66c3c6d76990b4a5f4a54c4bcd74a83 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Mon, 20 Jul 2026 13:48:07 +0200 Subject: [PATCH 11/14] feat(services): align agentic workflow api payload --- .../agentic-workflow-configuration.spec.tsx | 8 +- .../agentic-workflow-configuration.tsx | 146 +++++------------- .../ai-model-cards.spec.tsx | 4 +- .../ai-model-cards.tsx | 5 +- .../git-repository-card.spec.tsx | 1 - .../git-repository-card.tsx | 6 +- .../agentic-workflow-context.tsx | 17 +- .../agentic-workflow-summary.spec.tsx | 14 +- .../agentic-workflow-summary.tsx | 7 +- .../use-create-agentic-workflow.ts | 22 +-- 10 files changed, 82 insertions(+), 148 deletions(-) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.spec.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.spec.tsx index 52fad832c03..9c17beb8f0a 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.spec.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.spec.tsx @@ -8,22 +8,20 @@ describe('AgenticWorkflowConfiguration validation', () => { expect(getJsonError('{invalid', true)).toBe('Invalid JSON format.') }) - it('should require token, repository, branch, and root path for configured repositories', () => { + it('should require token, repository, and branch for configured repositories', () => { expect( isGitRepositoryComplete({ - gitTokenId: 'token-1', + provider: 'GITHUB', repository: 'https://github.com/qovery/console', branch: 'main', - rootPath: '/', }) ).toBe(true) expect( isGitRepositoryComplete({ - gitTokenId: 'token-1', + provider: 'GITHUB', repository: 'https://github.com/qovery/console', branch: '', - rootPath: '/', }) ).toBe(false) }) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx index de7f2b11d55..a1eeb6d3e6d 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx @@ -6,7 +6,6 @@ import { type AgenticWorkflowConfigurationSection, type AgenticWorkflowGitRepository, type AgenticWorkflowOutput, - DEFAULT_CONNECTOR_JSON, MCP_CONNECTOR_JSON_EXAMPLE, useAgenticWorkflowCreateContext, } from '../agentic-workflow-context' @@ -59,10 +58,9 @@ function getSectionTitle(section: AgenticWorkflowConfigurationSection) { export function isGitRepositoryComplete(repository: AgenticWorkflowGitRepository) { return Boolean( - repository.gitTokenId && + (repository.gitTokenId || repository.provider || repository.isPublicRepository) && repository.repository.trim() && - repository.branch.trim() && - (repository.rootPath || '/').trim() + repository.branch.trim() ) } @@ -180,7 +178,7 @@ function CodeEditorField({ }} />
- {hint && !error &&
{hint}
} + {hint && !error &&
{hint}
} {error &&

{error}

}
) @@ -194,7 +192,7 @@ export function AgenticWorkflowConfiguration() { const agentPromptTextareaRef = useRef(null) const values = form.watch() const { dirtyFields } = form.formState - const connectorErrors = values.connectors.map((connector) => getJsonError(connector.mcpServersJson, true)) + const mcpJsonError = getJsonError(values.mcpJson) const outputHeadersErrors = values.outputs.map((output) => getJsonError(output.headersJson)) const modelSettingsJsonError = getJsonError(values.modelSettingsJson, true) const gitRepositoriesValid = values.gitRepositories.every(isGitRepositoryComplete) @@ -204,7 +202,7 @@ export function AgenticWorkflowConfiguration() { const sectionInvalid: Record = { 'service-information': !values.name.trim(), 'ai-model': !values.modelApiKey.trim() || Boolean(modelSettingsJsonError), - connectors: connectorErrors.some(Boolean), + connectors: Boolean(mcpJsonError), 'git-repositories': !gitRepositoriesValid, governance: false, 'docker-fragment': false, @@ -217,7 +215,7 @@ export function AgenticWorkflowConfiguration() { Boolean(values.agentPrompt.trim()) && gitRepositoriesValid && outputsValid && - connectorErrors.every((error) => !error) && + !mcpJsonError && outputHeadersErrors.every((error) => !error) && !modelSettingsJsonError @@ -246,20 +244,6 @@ export function AgenticWorkflowConfiguration() { navigate({ to: `${creationFlowUrl}/summary` }) } - const addConnector = () => - form.setValue( - 'connectors', - [ - ...values.connectors, - { - mcpServersJson: DEFAULT_CONNECTOR_JSON, - }, - ], - { - shouldDirty: true, - } - ) - const addRepository = () => form.setValue( 'gitRepositories', @@ -273,7 +257,6 @@ export function AgenticWorkflowConfiguration() { repository: '', gitRepository: undefined, branch: '', - rootPath: '/', }, ], { shouldDirty: true } @@ -400,88 +383,41 @@ export function AgenticWorkflowConfiguration() { /> - - - Add MCP - - } - > - {values.connectors.map((connector, index) => ( -
-
- MCP {index + 1} - -
- - - See Claude Code docs for{' '} - - remote HTTP - {' '} - and{' '} - - local stdio - {' '} - servers. - - Example: -
-                      {MCP_CONNECTOR_JSON_EXAMPLE}
-                    
- - } - onChange={(value) => { - const connectors = [...values.connectors] - connectors[index] = { ...connector, mcpServersJson: value } - form.setValue('connectors', connectors, { shouldDirty: true }) - }} - /> -
- ))} - + + + + See Claude Code docs for{' '} + + remote HTTP + {' '} + and{' '} + + local stdio + {' '} + servers. + + + } + onChange={(value) => form.setValue('mcpJson', value, { shouldDirty: true })} + /> + { ) - expect(screen.getByTestId('selected-model')).toHaveTextContent('Claude') + expect(screen.getByTestId('selected-model')).toHaveTextContent('CLAUDE') expect(screen.getByRole('button', { name: /bedrock/i })).toBeDisabled() await userEvent.click(screen.getByRole('button', { name: /claude/i })) - expect(screen.getByTestId('selected-model')).toHaveTextContent('Claude') + expect(screen.getByTestId('selected-model')).toHaveTextContent('CLAUDE') }) }) diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.tsx index cd2c5f663ec..acb8eedccb9 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.tsx @@ -1,3 +1,4 @@ +import { AgenticWorkflowModelType } from 'qovery-typescript-axios' import { IconEnum } from '@qovery/shared/enums' import { Icon } from '@qovery/shared/ui' import { useAgenticWorkflowCreateContext } from '../agentic-workflow-context' @@ -11,11 +12,11 @@ export function AIModelCards() {