mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user