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 @@ -166,7 +166,7 @@ describe('RunNowFeature — opening the dialog', () => {
})

describe('RunNowFeature — confirming a run', () => {
it('starts the run through the real mutation and navigates to its inspector', async () => {
it('starts the run through the real mutation without leaving the editor', async () => {
const user = userEvent.setup()
renderFeature((api) => {
api.mockMutation('post', WORKFLOW_RUNS_PATH, () => ({
Expand All @@ -184,12 +184,9 @@ describe('RunNowFeature — confirming a run', () => {
)

await waitFor(() => {
expect(navigateSpy).toHaveBeenCalledWith(
expect.objectContaining({
params: { workflowId: 'workflow-1', runId: 'run-42' },
}),
)
expect(screen.queryByRole('dialog')).toBeNull()
})
expect(navigateSpy).not.toHaveBeenCalled()
})

it('shows the failure and keeps the dialog open when the start is refused', async () => {
Expand Down
12 changes: 1 addition & 11 deletions apps/webapp/src/pages/automation/feature/run-now-feature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,11 @@ function RunNowWorkspace({
async function handleConfirm(payload: unknown) {
setStartError(null)
try {
const result = await startRun.mutateAsync({
await startRun.mutateAsync({
path: { organization_id: organizationId, workflow_id: workflowId },
body: { trigger_payload: payload },
})
setOpen(false)
await navigate({
to: buildOrgPath(
organizationSlug,
'/automatisation/$workflowId/executions/$runId',
),
params: {
workflowId,
runId: (result.data as { run_id: string }).run_id,
},
})
} catch (error) {
setStartError(
error instanceof Error && error.message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -955,11 +955,8 @@ describe('WorkflowCanvasFeature — the last run', () => {
const node = await screen.findByTestId('rf__node-c1')
node.click()

const lastRunButton = (await screen.findByRole('button', {
name: 'Dernière exécution',
})) as HTMLButtonElement
await waitFor(() => {
expect(lastRunButton.disabled).toBe(false)
expect(screen.getByRole('button', { name: 'trigger' })).toBeDefined()
})
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
useConnectorCatalogue,
useCreateCredential,
useEvaluateExpression,
useRun,
useRunPolling,
useSaveWorkflowVersion,
useWorkflow,
type WorkflowDetail,
Expand All @@ -23,6 +23,7 @@ import {
projectGraphErrors,
} from '#/pages/automation/lib/validation'
import {
aggregateConnectorStatuses,
connectorOutputsFromSteps,
latestRunId,
} from '#/pages/automation/lib/workflow-runs'
Expand Down Expand Up @@ -85,7 +86,7 @@ function WorkflowCanvasWorkspace({
const eventsQuery = useAutomationEvents(organizationId)
const runsQuery = useAutomationRuns(organizationId)
const latestId = latestRunId(runsQuery.data?.data ?? [], workflowId)
const runDetailQuery = useRun(organizationId, latestId)
const runDetailQuery = useRunPolling(organizationId, latestId)

if (
workflowQuery.isLoading ||
Expand Down Expand Up @@ -128,6 +129,10 @@ function WorkflowCanvasWorkspace({
}
: null

const runStatuses = runDetail
? aggregateConnectorStatuses(runDetail.steps)
: new Map<string, string>()

return (
<WorkflowCanvasLoaded
organizationId={organizationId}
Expand All @@ -140,6 +145,7 @@ function WorkflowCanvasWorkspace({
credentials={credentialsQuery.data?.data ?? []}
events={eventsQuery.data?.data ?? []}
lastRun={lastRun}
runStatuses={runStatuses}
/>
)
}
Expand All @@ -153,6 +159,7 @@ function WorkflowCanvasLoaded({
credentials,
events,
lastRun,
runStatuses,
}: {
organizationId: string
workflowId: string
Expand All @@ -162,6 +169,7 @@ function WorkflowCanvasLoaded({
credentials: Schemas.CredentialResponse[]
events: Schemas.EventDescriptorResponse[]
lastRun: LastRunData | null
runStatuses: Map<string, string>
}) {
const saveVersion = useSaveWorkflowVersion()
const createCredential = useCreateCredential(organizationId)
Expand Down Expand Up @@ -260,6 +268,7 @@ function WorkflowCanvasLoaded({
connectorErrors={validation.connectorErrors}
events={events}
lastRun={lastRun}
runStatuses={runStatuses}
onEvaluateExpression={handleEvaluateExpression}
credentials={credentials}
authSchemes={authSchemes}
Expand Down
25 changes: 25 additions & 0 deletions apps/webapp/src/pages/automation/lib/workflow-runs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
latestRunByWorkflow,
latestRunId,
runDurationMs,
runStatusBorderClass,
runTriggerLabel,
} from '#/pages/automation/lib/workflow-runs'

Expand Down Expand Up @@ -361,3 +362,27 @@ describe('canReplay', () => {
expect(canReplay('running')).toBe(false)
})
})

describe('runStatusBorderClass', () => {
it('marks a step in progress amber', () => {
expect(runStatusBorderClass('running')).toBe('border-amber-500')
})

it('marks a succeeded step green', () => {
expect(runStatusBorderClass('succeeded')).toBe('border-emerald-500')
})

it('marks a failed step red', () => {
expect(runStatusBorderClass('failed')).toBe('border-destructive')
})

it('leaves a pending or cancelled step unmarked', () => {
expect(runStatusBorderClass('pending')).toBeNull()
expect(runStatusBorderClass('cancelled')).toBeNull()
})

it('leaves a connector with no step at all unmarked', () => {
expect(runStatusBorderClass(null)).toBeNull()
expect(runStatusBorderClass(undefined)).toBeNull()
})
})
14 changes: 14 additions & 0 deletions apps/webapp/src/pages/automation/lib/workflow-runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,20 @@ const STATUS_PRIORITY: Record<string, number> = {
succeeded: 0,
}

const RUN_STATUS_BORDER: Record<string, string> = {
running: 'border-amber-500',
succeeded: 'border-emerald-500',
failed: 'border-destructive',
}

export function runStatusBorderClass(
status: string | null | undefined,
): string | null {
if (!status) return null

return RUN_STATUS_BORDER[status] ?? null
}

export function aggregateConnectorStatuses(
steps: Schemas.RunStepResponse[],
): Map<string, string> {
Expand Down
36 changes: 1 addition & 35 deletions apps/webapp/src/pages/automation/ui/available-data-tree.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,14 @@ function renderTree(
overrides: Partial<Parameters<typeof AvailableDataTree>[0]> = {},
) {
const onInsert = vi.fn()
const onSourceChange = vi.fn()
render(
<AvailableDataTree
branches={[TRIGGER_BRANCH, CONNECTOR_BRANCH]}
source="example"
hasLastRun={false}
onSourceChange={onSourceChange}
onInsert={onInsert}
{...overrides}
/>,
)
return { onInsert, onSourceChange }
return { onInsert }
}

describe('AvailableDataTree — structure', () => {
Expand Down Expand Up @@ -131,33 +127,3 @@ describe('AvailableDataTree — insertion', () => {
)
})
})

describe('AvailableDataTree — source toggle', () => {
it('disables the last-run option until a run exists', () => {
renderTree({ hasLastRun: false })

const button = screen.getByRole('button', {
name: 'Dernière exécution',
}) as HTMLButtonElement
expect(button.disabled).toBe(true)
})

it('reports the requested source once a run exists', async () => {
const user = userEvent.setup()
const { onSourceChange } = renderTree({ hasLastRun: true })

await user.click(screen.getByRole('button', { name: 'Dernière exécution' }))

expect(onSourceChange).toHaveBeenCalledWith('last_run')
})

it('marks the active source', () => {
renderTree({ source: 'example' })

expect(
screen
.getByRole('button', { name: 'Exemple' })
.getAttribute('aria-pressed'),
).toBe('true')
})
})
50 changes: 0 additions & 50 deletions apps/webapp/src/pages/automation/ui/available-data-tree.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,18 @@
import { ChevronDown, ChevronRight } from 'lucide-react'
import { useState } from 'react'
import { cn } from '#/lib/utils'
import type { DataTreeNode } from '#/pages/automation/lib/data-tree'

export type DataTreeSource = 'example' | 'last_run'

export interface AvailableDataTreeProps {
branches: DataTreeNode[]
source: DataTreeSource
hasLastRun: boolean
onSourceChange: (source: DataTreeSource) => void
onInsert: (path: string) => void
}

export function AvailableDataTree({
branches,
source,
hasLastRun,
onSourceChange,
onInsert,
}: AvailableDataTreeProps) {
return (
<div className="flex flex-col gap-2">
<div className="flex gap-1 rounded-md border p-0.5 text-xs">
<SourceButton
label="Exemple"
active={source === 'example'}
onClick={() => onSourceChange('example')}
/>
<SourceButton
label="Dernière exécution"
active={source === 'last_run'}
disabled={!hasLastRun}
onClick={() => onSourceChange('last_run')}
/>
</div>
{branches.length === 0 ? (
<p className="text-sm text-muted-foreground">
Aucune donnée disponible.
Expand All @@ -50,34 +28,6 @@ export function AvailableDataTree({
)
}

function SourceButton({
label,
active,
disabled = false,
onClick,
}: {
label: string
active: boolean
disabled?: boolean
onClick: () => void
}) {
return (
<button
type="button"
aria-pressed={active}
disabled={disabled}
onClick={onClick}
className={cn(
'flex-1 rounded px-2 py-1',
active ? 'bg-accent font-medium' : 'text-muted-foreground',
'disabled:cursor-not-allowed disabled:opacity-50',
)}
>
{label}
</button>
)
}

function TreeNodeItem({
node,
onInsert,
Expand Down
40 changes: 18 additions & 22 deletions apps/webapp/src/pages/automation/ui/connector-config-panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -445,26 +445,34 @@ describe('ConnectorConfigPanel — the available-data tree', () => {
expect(screen.getByRole('button', { name: /trigger/ })).toBeDefined()
})

it('disables the last-run toggle until a run exists', () => {
it('shows the real values rather than the example once a run exists', async () => {
const user = userEvent.setup()
render(
<ConnectorConfigPanel
{...baseProps({
exampleData: {
tree: [TRIGGER_BRANCH],
context: { trigger: null, connectors: {}, loop: null },
context: { trigger: { name: 'Julie' }, connectors: {}, loop: null },
},
lastRunData: {
tree: [LAST_RUN_TRIGGER_BRANCH],
context: {
trigger: { name: 'Real customer' },
connectors: {},
loop: null,
},
},
lastRunData: null,
})}
/>,
)

const button = screen.getByRole('button', {
name: 'Dernière exécution',
}) as HTMLButtonElement
expect(button.disabled).toBe(true)
await user.click(screen.getByRole('button', { name: 'trigger' }))

expect(screen.getByText('confirmed_name')).toBeDefined()
expect(screen.queryByText('name')).toBeNull()
})

it('switches the same tree to the real values once a run exists', async () => {
it('falls back to the example while no run has happened', async () => {
const user = userEvent.setup()
render(
<ConnectorConfigPanel
Expand All @@ -473,26 +481,14 @@ describe('ConnectorConfigPanel — the available-data tree', () => {
tree: [TRIGGER_BRANCH],
context: { trigger: { name: 'Julie' }, connectors: {}, loop: null },
},
lastRunData: {
tree: [LAST_RUN_TRIGGER_BRANCH],
context: {
trigger: { name: 'Real customer' },
connectors: {},
loop: null,
},
},
lastRunData: null,
})}
/>,
)

await user.click(screen.getByRole('button', { name: 'trigger' }))
expect(screen.getByText('name')).toBeDefined()
expect(screen.queryByText('confirmed_name')).toBeNull()

await user.click(screen.getByRole('button', { name: 'Dernière exécution' }))

expect(screen.getByText('confirmed_name')).toBeDefined()
expect(screen.queryByText('name')).toBeNull()
expect(screen.getByText('name')).toBeDefined()
})
})

Expand Down
Loading
Loading