Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const Route = createFileRoute(
})

function RouteComponent() {
useDocumentTitle('Agentic workflow configuration')
useDocumentTitle('Agent task configuration')

return <AgenticWorkflowConfiguration />
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const Route = createFileRoute(
})

function RouteComponent() {
useDocumentTitle('Summary - Create agentic workflow')
useDocumentTitle('Summary - Create agent task')

return <AgenticWorkflowSummary />
}
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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(
<FilterSection
{...props}
queryParams={queryParamsWithTarget as DecodedValueMap<AuditLogsParams>}
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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
})
}
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<RowEvent
{...props}
event={{
...props.event,
target_type: OrganizationEventTargetType.AGENTIC_WORKFLOW,
}}
/>
)

expect(screen.getByText('Agent task')).toBeInTheDocument()
})

it.each([
[OrganizationEventTargetType.ORGANIZATION, '/organization/1/settings'],
[OrganizationEventTargetType.MEMBERS_AND_ROLES, '/organization/1/settings/members'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -312,7 +313,7 @@ export function RowEvent(props: RowEventProps) {
</div>
<div className="min-w-0 px-4 text-neutral-subtle">
<Skeleton height={10} width={80} show={isPlaceholder}>
<>{upperCaseFirstLetter(event.target_type ?? '')?.replace(/_/g, ' ')}</>
<>{getTargetTypeLabel(event.target_type ?? '')}</>
</Skeleton>
</div>
<div className="min-w-0 px-4">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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')

const targetTypeItem: SelectedItem = {
filterKey: 'target_type',
item: {
value: OrganizationEventTargetType.AGENTIC_WORKFLOW,
name: 'Agentic workflow',
name: 'Agent task',
},
}

Expand Down Expand Up @@ -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')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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<OrganizationEventTargetType> = new Set([
OrganizationEventTargetType.AGENTIC_WORKFLOW,
Expand All @@ -17,6 +18,12 @@ const SERVICE_TARGET_TYPES: ReadonlySet<OrganizationEventTargetType> = 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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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,
Expand All @@ -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, () => {
Expand Down Expand Up @@ -295,7 +310,7 @@ export function EnvironmentSection({
</Table.Header>

<Table.Body className="divide-y divide-neutral">
{items.map((environmentOverview) => (
{sortedItems.map((environmentOverview) => (
<EnvRow
key={environmentOverview.id}
overview={environmentOverview}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ jest.mock('./environments-table-action-bar', () => ({
EnvironmentsTableActionBar: () => <div data-testid="environments-table-action-bar" />,
}))

function environmentOverview(id: string, mode: EnvironmentModeEnum, name: string): EnvironmentOverviewResponse {
function environmentOverview(
id: string,
mode: EnvironmentModeEnum,
name: string,
lastDeploymentDate?: string
): EnvironmentOverviewResponse {
return {
id,
mode,
Expand All @@ -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
}

Expand Down Expand Up @@ -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(<EnvironmentsTable />)

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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand All @@ -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 (
<div
role="checkbox"
Expand All @@ -59,8 +53,7 @@ function UseCaseCard({ value, label, iconName, colSpan, selected, onToggle }: Us
}
}}
className={twMerge(
'focus-visible:outline-brand-11 flex cursor-pointer items-center gap-3 rounded-lg border p-4 transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
colSpan,
'focus-visible:outline-brand-11 flex w-[calc((100%_-_1.5rem)/3)] cursor-pointer items-center gap-3 rounded-lg border p-4 transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
selected
? 'border-brand-component bg-surface-brand-subtle'
: 'border-neutral bg-background hover:border-neutral-component hover:bg-surface-neutral-subtle'
Expand Down Expand Up @@ -88,14 +81,13 @@ export function StepUseCases({ onSubmit, onBack }: StepUseCasesProps) {
<div className="mx-auto max-w-content-with-navigation-left pb-10">
<h1 className="h3 mb-3 text-neutral">What are you looking to do?</h1>
<p className="mb-10 text-sm text-neutral">Select all that apply.</p>
<div className="grid grid-cols-6 gap-3">
<div className="flex flex-wrap justify-center gap-3">
{USE_CASES.map((useCase) => (
<UseCaseCard
key={useCase.value}
value={useCase.value}
label={useCase.label}
iconName={useCase.iconName}
colSpan="col-span-2"
selected={selected.includes(useCase.value)}
onToggle={toggle}
/>
Expand All @@ -106,7 +98,7 @@ export function StepUseCases({ onSubmit, onBack }: StepUseCasesProps) {
<Icon iconName="arrow-left" />
Back
</Button>
<Button type="button" size="lg" onClick={() => onSubmit(selected)}>
<Button type="button" size="lg" disabled={selected.length === 0} onClick={() => onSubmit(selected)}>
Continue
</Button>
</div>
Expand Down
Loading
Loading