mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
feat(editor): "Executing" state in the output panel (#15470)
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
<N8nText v-if="!isCompact" tag="div" color="text-light" size="small" :class="$style.timeTook">
|
||||
<I18nT v-if="isSettled" keypath="logs.overview.body.summaryText">
|
||||
<I18nT v-if="isSettled" keypath="logs.overview.body.summaryText.in">
|
||||
<template #status>
|
||||
<N8nText v-if="isError" color="danger" :bold="true" size="small">
|
||||
<N8nIcon icon="exclamation-triangle" :class="$style.errorIcon" />{{
|
||||
upperFirst(props.data.runData.executionStatus)
|
||||
}}
|
||||
<N8nIcon icon="exclamation-triangle" :class="$style.errorIcon" />
|
||||
{{ statusText }}
|
||||
</N8nText>
|
||||
<template v-else>{{ upperFirst(props.data.runData.executionStatus) }}</template>
|
||||
<template v-else>{{ statusText }}</template>
|
||||
</template>
|
||||
<template #time>{{ locale.displayTimer(props.data.runData.executionTime, true) }}</template>
|
||||
<template #time>{{ timeText }}</template>
|
||||
</I18nT>
|
||||
<template v-else>{{ upperFirst(props.data.runData.executionStatus) }}</template></N8nText
|
||||
>
|
||||
<template v-else>
|
||||
{{
|
||||
locale.baseText('logs.overview.body.summaryText.for', {
|
||||
interpolate: { status: statusText, time: timeText },
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
</N8nText>
|
||||
<N8nText
|
||||
v-if="!isCompact"
|
||||
tag="div"
|
||||
|
||||
@@ -3,26 +3,36 @@ import LogsViewConsumedTokenCountText from '@/components/CanvasChat/future/compo
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { type LlmTokenUsageData } from '@/Interface';
|
||||
import { N8nText } from '@n8n/design-system';
|
||||
import { useTimestamp } from '@vueuse/core';
|
||||
import { upperFirst } from 'lodash-es';
|
||||
import { type ExecutionStatus } from 'n8n-workflow';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const { status, consumedTokens, timeTook } = defineProps<{
|
||||
const { status, consumedTokens, startTime, timeTook } = defineProps<{
|
||||
status: ExecutionStatus;
|
||||
consumedTokens: LlmTokenUsageData;
|
||||
startTime: number;
|
||||
timeTook?: number;
|
||||
}>();
|
||||
|
||||
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),
|
||||
},
|
||||
}),
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
</N8nText>
|
||||
</template>
|
||||
|
||||
<template #node-waiting>
|
||||
<N8nText :bold="true" color="text-dark" size="large">
|
||||
{{ locale.baseText('ndv.output.waitNodeWaiting.title') }}
|
||||
</N8nText>
|
||||
<N8nText v-n8n-html="waitingNodeTooltip(logEntry.node)"></N8nText>
|
||||
</template>
|
||||
|
||||
<template v-if="isMultipleInput" #content>
|
||||
<!-- leave empty -->
|
||||
</template>
|
||||
|
||||
@@ -507,7 +507,9 @@ function activatePane() {
|
||||
</template>
|
||||
|
||||
<template #node-waiting>
|
||||
<N8nText :bold="true" color="text-dark" size="large">Waiting for input</N8nText>
|
||||
<N8nText :bold="true" color="text-dark" size="large">
|
||||
{{ i18n.baseText('ndv.output.waitNodeWaiting.title') }}
|
||||
</N8nText>
|
||||
<N8nText v-n8n-html="waitingMessage"></N8nText>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -402,7 +402,9 @@ const activatePane = () => {
|
||||
</template>
|
||||
|
||||
<template #node-waiting>
|
||||
<N8nText :bold="true" color="text-dark" size="large">Waiting for input</N8nText>
|
||||
<N8nText :bold="true" color="text-dark" size="large">
|
||||
{{ i18n.baseText('ndv.output.waitNodeWaiting.title') }}
|
||||
</N8nText>
|
||||
<N8nText v-n8n-html="waitingNodeTooltip(node)"></N8nText>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -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.<br>Test node again to refresh output.",
|
||||
"ndv.output.staleDataWarning.pinData": "Node parameter changes will not affect pinned output data.",
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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}<a href="${resumeUrl}" target="_blank">${resumeUrl}</a>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user