diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts index 010854052f..22f9dc6b50 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts @@ -230,7 +230,7 @@ describe('LogsPanel', () => { await fireEvent.click(rendered.getByText('Overview')); - expect(rendered.getByText('Running')).toBeInTheDocument(); + expect(rendered.getByText(/Running/)).toBeInTheDocument(); expect(rendered.queryByText('AI Agent')).not.toBeInTheDocument(); workflowsStore.addNodeExecutionStartedData({ @@ -247,7 +247,7 @@ describe('LogsPanel', () => { }); expect(lastTreeItem.getByText('AI Agent')).toBeInTheDocument(); - expect(lastTreeItem.getByText('Running')).toBeInTheDocument(); + expect(lastTreeItem.getByText(/Running/)).toBeInTheDocument(); workflowsStore.updateNodeExecutionData({ nodeName: 'AI Agent', diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogDetailsPanel.test.ts b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogDetailsPanel.test.ts index 80c6eaa18f..708baef576 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogDetailsPanel.test.ts +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogDetailsPanel.test.ts @@ -108,6 +108,24 @@ describe('LogDetailsPanel', () => { expect(await outputPanel.findByText('Hello!')).toBeInTheDocument(); }); + it('should show a message in the output panel and data in the input panel when node is running', async () => { + const rendered = render({ + isOpen: true, + logEntry: createLogEntry({ + node: aiNode, + runIndex: 0, + runData: { ...aiNodeRunData, executionStatus: 'running' }, + }), + panels: LOG_DETAILS_PANEL_STATE.BOTH, + }); + + const inputPanel = within(rendered.getByTestId('log-details-input')); + const outputPanel = within(rendered.getByTestId('log-details-output')); + + expect(await inputPanel.findByText('hey')).toBeInTheDocument(); + expect(await outputPanel.findByText('Executing node...')).toBeInTheDocument(); + }); + it('should close input panel by dragging the divider to the left end', async () => { const rendered = render({ isOpen: true, diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogDetailsPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogDetailsPanel.vue index 59d27377c0..ac2c37fa17 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogDetailsPanel.vue +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogDetailsPanel.vue @@ -88,6 +88,7 @@ function handleResizeEnd() { :class="$style.executionSummary" :status="logEntry.runData.executionStatus ?? 'unknown'" :consumed-tokens="consumedTokens" + :start-time="logEntry.runData.startTime" :time-took="logEntry.runData.executionTime" /> diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue index e6d2c8f262..2a18dcc5b1 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue @@ -163,6 +163,7 @@ watch( :class="$style.summary" :status="execution.status" :consumed-tokens="consumedTokens" + :start-time="+new Date(execution.startedAt)" :time-took=" execution.startedAt && execution.stoppedAt ? +new Date(execution.stoppedAt) - +new Date(execution.startedAt) @@ -247,6 +248,8 @@ watch( .tree { padding: 0 var(--spacing-2xs) var(--spacing-2xs) var(--spacing-2xs); + scroll-padding-block: var(--spacing-3xs); + & :global(.el-icon) { display: none; } diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewRow.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewRow.vue index 316f1773d4..091037b315 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewRow.vue +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewRow.vue @@ -13,6 +13,7 @@ import { type LatestNodeInfo, type LogEntry, } from '@/components/RunDataAi/utils'; +import { useTimestamp } from '@vueuse/core'; const props = defineProps<{ data: LogEntry; @@ -34,12 +35,13 @@ const emit = defineEmits<{ const container = useTemplateRef('containerRef'); const locale = useI18n(); +const now = useTimestamp({ interval: 1000 }); const nodeTypeStore = useNodeTypesStore(); const type = computed(() => nodeTypeStore.getNodeType(props.data.node.type)); const isSettled = computed( () => props.data.runData.executionStatus && - ['crashed', 'error', 'success'].includes(props.data.runData.executionStatus), + !['running', 'waiting'].includes(props.data.runData.executionStatus), ); const isError = computed(() => !!props.data.runData.error); const startedAtText = computed(() => { @@ -51,6 +53,15 @@ const startedAtText = computed(() => { }, }); }); +const statusText = computed(() => upperFirst(props.data.runData.executionStatus)); +const timeText = computed(() => + locale.displayTimer( + isSettled.value + ? props.data.runData.executionTime + : Math.floor((now.value - props.data.runData.startTime) / 1000) * 1000, + true, + ), +); const subtreeConsumedTokens = computed(() => props.shouldShowTokenCountColumn ? getSubtreeTotalConsumedTokens(props.data, false) : undefined, @@ -126,19 +137,24 @@ watch( :is-deleted="latestInfo?.deleted ?? false" /> - + - + - + + (); const locale = useI18n(); +const now = useTimestamp({ interval: 1000 }); const executionStatusText = computed(() => - timeTook === undefined - ? upperFirst(status) - : locale.baseText('logs.overview.body.summaryText', { + status === 'running' || status === 'waiting' + ? locale.baseText('logs.overview.body.summaryText.for', { interpolate: { status: upperFirst(status), - time: locale.displayTimer(timeTook, true), + time: locale.displayTimer(Math.floor((now.value - startTime) / 1000) * 1000, true), }, - }), + }) + : timeTook === undefined + ? upperFirst(status) + : locale.baseText('logs.overview.body.summaryText.in', { + interpolate: { + status: upperFirst(status), + time: locale.displayTimer(timeTook, true), + }, + }), ); diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsViewRunData.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsViewRunData.vue index 92aa91d6d7..bbcad24f0e 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsViewRunData.vue +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsViewRunData.vue @@ -4,6 +4,7 @@ import { type LogEntry } from '@/components/RunDataAi/utils'; import { useI18n } from '@/composables/useI18n'; import { type IRunDataDisplayMode, type NodePanelType } from '@/Interface'; import { useNDVStore } from '@/stores/ndv.store'; +import { waitingNodeTooltip } from '@/utils/executionUtils'; import { N8nLink, N8nText } from '@n8n/design-system'; import { computed, ref } from 'vue'; import { I18nT } from 'vue-i18n'; @@ -42,6 +43,12 @@ const runDataProps = computed< overrideOutputs: [source.previousNodeOutput ?? 0], }; }); +const isExecuting = computed( + () => + paneType === 'output' && + (logEntry.runData.executionStatus === 'running' || + logEntry.runData.executionStatus === 'waiting'), +); function handleClickOpenNdv() { ndvStore.setActiveNodeName(logEntry.node.name); @@ -69,6 +76,7 @@ function handleChangeDisplayMode(value: IRunDataDisplayMode) { :disable-hover-highlight="true" :display-mode="displayMode" :disable-ai-content="logEntry.depth === 0" + :is-executing="isExecuting" table-header-bg-color="light" @display-mode-change="handleChangeDisplayMode" > @@ -84,6 +92,13 @@ function handleChangeDisplayMode(value: IRunDataDisplayMode) { + + diff --git a/packages/frontend/editor-ui/src/components/InputPanel.vue b/packages/frontend/editor-ui/src/components/InputPanel.vue index 0f2813c9ae..d10e9890b7 100644 --- a/packages/frontend/editor-ui/src/components/InputPanel.vue +++ b/packages/frontend/editor-ui/src/components/InputPanel.vue @@ -507,7 +507,9 @@ function activatePane() { diff --git a/packages/frontend/editor-ui/src/components/OutputPanel.vue b/packages/frontend/editor-ui/src/components/OutputPanel.vue index c1704bec9e..afb41ebe4c 100644 --- a/packages/frontend/editor-ui/src/components/OutputPanel.vue +++ b/packages/frontend/editor-ui/src/components/OutputPanel.vue @@ -402,7 +402,9 @@ const activatePane = () => { diff --git a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json b/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json index 8f171d628d..be8bc44859 100644 --- a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json @@ -1005,7 +1005,8 @@ "logs.overview.header.switch.overview": "Overview", "logs.overview.body.empty.message": "Nothing to display yet. Execute the workflow to see execution logs.", "logs.overview.body.empty.action": "Execute the workflow", - "logs.overview.body.summaryText": "{status} in {time}", + "logs.overview.body.summaryText.for": "{status} for {time}", + "logs.overview.body.summaryText.in": "{status} in {time}", "logs.overview.body.started": "Started {time}", "logs.overview.body.run": "Execute step", "logs.overview.body.open": "Open...", @@ -1136,11 +1137,12 @@ "ndv.output.run": "Run", "ndv.output.runNodeHint": "Execute this node to view data", "ndv.output.runNodeHintSubNode": "Output will appear here once the parent node is run", - "ndv.output.waitNodeWaitingForWebhook": "Execution will continue when webhook is received on ", "ndv.output.githubNodeWaitingForWebhook": "Execution will continue when the following webhook URL is called: ", "ndv.output.sendAndWaitWaitingApproval": "Execution will continue after the user's response", - "ndv.output.waitNodeWaitingForFormSubmission": "Execution will continue when form is submitted on ", - "ndv.output.waitNodeWaiting": "Execution will continue when wait time is over", + "ndv.output.waitNodeWaiting.title": "Waiting for input", + "ndv.output.waitNodeWaiting.description.webhook": "Execution will continue when webhook is received on ", + "ndv.output.waitNodeWaiting.description.form": "Execution will continue when form is submitted on ", + "ndv.output.waitNodeWaiting.description.timer": "Execution will continue when wait time is over", "ndv.output.insertTestData": "set mock data", "ndv.output.staleDataWarning.regular": "Node parameters have changed.
Test node again to refresh output.", "ndv.output.staleDataWarning.pinData": "Node parameter changes will not affect pinned output data.", diff --git a/packages/frontend/editor-ui/src/utils/executionUtils.test.ts b/packages/frontend/editor-ui/src/utils/executionUtils.test.ts index 1d9a704085..933bc3d430 100644 --- a/packages/frontend/editor-ui/src/utils/executionUtils.test.ts +++ b/packages/frontend/editor-ui/src/utils/executionUtils.test.ts @@ -33,9 +33,9 @@ vi.mock('@/plugins/i18n', () => ({ i18n: { baseText: (key: string, options?: { interpolate?: { error?: string; details?: string } }) => { const texts: { [key: string]: string } = { - 'ndv.output.waitNodeWaiting': 'Waiting for execution to resume...', - 'ndv.output.waitNodeWaitingForFormSubmission': 'Waiting for form submission: ', - 'ndv.output.waitNodeWaitingForWebhook': 'Waiting for webhook call: ', + 'ndv.output.waitNodeWaiting.description.timer': 'Waiting for execution to resume...', + 'ndv.output.waitNodeWaiting.description.form': 'Waiting for form submission: ', + 'ndv.output.waitNodeWaiting.description.webhook': 'Waiting for webhook call: ', 'ndv.output.githubNodeWaitingForWebhook': 'Waiting for webhook call: ', 'ndv.output.sendAndWaitWaitingApproval': 'Waiting for approval...', 'pushConnection.executionError': `Execution error${options?.interpolate?.error}`, diff --git a/packages/frontend/editor-ui/src/utils/executionUtils.ts b/packages/frontend/editor-ui/src/utils/executionUtils.ts index 4bc81926e5..751daa81cb 100644 --- a/packages/frontend/editor-ui/src/utils/executionUtils.ts +++ b/packages/frontend/editor-ui/src/utils/executionUtils.ts @@ -157,7 +157,7 @@ export const waitingNodeTooltip = (node: INodeUi | null | undefined) => { } if (resume) { if (!['webhook', 'form'].includes(resume as string)) { - return i18n.baseText('ndv.output.waitNodeWaiting'); + return i18n.baseText('ndv.output.waitNodeWaiting.description.timer'); } const { webhookSuffix } = (node.parameters.options ?? {}) as { webhookSuffix: string }; @@ -168,12 +168,12 @@ export const waitingNodeTooltip = (node: INodeUi | null | undefined) => { if (resume === 'form') { resumeUrl = `${useRootStore().formWaitingUrl}/${useWorkflowsStore().activeExecutionId}${suffix}`; - message = i18n.baseText('ndv.output.waitNodeWaitingForFormSubmission'); + message = i18n.baseText('ndv.output.waitNodeWaiting.description.form'); } if (resume === 'webhook') { resumeUrl = `${useRootStore().webhookWaitingUrl}/${useWorkflowsStore().activeExecutionId}${suffix}`; - message = i18n.baseText('ndv.output.waitNodeWaitingForWebhook'); + message = i18n.baseText('ndv.output.waitNodeWaiting.description.webhook'); } if (message && resumeUrl) { @@ -182,7 +182,7 @@ export const waitingNodeTooltip = (node: INodeUi | null | undefined) => { } if (node?.type === FORM_NODE_TYPE) { - const message = i18n.baseText('ndv.output.waitNodeWaitingForFormSubmission'); + const message = i18n.baseText('ndv.output.waitNodeWaiting.description.form'); const resumeUrl = `${useRootStore().formWaitingUrl}/${useWorkflowsStore().activeExecutionId}`; return `${message}${resumeUrl}`; }