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/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 c461f4b9f7e..cc739b490b9 100644 --- a/libs/domains/services/feature/src/index.ts +++ b/libs/domains/services/feature/src/index.ts @@ -106,6 +106,10 @@ 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/agentic-workflow-configuration' +export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-summary' +export * from './lib/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/hooks/use-create-agentic-workflow/use-create-agentic-workflow.ts b/libs/domains/services/feature/src/lib/hooks/use-create-agentic-workflow/use-create-agentic-workflow.ts new file mode 100644 index 00000000000..b85204e7e20 --- /dev/null +++ b/libs/domains/services/feature/src/lib/hooks/use-create-agentic-workflow/use-create-agentic-workflow.ts @@ -0,0 +1,76 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { type AgenticWorkflowHeader, 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 '../../service-creation-flow/agentic-workflow/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 [] + } + + 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 })) +} + +export function formatAgenticWorkflowRequest(values: AgenticWorkflowFormData): AgenticWorkflowRequest { + return { + name: values.name, + description: values.description, + docker_fragment: values.dockerFragment, + enabled: values.workflowEnabled, + mcp: values.mcpJson.trim() || undefined, + outputs: values.outputs.map((output, index) => ({ + name: `Output ${index + 1}`, + url: output.url, + headers: parseHeaders(output.headersJson), + instructions: output.prompt, + })), + model: { + type: values.aiModel, + api_key: values.modelApiKey, + settings: values.modelSettingsJson, + }, + project_repositories: values.gitRepositories.map((repository) => ({ + url: repository.repository, + branch: repository.branch, + git_token_id: repository.gitTokenId ?? '', + })), + agent_prompt: values.agentPrompt, + governance: { + host_allowlist: formatWhitelistHosts(values.whitelistHosts), + }, + } +} + +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, + }, + }) +} 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..9c17beb8f0a --- /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,33 @@ +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, and branch for configured repositories', () => { + expect( + isGitRepositoryComplete({ + provider: 'GITHUB', + repository: 'https://github.com/qovery/console', + branch: 'main', + }) + ).toBe(true) + + expect( + isGitRepositoryComplete({ + provider: 'GITHUB', + repository: 'https://github.com/qovery/console', + branch: '', + }) + ).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..fa4db11099b --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx @@ -0,0 +1,616 @@ +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, + 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.provider || repository.isPublicRepository) && + repository.repository.trim() && + repository.branch.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 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) + 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: Boolean(mcpJsonError), + '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 && + !mcpJsonError && + 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 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, + { + 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 })} + /> + + + + + + See Claude Code docs for{' '} + + remote HTTP + {' '} + and{' '} + + local stdio + {' '} + servers. + + } + onChange={(value) => form.setValue('mcpJson', value, { 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..1a34a2889b0 --- /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..acb8eedccb9 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.tsx @@ -0,0 +1,40 @@ +import { AgenticWorkflowModelType } from 'qovery-typescript-axios' +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..3ea02939d86 --- /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,59 @@ +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..00e6d11709a --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/git-repository-card.tsx @@ -0,0 +1,92 @@ +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 + 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 && ( + + )} + + )} +
+
+
+ ) +} 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-context.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.tsx new file mode 100644 index 00000000000..43959558c55 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.tsx @@ -0,0 +1,172 @@ +import { AgenticWorkflowModelType, 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", + "models": [ + { + "name": "claude-sonnet-4" + }, + { + "name": "claude-haiku-4" + } + ], + "extendedThinking": false, + "tools": [ + "Agent", + "Bash", + "Edit", + "Glob", + "Grep", + "LSP", + "NotebookEdit", + "Read", + "Skill", + "TaskCreate", + "TaskGet", + "TaskList", + "TaskUpdate", + "TodoWrite", + "WebFetch", + "WebSearch", + "Write" + ] +}` + +export const MCP_CONNECTOR_JSON_EXAMPLE = `{ + "mcpServers": { + "investigator": { + "type": "stdio", + "command": "target/debug/mcp_server_example", + "args": [], + "env": { + "INVESTIGATOR_TOKEN": "{{INVESTIGATOR_TOKEN}}" + } + }, + "costory": { + "type": "http", + "url": "https://app-api.costory.io/mcp", + "headers": { + "Authorization": "Bearer {{COSTORY_TOKEN}}" + } + } + } +}` + +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 { + url: string + headersJson: 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 AgenticWorkflowFormData { + name: string + description: string + cpu: string + memory: string + storage: string + workflowEnabled: boolean + aiModel: AgenticWorkflowModelType + webhookEnabled: boolean + mcpJson: string + gitRepositories: AgenticWorkflowGitRepository[] + modelApiKey: string + modelSettingsJson: string + whitelistHosts: 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: '', + cpu: '2000', + memory: '2048', + storage: '10', + workflowEnabled: true, + aiModel: AgenticWorkflowModelType.CLAUDE, + webhookEnabled: true, + mcpJson: MCP_CONNECTOR_JSON_EXAMPLE, + gitRepositories: [], + modelApiKey: '', + modelSettingsJson: DEFAULT_MODEL_SETTINGS, + whitelistHosts: '*', + dockerFragment: '', + outputs: [], + agentPrompt: '', + }, + mode: 'onChange', + }) + + return ( + + + + {children} + + + + ) +} 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..2e5d0256e86 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx @@ -0,0 +1,137 @@ +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/use-create-agentic-workflow', () => ({ + ...jest.requireActual('../../hooks/use-create-agentic-workflow/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: '*', + mcpJson: '{"mcpServers":{"costory":{"type":"http","url":"https://app-api.costory.io/mcp"}}}', + 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({ + ...validValues, + gitRepositories: [ + { + provider: 'GITHUB', + repository: 'Qovery/console', + branch: 'staging', + }, + ], + }) + + expect(screen.getByText('review-agent')).toBeInTheDocument() + expect(screen.getByText('Reviews incoming changes')).toBeInTheDocument() + expect(screen.getByText('********')).toBeInTheDocument() + expect(screen.getByText('*')).toBeInTheDocument() + expect(screen.getByText(validValues.mcpJson ?? '')).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', + mcp: validValues.mcpJson, + 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 new file mode 100644 index 00000000000..6c2dd24134c --- /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 } from 'react' +import { Button, FunnelFlowBody, Heading, Icon, Section, SummaryValue, truncateText } from '@qovery/shared/ui' +import { pluralize } from '@qovery/shared/util-js' +import { + formatAgenticWorkflowRequest, + useCreateAgenticWorkflow, +} from '../../hooks/use-create-agentic-workflow/use-create-agentic-workflow' +import { isGitRepositoryComplete } from './agentic-workflow-configuration/agentic-workflow-configuration' +import { + type AgenticWorkflowConfigurationSection, + type AgenticWorkflowFormData, + useAgenticWorkflowCreateContext, +} from './agentic-workflow-context' + +function truncateSummary(value: string) { + if (!value.trim()) return '-' + + return value.length > 160 ? `${truncateText(value, 157)}…` : value +} + +function formatCount(count: number, singular: string) { + if (count === 0) return 'None' + + return `${count} ${pluralize(count, singular)}` +} + +function maskValue(value: string) { + return value.trim() ? '********' : '-' +} + +function hasIncompleteGitRepository(values: AgenticWorkflowFormData) { + return values.gitRepositories.some((repository) => !isGitRepositoryComplete(repository)) +} + +function hasIncompleteOutput(values: AgenticWorkflowFormData) { + 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 }) { + return ( +
+
+ {title} + +
+
    {children}
+
+ ) +} + +export function AgenticWorkflowSummary() { + const navigate = useNavigate() + const { environmentId = '', organizationId = '', projectId = '' } = useParams({ strict: false }) + const { creationFlowUrl, form, setActiveSection, setCurrentStep } = useAgenticWorkflowCreateContext() + const { isLoading: isCreating, mutateAsync: createAgenticWorkflow } = useCreateAgenticWorkflow({ environmentId }) + 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 = async () => { + await createAgenticWorkflow({ + environmentId, + payload: formatAgenticWorkflowRequest(form.getValues()), + }) + posthog.capture('create-service', { + selectedServiceType: 'agentic-workflow', + }) + 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-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('')