diff --git a/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview/route.tsx b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview/route.tsx index d99c028b8b6..57a2a628f12 100644 --- a/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview/route.tsx +++ b/apps/console/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview/route.tsx @@ -2,6 +2,7 @@ import { type IconName } from '@fortawesome/fontawesome-common-types' import { Outlet, createFileRoute, useMatchRoute } from '@tanstack/react-router' import { Link as RouterLink } from '@tanstack/react-router' import posthog from 'posthog-js' +import { useFeatureFlagEnabled } from 'posthog-js/react' import { useEffect, useMemo } from 'react' import { ClusterAvatar, @@ -22,7 +23,7 @@ import { } from '@qovery/domains/environments/feature' import { useEnvironmentsOverview } from '@qovery/domains/projects/feature' import { isArgoCd, isEditableService } from '@qovery/domains/services/data-access' -import { ArgoCdServiceList, useServices } from '@qovery/domains/services/feature' +import { AgenticWorkflowServiceList, ArgoCdServiceList, useServices } from '@qovery/domains/services/feature' import { Heading, Icon, Link, Navbar, Section, Tooltip } from '@qovery/shared/ui' export const Route = createFileRoute( @@ -44,6 +45,7 @@ function RouteComponent() { const { data: deploymentStatus } = useDeploymentStatus({ environmentId }) const { data: cluster } = useCluster({ organizationId, clusterId: environment?.cluster_id, suspense: true }) const { data: services = [] } = useServices({ environmentId, suspense: true }) + const isAgenticWorkflowEnabled = Boolean(useFeatureFlagEnabled('argentic-workflow')) useClusterRunningStatusSocket({ organizationId, @@ -195,6 +197,9 @@ function RouteComponent() { )} {shouldDisplayArgoCdServicesBelowQovery && } + {isServicesListTab && isAgenticWorkflowEnabled && ( + + )} 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 de59538e01e..f35a54b415a 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 AgenticWorkflowResponse, + 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() @@ -238,7 +241,7 @@ export function isArgoCd(service?: AnyService): service is ArgoCd { } export function isEditableService(service: AnyService): service is EditableService { - return !isArgoCd(service) + return !isArgoCd(service) && (service.service_type as string) !== 'AGENTIC_WORKFLOW' } export function isEditableServiceType(serviceType?: ServiceType): serviceType is EditableServiceType { @@ -365,6 +368,13 @@ export const services = createQueryKeys('services', { ) }, }), + listAgenticWorkflows: (environmentId: string) => ({ + queryKey: [environmentId], + async queryFn(): Promise { + const response = await agenticWorkflowsApi.listAgenticWorkflows(environmentId) + return response.data.results ?? [] + }, + }), argocdManifest: (serviceId: string) => ({ queryKey: [serviceId], async queryFn() { diff --git a/libs/domains/services/feature/src/index.ts b/libs/domains/services/feature/src/index.ts index f798adcc252..744892f77ba 100644 --- a/libs/domains/services/feature/src/index.ts +++ b/libs/domains/services/feature/src/index.ts @@ -67,6 +67,8 @@ export * from './lib/hooks/use-recent-services/use-recent-services' export * from './lib/hooks/use-favorite-services/use-favorite-services' export * from './lib/service-actions/service-actions' export * from './lib/argocd-service-list/argocd-service-list' +export * from './lib/agentic-workflow-service-list/agentic-workflow-service-list' +export * from './lib/hooks/use-agentic-workflow-services/use-agentic-workflow-services' export * from './lib/argocd-manifest/argocd-manifest' export * from './lib/service-deployment-status-label/service-deployment-status-label' export * from './lib/service-overview/service-header/service-header' diff --git a/libs/domains/services/feature/src/lib/agentic-workflow-service-list/agentic-workflow-service-list.spec.tsx b/libs/domains/services/feature/src/lib/agentic-workflow-service-list/agentic-workflow-service-list.spec.tsx new file mode 100644 index 00000000000..3d2bf230e10 --- /dev/null +++ b/libs/domains/services/feature/src/lib/agentic-workflow-service-list/agentic-workflow-service-list.spec.tsx @@ -0,0 +1,33 @@ +import { renderWithProviders, screen } from '@qovery/shared/util-tests' +import { AgenticWorkflowServiceList } from './agentic-workflow-service-list' + +const mockUseAgenticWorkflowServices = jest.fn() + +jest.mock('../hooks/use-agentic-workflow-services/use-agentic-workflow-services', () => ({ + useAgenticWorkflowServices: () => mockUseAgenticWorkflowServices(), +})) + +describe('AgenticWorkflowServiceList', () => { + it('should not render an empty section', () => { + mockUseAgenticWorkflowServices.mockReturnValue({ data: [] }) + const { container } = renderWithProviders() + + expect(container).toBeEmptyDOMElement() + }) + + it('should render agentic workflows in their own section', () => { + mockUseAgenticWorkflowServices.mockReturnValue({ + data: [ + { id: 'workflow-1', name: 'Review pull requests', enabled: true, model: { type: 'CLAUDE' } }, + { id: 'workflow-2', name: 'Triage incidents', enabled: false, model: { type: 'OPENAI' } }, + ], + }) + renderWithProviders() + + expect(screen.getByRole('heading', { name: 'Agentic workflows' })).toBeInTheDocument() + expect(screen.getByText('Review pull requests')).toBeInTheDocument() + expect(screen.getByText('Triage incidents')).toBeInTheDocument() + expect(screen.getByText('1 enabled')).toBeInTheDocument() + expect(screen.getByText('CLAUDE')).toBeInTheDocument() + }) +}) diff --git a/libs/domains/services/feature/src/lib/agentic-workflow-service-list/agentic-workflow-service-list.tsx b/libs/domains/services/feature/src/lib/agentic-workflow-service-list/agentic-workflow-service-list.tsx new file mode 100644 index 00000000000..5277d0b36fa --- /dev/null +++ b/libs/domains/services/feature/src/lib/agentic-workflow-service-list/agentic-workflow-service-list.tsx @@ -0,0 +1,80 @@ +import { type AgenticWorkflowResponse } from 'qovery-typescript-axios' +import { Badge, Heading, Icon, Section, TablePrimitives, Tooltip } from '@qovery/shared/ui' +import { useAgenticWorkflowServices } from '../hooks/use-agentic-workflow-services/use-agentic-workflow-services' + +const { Table } = TablePrimitives + +const tableGridLayoutClassName = 'grid w-full grid-cols-[minmax(350px,1fr)_minmax(180px,260px)_140px]' + +export interface AgenticWorkflowServiceListProps { + environmentId: string +} + +export function AgenticWorkflowServiceList({ environmentId }: AgenticWorkflowServiceListProps) { + const { data: services = [] } = useAgenticWorkflowServices({ environmentId, suspense: true }) + + if (services.length === 0) return null + + const enabledServicesCount = services.filter(({ enabled }) => enabled).length + + return ( + + + + Agentic workflows + + AI workflows triggered through webhooks and connected services. + + + + + + {enabledServicesCount} enabled + + + + + + + Service + + + Model + + + Status + + + + + {services.map((service: AgenticWorkflowResponse) => ( + + + + + + {service.name} + + + + + {service.model?.type ?? '-'} + + + + {service.enabled ? 'Enabled' : 'Disabled'} + + + + ))} + + + + + ) +} + +export default AgenticWorkflowServiceList diff --git a/libs/domains/services/feature/src/lib/hooks/use-agentic-workflow-services/use-agentic-workflow-services.ts b/libs/domains/services/feature/src/lib/hooks/use-agentic-workflow-services/use-agentic-workflow-services.ts new file mode 100644 index 00000000000..f712ad971e8 --- /dev/null +++ b/libs/domains/services/feature/src/lib/hooks/use-agentic-workflow-services/use-agentic-workflow-services.ts @@ -0,0 +1,16 @@ +import { useQuery } from '@tanstack/react-query' +import { queries } from '@qovery/state/util-queries' + +export interface UseAgenticWorkflowServicesProps { + environmentId: string + suspense?: boolean +} + +export function useAgenticWorkflowServices({ environmentId, suspense = false }: UseAgenticWorkflowServicesProps) { + return useQuery({ + ...queries.services.listAgenticWorkflows(environmentId), + suspense, + }) +} + +export default useAgenticWorkflowServices diff --git a/libs/domains/services/feature/src/lib/service-list/service-list.spec.tsx b/libs/domains/services/feature/src/lib/service-list/service-list.spec.tsx index 58e52241fe8..f67bda65b57 100644 --- a/libs/domains/services/feature/src/lib/service-list/service-list.spec.tsx +++ b/libs/domains/services/feature/src/lib/service-list/service-list.spec.tsx @@ -469,6 +469,24 @@ describe('ServiceList', () => { expect(screen.queryByText('ARGOCD APP')).not.toBeInTheDocument() }) + it('should not pass agentic workflows to the standard service table', () => { + mockServicesData = [ + { + id: 'agentic-workflow-id', + name: 'Review pull requests', + service_type: 'AGENTIC_WORKFLOW', + serviceType: 'AGENTIC_WORKFLOW', + enabled: true, + model: { type: 'CLAUDE' }, + }, + ] + + renderWithProviders() + + expect(screen.getByText('No service found')).toBeInTheDocument() + expect(screen.queryByText('Review pull requests')).not.toBeInTheDocument() + }) + it('should navigate to service on row click', async () => { const { userEvent } = renderWithProviders() const rows = screen.getAllByRole('row')
AI workflows triggered through webhooks and connected services.