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 index 836922c909b..f6fb03b2436 100644 --- 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 @@ -9,7 +9,7 @@ export const Route = createFileRoute( }) function RouteComponent() { - useDocumentTitle('Agentic workflow configuration') + useDocumentTitle('Agent task configuration') return } 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 index 6bb8d520991..0b958e78c35 100644 --- 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 @@ -9,7 +9,7 @@ export const Route = createFileRoute( }) function RouteComponent() { - useDocumentTitle('Summary - Create agentic workflow') + useDocumentTitle('Summary - Create agent task') return } diff --git a/libs/domains/audit-logs/feature/src/lib/audit-logs-view/audit-logs-view.tsx b/libs/domains/audit-logs/feature/src/lib/audit-logs-view/audit-logs-view.tsx index 22c4d075bec..1c08416064f 100644 --- a/libs/domains/audit-logs/feature/src/lib/audit-logs-view/audit-logs-view.tsx +++ b/libs/domains/audit-logs/feature/src/lib/audit-logs-view/audit-logs-view.tsx @@ -7,9 +7,8 @@ import { eventsFactoryMock } from '@qovery/shared/factories' import { type AuditLogsParams, DEFAULT_PAGE_SIZE } from '@qovery/shared/router' import { ALL, type NavigationLevel, type SelectedItem, type TableFilterProps } from '@qovery/shared/ui' import { useDocumentTitle, useSupportChat } from '@qovery/shared/util-hooks' -import { upperCaseFirstLetter } from '@qovery/shared/util-js' import { AuditLogs } from '../audit-logs/audit-logs' -import { initializeSelectedItemsFromQueryParams } from '../utils/target-type-selection-utils' +import { getTargetTypeLabel, initializeSelectedItemsFromQueryParams } from '../utils/target-type-selection-utils' const route = getRouteApi('/_authenticated/organization/$organizationId/audit-logs') @@ -107,7 +106,7 @@ export function AuditLogsView() { const organizationEventTargetTypes = Object.keys(OrganizationEventTargetType).map((item) => ({ value: item, - name: upperCaseFirstLetter(item).replace(/_/g, ' '), + name: getTargetTypeLabel(item), })) initializeSelectedItemsFromQueryParams(organizationId, organizationEventTargetTypes, 'target_type', urlParams) diff --git a/libs/domains/audit-logs/feature/src/lib/audit-logs/audit-logs.tsx b/libs/domains/audit-logs/feature/src/lib/audit-logs/audit-logs.tsx index 6c50201c72c..0a41018187e 100644 --- a/libs/domains/audit-logs/feature/src/lib/audit-logs/audit-logs.tsx +++ b/libs/domains/audit-logs/feature/src/lib/audit-logs/audit-logs.tsx @@ -23,13 +23,13 @@ import { type TableHeadProps, } from '@qovery/shared/ui' import { type SelectedTimestamps } from '@qovery/shared/ui' -import { upperCaseFirstLetter } from '@qovery/shared/util-js' import FilterSection from '../filter-section/filter-section' import RowEventFeature from '../row-event-feature/row-event-feature' import { computeDisplayByLabel, computeMenusToDisplay, computeSelectedItemsFromFilter, + getTargetTypeLabel, } from '../utils/target-type-selection-utils' export interface AuditLogsProps { @@ -141,7 +141,7 @@ function createTableDataHead( initialData: Object.keys(OrganizationEventTargetType).map((item) => { return { value: item, - name: upperCaseFirstLetter(item).replace(/_/g, ' '), + name: getTargetTypeLabel(item), } }), initialSelectedItems: targetTypeSelectedItems, diff --git a/libs/domains/audit-logs/feature/src/lib/filter-section/filter-section.spec.tsx b/libs/domains/audit-logs/feature/src/lib/filter-section/filter-section.spec.tsx index a3a672a12f3..bd3b4cc23d9 100644 --- a/libs/domains/audit-logs/feature/src/lib/filter-section/filter-section.spec.tsx +++ b/libs/domains/audit-logs/feature/src/lib/filter-section/filter-section.spec.tsx @@ -1,3 +1,4 @@ +import { OrganizationEventTargetType } from 'qovery-typescript-axios' import { type DecodedValueMap } from 'use-query-params' import { type AuditLogsParams } from '@qovery/shared/router' import { type SelectedItem } from '@qovery/shared/ui' @@ -74,6 +75,30 @@ describe('FilterSection', () => { screen.getByText(/Application/) }) + it('should render Agent task in target type and target badges', () => { + const queryParamsWithTarget = { + targetId: 'target-123', + targetType: OrganizationEventTargetType.AGENTIC_WORKFLOW, + } + const selectedItems: SelectedItem[] = [ + { + filterKey: 'target_id', + item: { value: 'target-123', name: 'Review pull requests' }, + }, + ] + + renderWithProviders( + } + targetTypeSelectedItems={selectedItems} + /> + ) + + expect(screen.getByRole('button', { name: /Target Type: Agent task/ })).toBeInTheDocument() + expect(screen.getByRole('button', { name: /Agent task: Review pull requests/ })).toBeInTheDocument() + }) + it('should render user badge when triggeredBy is set', () => { const queryParamsWithTriggeredBy = { triggeredBy: 'john_doe', diff --git a/libs/domains/audit-logs/feature/src/lib/filter-section/filter-section.tsx b/libs/domains/audit-logs/feature/src/lib/filter-section/filter-section.tsx index 2c83f038ef6..f02adde06f2 100644 --- a/libs/domains/audit-logs/feature/src/lib/filter-section/filter-section.tsx +++ b/libs/domains/audit-logs/feature/src/lib/filter-section/filter-section.tsx @@ -4,6 +4,7 @@ import { type AuditLogsParams } from '@qovery/shared/router' import { Button, Icon, type SelectedItem, type TableFilterProps, Truncate } from '@qovery/shared/ui' import { dateYearMonthDayHourMinuteSecond } from '@qovery/shared/util-dates' import { twMerge, upperCaseFirstLetter } from '@qovery/shared/util-js' +import { getTargetTypeLabel } from '../utils/target-type-selection-utils' export interface CustomFilterProps { clearFilter: () => void @@ -55,7 +56,7 @@ function buildBadges(queryParams: AuditLogsParams, selectedItemsTargetType: Sele badges.push({ key: 'target_type', displayedName: 'Target Type', - value: upperCaseFirstLetter(queryParams.targetType).split('_').join(' '), + value: getTargetTypeLabel(queryParams.targetType), isDeletable: true, }) } @@ -105,9 +106,7 @@ function buildBadges(queryParams: AuditLogsParams, selectedItemsTargetType: Sele const selectedItem = selectedItemsTargetType.find((selectedItem) => selectedItem.filterKey === 'target_id') if (selectedItem) { const targetName = selectedItem?.item?.name ?? '...' - const targetType = upperCaseFirstLetter(queryParams.targetType ?? '...') - .split('_') - .join(' ') + const targetType = getTargetTypeLabel(queryParams.targetType ?? '...') badges.push({ key: 'target_id', displayedName: targetType, diff --git a/libs/domains/audit-logs/feature/src/lib/row-event/row-event.spec.tsx b/libs/domains/audit-logs/feature/src/lib/row-event/row-event.spec.tsx index 30cb3bb5676..50b0912193c 100644 --- a/libs/domains/audit-logs/feature/src/lib/row-event/row-event.spec.tsx +++ b/libs/domains/audit-logs/feature/src/lib/row-event/row-event.spec.tsx @@ -114,6 +114,20 @@ describe('RowEvent', () => { expect(screen.getByText('unsupported-target')).not.toHaveAttribute('href') }) + it('should render Agent task for the agentic workflow target type', () => { + renderWithProviders( + + ) + + expect(screen.getByText('Agent task')).toBeInTheDocument() + }) + it.each([ [OrganizationEventTargetType.ORGANIZATION, '/organization/1/settings'], [OrganizationEventTargetType.MEMBERS_AND_ROLES, '/organization/1/settings/members'], diff --git a/libs/domains/audit-logs/feature/src/lib/row-event/row-event.tsx b/libs/domains/audit-logs/feature/src/lib/row-event/row-event.tsx index c08001956ef..8c1907adddb 100644 --- a/libs/domains/audit-logs/feature/src/lib/row-event/row-event.tsx +++ b/libs/domains/audit-logs/feature/src/lib/row-event/row-event.tsx @@ -13,6 +13,7 @@ import { IconEnum } from '@qovery/shared/enums' import { CodeDiffEditor, CodeEditor, type DiffStats, Icon, Skeleton, Tooltip, Truncate } from '@qovery/shared/ui' import { dateFullFormat, dateUTCString } from '@qovery/shared/util-dates' import { twMerge, upperCaseFirstLetter } from '@qovery/shared/util-js' +import { getTargetTypeLabel } from '../utils/target-type-selection-utils' export interface RowEventProps { event: OrganizationEventResponse @@ -312,7 +313,7 @@ export function RowEvent(props: RowEventProps) {
- <>{upperCaseFirstLetter(event.target_type ?? '')?.replace(/_/g, ' ')} + <>{getTargetTypeLabel(event.target_type ?? '')}
diff --git a/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.spec.ts b/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.spec.ts index b57b940f1c9..5bd83b5941b 100644 --- a/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.spec.ts +++ b/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.spec.ts @@ -1,6 +1,6 @@ import { OrganizationEventApi, OrganizationEventTargetType } from 'qovery-typescript-axios' import { type SelectedItem } from '@qovery/shared/ui' -import { computeMenusToDisplay } from './target-type-selection-utils' +import { computeMenusToDisplay, getTargetTypeLabel } from './target-type-selection-utils' const mockGetOrganizationEventTargets = jest.spyOn(OrganizationEventApi.prototype, 'getOrganizationEventTargets') @@ -8,7 +8,7 @@ const targetTypeItem: SelectedItem = { filterKey: 'target_type', item: { value: OrganizationEventTargetType.AGENTIC_WORKFLOW, - name: 'Agentic workflow', + name: 'Agent task', }, } @@ -85,3 +85,13 @@ describe('computeMenusToDisplay', () => { ) }) }) + +describe('getTargetTypeLabel', () => { + it('uses the Agent task product name for the agentic workflow API type', () => { + expect(getTargetTypeLabel(OrganizationEventTargetType.AGENTIC_WORKFLOW)).toBe('Agent task') + }) + + it('formats other API target types', () => { + expect(getTargetTypeLabel(OrganizationEventTargetType.CONTAINER_REGISTRY)).toBe('Container registry') + }) +}) diff --git a/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.tsx b/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.tsx index e56246307e6..807c1dc989f 100644 --- a/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.tsx +++ b/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.tsx @@ -6,6 +6,7 @@ import { type NavigationLevel, type SelectedItem, } from '@qovery/shared/ui' +import { upperCaseFirstLetter } from '@qovery/shared/util-js' const SERVICE_TARGET_TYPES: ReadonlySet = new Set([ OrganizationEventTargetType.AGENTIC_WORKFLOW, @@ -17,6 +18,12 @@ const SERVICE_TARGET_TYPES: ReadonlySet = new Set([ OrganizationEventTargetType.TERRAFORM, ]) +export function getTargetTypeLabel(targetType: string) { + return targetType === OrganizationEventTargetType.AGENTIC_WORKFLOW + ? 'Agent task' + : upperCaseFirstLetter(targetType).replace(/_/g, ' ') +} + function getTargetTypeSelected(selectedItems: SelectedItem[]): OrganizationEventTargetType | undefined { const selectedTargetType = selectedItems.find((selectedItem) => { return selectedItem.filterKey === 'target_type' diff --git a/libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx b/libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx index ff343493176..0be8a4fb4da 100644 --- a/libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx +++ b/libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx @@ -1,6 +1,6 @@ import { Link, useNavigate, useParams } from '@tanstack/react-router' import { EnvironmentModeEnum, type EnvironmentOverviewResponse, StateEnum } from 'qovery-typescript-axios' -import { type KeyboardEvent, type MouseEvent } from 'react' +import { type KeyboardEvent, type MouseEvent, useMemo } from 'react' import { match } from 'ts-pattern' import { ClusterAvatar } from '@qovery/domains/clusters/feature' import { Button, Checkbox, DeploymentAction, Heading, Icon, Section, TablePrimitives, Tooltip } from '@qovery/shared/ui' @@ -187,6 +187,11 @@ function EnvRow({ ) } +function lastOperationTimestamp(overview: EnvironmentOverviewResponse) { + const lastDeploymentDate = overview.deployment_status?.last_deployment_date + return lastDeploymentDate ? new Date(lastDeploymentDate).getTime() : 0 +} + export function EnvironmentSection({ type, items, @@ -209,6 +214,16 @@ export function EnvironmentSection({ .with('PREVIEW', () => 'Ephemeral') .exhaustive() + const sortedItems = useMemo(() => { + if (type !== EnvironmentModeEnum.PREVIEW) { + return items + } + + return [...items].sort( + (environmentA, environmentB) => lastOperationTimestamp(environmentB) - lastOperationTimestamp(environmentA) + ) + }, [items, type]) + const EmptyState = () => match(type) .with(EnvironmentModeEnum.PREVIEW, () => { @@ -295,7 +310,7 @@ export function EnvironmentSection({ - {items.map((environmentOverview) => ( + {sortedItems.map((environmentOverview) => ( ({ EnvironmentsTableActionBar: () =>
, })) -function environmentOverview(id: string, mode: EnvironmentModeEnum, name: string): EnvironmentOverviewResponse { +function environmentOverview( + id: string, + mode: EnvironmentModeEnum, + name: string, + lastDeploymentDate?: string +): EnvironmentOverviewResponse { return { id, mode, @@ -48,6 +53,7 @@ function environmentOverview(id: string, mode: EnvironmentModeEnum, name: string service_count: 0, managed_by: 'QOVERY', }, + ...(lastDeploymentDate ? { deployment_status: { last_deployment_date: lastDeploymentDate } } : {}), } as EnvironmentOverviewResponse } @@ -94,6 +100,25 @@ describe('EnvironmentsTable', () => { ]) }) + it('should sort ephemeral environments by last operation with newer environments first', () => { + mockUseProject.mockReturnValue({ data: { name: 'Project Alpha' } }) + mockUseEnvironmentsOverview.mockReturnValue({ + data: [ + environmentOverview('env-1', EnvironmentModeEnum.PREVIEW, 'Bravo', '2024-03-01T00:00:00Z'), + environmentOverview('env-2', EnvironmentModeEnum.PREVIEW, 'Alpha', '2024-01-01T00:00:00Z'), + environmentOverview('env-3', EnvironmentModeEnum.PREVIEW, 'Charlie', '2024-02-01T00:00:00Z'), + ], + }) + + renderWithProviders() + + expect(screen.getAllByRole('link', { name: /^(Alpha|Bravo|Charlie)$/ }).map((link) => link.textContent)).toEqual([ + 'Bravo', + 'Charlie', + 'Alpha', + ]) + }) + it('should preserve checkbox focus when selecting an environment', async () => { mockUseProject.mockReturnValue({ data: { name: 'Project Alpha' } }) mockUseEnvironmentsOverview.mockReturnValue({ diff --git a/libs/domains/onboarding/feature/src/lib/step-use-cases/step-use-cases.tsx b/libs/domains/onboarding/feature/src/lib/step-use-cases/step-use-cases.tsx index 5d30f00d779..4c41d8f9914 100644 --- a/libs/domains/onboarding/feature/src/lib/step-use-cases/step-use-cases.tsx +++ b/libs/domains/onboarding/feature/src/lib/step-use-cases/step-use-cases.tsx @@ -9,21 +9,16 @@ const USE_CASES: Array<{ value: string; label: string; iconName: IconName }> = [ label: 'Build workflows where AI can take actions on my systems with full auditability', iconName: 'microchip-ai', }, - { - value: 'rde', - label: 'Enable my non-tech team to ship apps', - iconName: 'users', - }, - { - value: 'spec-to-prod', - label: 'Go from spec to production with AI coding agents', - iconName: 'diagram-project', - }, { value: 'automate-deployments', label: 'Automate deployments without manual steps', iconName: 'rocket', }, + { + value: 'migrate-cloud-provider', + label: 'Migrate to another cloud provider from Heroku, Render or another PaaS', + iconName: 'right-left', + }, { value: 'ephemeral-environments', label: 'Create environments on demand (testing/dev/QA)', @@ -40,12 +35,11 @@ interface UseCaseCardProps { value: string label: string iconName: IconName - colSpan: string selected: boolean onToggle: (value: string) => void } -function UseCaseCard({ value, label, iconName, colSpan, selected, onToggle }: UseCaseCardProps) { +function UseCaseCard({ value, label, iconName, selected, onToggle }: UseCaseCardProps) { return (

What are you looking to do?

Select all that apply.

-
+
{USE_CASES.map((useCase) => ( @@ -106,7 +98,7 @@ export function StepUseCases({ onSubmit, onBack }: StepUseCasesProps) { Back -
diff --git a/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.tsx b/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.tsx index 74c09ee972e..17ac517f938 100644 --- a/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.tsx +++ b/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.tsx @@ -42,19 +42,19 @@ interface FormValues { } const PAGE_CONTENT: Record = { - general: { title: 'General settings', description: 'Configure the workflow name, description, and availability.' }, + general: { title: 'General settings', description: 'Configure the agent task name, description, and availability.' }, 'ai-configuration': { title: 'AI configuration', - description: 'Configure the model and instructions used by this workflow.', + description: 'Configure the model and instructions used by this agent task.', }, connections: { title: 'Connections', - description: 'Configure the Git repositories, MCP servers, and Dockerfile fragment available to the workflow.', + description: 'Configure the Git repositories, MCP servers, and Dockerfile fragment available to the agent task.', }, - outputs: { title: 'Outputs', description: 'Configure the webhooks called when the workflow produces an output.' }, + outputs: { title: 'Outputs', description: 'Configure the webhooks called when the agent task produces an output.' }, governance: { title: 'Governance', - description: 'Control the hosts and webhook source addresses allowed for this workflow.', + description: 'Control the hosts and webhook source addresses allowed for this agent task.', }, } @@ -241,7 +241,7 @@ export function AgenticWorkflowSettings({ page }: AgenticWorkflowSettingsProps) name="name" label="Name" value={values.name} - error={!values.name.trim() ? 'Please enter a workflow name.' : undefined} + error={!values.name.trim() ? 'Please enter an agent task name.' : undefined} onChange={(event) => form.setValue('name', event.currentTarget.value, { shouldDirty: true })} /> form.setValue('enabled', value, { shouldDirty: true })} />

Resources

- Configure the compute resources allocated to the workflow. + Configure the compute resources allocated to the agent task.

{(['cpu', 'ram', 'gpu', 'storage'] as const).map((name) => ( @@ -303,7 +303,7 @@ export function AgenticWorkflowSettings({ page }: AgenticWorkflowSettingsProps)

Git repositories

- Select the repositories and branches the workflow can access. + Select the repositories and branches the agent task can access.