diff --git a/cypress/e2e/50-logs.cy.ts b/cypress/e2e/50-logs.cy.ts index 5d20e834cb..f370bf33d4 100644 --- a/cypress/e2e/50-logs.cy.ts +++ b/cypress/e2e/50-logs.cy.ts @@ -194,6 +194,7 @@ describe('Logs', () => { it('should show logs for a workflow with a node that waits for webhook', () => { workflow.navigateToNewWorkflowPage(); workflow.pasteWorkflow(Workflow_wait_for_webhook); + workflow.getCanvas().click('topLeft'); // click canvas to deselect nodes workflow.clickZoomToFit(); logs.openLogsPanel(); @@ -202,7 +203,6 @@ describe('Logs', () => { workflow.getNodesWithSpinner().should('contain.text', 'Wait'); workflow.getWaitingNodes().should('contain.text', 'Wait'); logs.getLogEntries().should('have.length', 2); - logs.getLogEntries().eq(0).click(); // click selected row to deselect logs.getLogEntries().eq(1).should('contain.text', 'Wait node'); logs.getLogEntries().eq(1).should('contain.text', 'Waiting'); @@ -224,6 +224,7 @@ describe('Logs', () => { .getOverviewStatus() .contains(/Success in [\d\.]+m?s/) .should('exist'); + logs.getLogEntries().eq(1).click(); // click selected row to deselect logs.getLogEntries().should('have.length', 2); logs.getLogEntries().eq(1).should('contain.text', 'Wait node'); logs.getLogEntries().eq(1).should('contain.text', 'Success'); diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 4a6eab1bed..6d8bfd7dea 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -215,7 +215,7 @@ "chat.window.logsFromNode": "from {nodeName} node", "chat.window.noChatNode": "No Chat Node", "chat.window.noExecution": "Nothing got executed yet", - "chat.window.chat.placeholder": "Type a message, or press ‘up’ arrow for previous one", + "chat.window.chat.placeholder": "Type message, or press ‘up’ for prev one", "chat.window.chat.placeholderPristine": "Type a message", "chat.window.chat.sendButtonText": "Send", "chat.window.chat.provideMessage": "Please provide a message", diff --git a/packages/frontend/editor-ui/src/components/RunData.vue b/packages/frontend/editor-ui/src/components/RunData.vue index d24d617c5c..28ad161346 100644 --- a/packages/frontend/editor-ui/src/components/RunData.vue +++ b/packages/frontend/editor-ui/src/components/RunData.vue @@ -1601,10 +1601,12 @@ defineExpose({ enterEditMode });
-
+
+ +
{{ executingMessage }}
@@ -2302,6 +2304,12 @@ defineExpose({ enterEditMode }); } } +.executingMessage { + .compact & { + color: var(--color-text-light); + } +} + @container (max-width: 240px) { /* Hide title when the panel is too narrow */ .compact:hover .title { diff --git a/packages/frontend/editor-ui/src/components/RunDataParsedAiContent.vue b/packages/frontend/editor-ui/src/components/RunDataParsedAiContent.vue index 5aef8e702a..d187922474 100644 --- a/packages/frontend/editor-ui/src/components/RunDataParsedAiContent.vue +++ b/packages/frontend/editor-ui/src/components/RunDataParsedAiContent.vue @@ -175,6 +175,12 @@ function onCopyToClipboard(object: IDataObject | IDataObject[]) { font-size: var(--font-size-xs); } } + + p { + .compact & { + line-height: var(--font-line-height-xloose); + } + } } } @@ -202,7 +208,7 @@ function onCopyToClipboard(object: IDataObject | IDataObject[]) { .compact & { padding-top: 0; padding-inline: var(--spacing-2xs); - font-size: var(--font-size-xs); + font-size: var(--font-size-2xs); } } diff --git a/packages/frontend/editor-ui/src/composables/useKeybindings.ts b/packages/frontend/editor-ui/src/composables/useKeybindings.ts index 7bf1d7480a..f562dcf841 100644 --- a/packages/frontend/editor-ui/src/composables/useKeybindings.ts +++ b/packages/frontend/editor-ui/src/composables/useKeybindings.ts @@ -1,7 +1,7 @@ import { useActiveElement, useEventListener } from '@vueuse/core'; import { useDeviceSupport } from '@n8n/composables/useDeviceSupport'; import type { MaybeRef, Ref } from 'vue'; -import { computed, inject, unref } from 'vue'; +import { computed, inject, ref, unref } from 'vue'; import { PiPWindowSymbol } from '@/constants'; type KeyboardEventHandler = @@ -30,7 +30,7 @@ export const useKeybindings = ( disabled: MaybeRef; }, ) => { - const pipWindow = inject(PiPWindowSymbol); + const pipWindow = inject(PiPWindowSymbol, ref()); const activeElement = useActiveElement({ window: pipWindow?.value }); const { isCtrlKeyPressed } = useDeviceSupport(); diff --git a/packages/frontend/editor-ui/src/constants.ts b/packages/frontend/editor-ui/src/constants.ts index 77f4b250eb..56dd89cfc8 100644 --- a/packages/frontend/editor-ui/src/constants.ts +++ b/packages/frontend/editor-ui/src/constants.ts @@ -482,7 +482,7 @@ export const LOCAL_STORAGE_HIDE_GITHUB_STAR_BUTTON = 'N8N_HIDE_HIDE_GITHUB_STAR_ export const LOCAL_STORAGE_NDV_INPUT_PANEL_DISPLAY_MODE = 'N8N_NDV_INPUT_PANEL_DISPLAY_MODE'; export const LOCAL_STORAGE_NDV_OUTPUT_PANEL_DISPLAY_MODE = 'N8N_NDV_OUTPUT_PANEL_DISPLAY_MODE'; export const LOCAL_STORAGE_LOGS_PANEL_OPEN = 'N8N_LOGS_PANEL_OPEN'; -export const LOCAL_STORAGE_LOGS_SYNC_SELECTION = 'N8N_LOGS_SYNC_SELECTION'; +export const LOCAL_STORAGE_LOGS_SYNC_SELECTION = 'N8N_LOGS_SYNC_SELECTION_ENABLED'; export const LOCAL_STORAGE_LOGS_PANEL_DETAILS_PANEL = 'N8N_LOGS_DETAILS_PANEL'; export const LOCAL_STORAGE_LOGS_PANEL_DETAILS_PANEL_SUB_NODE = 'N8N_LOGS_DETAILS_PANEL_SUB_NODE'; export const LOCAL_STORAGE_WORKFLOW_LIST_PREFERENCES_KEY = 'N8N_WORKFLOWS_LIST_PREFERENCES'; diff --git a/packages/frontend/editor-ui/src/features/logs/__test__/data.ts b/packages/frontend/editor-ui/src/features/logs/__test__/data.ts index 9f011ee86a..e4f7a3efca 100644 --- a/packages/frontend/editor-ui/src/features/logs/__test__/data.ts +++ b/packages/frontend/editor-ui/src/features/logs/__test__/data.ts @@ -20,7 +20,7 @@ export function createTestLogTreeCreationContext( workflows: {}, subWorkflowData: {}, executionId: 'test-execution-id', - depth: 0, + ancestorRunIndexes: [], data: { resultData: { runData, diff --git a/packages/frontend/editor-ui/src/features/logs/__test__/mocks.ts b/packages/frontend/editor-ui/src/features/logs/__test__/mocks.ts index 13a1933cef..6f971f9191 100644 --- a/packages/frontend/editor-ui/src/features/logs/__test__/mocks.ts +++ b/packages/frontend/editor-ui/src/features/logs/__test__/mocks.ts @@ -17,7 +17,6 @@ export function createTestLogEntry(data: Partial = {}): LogEntry { id: uuid(), children: [], consumedTokens: { completionTokens: 0, totalTokens: 0, promptTokens: 0, isEstimate: false }, - depth: 0, workflow: createTestWorkflowObject(), executionId, execution: createTestWorkflowExecutionResponse({ id: executionId }).data!, diff --git a/packages/frontend/editor-ui/src/features/logs/components/ChatMessagesPanel.vue b/packages/frontend/editor-ui/src/features/logs/components/ChatMessagesPanel.vue index 0819bdee52..971593b25a 100644 --- a/packages/frontend/editor-ui/src/features/logs/components/ChatMessagesPanel.vue +++ b/packages/frontend/editor-ui/src/features/logs/components/ChatMessagesPanel.vue @@ -255,7 +255,7 @@ async function copySessionId() { .chat { --chat--spacing: var(--spacing-xs); --chat--message--padding: var(--spacing-2xs); - --chat--message--font-size: var(--font-size-xs); + --chat--message--font-size: var(--font-size-2xs); --chat--input--font-size: var(--font-size-s); --chat--input--placeholder--font-size: var(--font-size-xs); --chat--message--bot--background: transparent; @@ -269,7 +269,10 @@ async function copySessionId() { --chat--color-typing: var(--color-text-light); --chat--textarea--max-height: calc(var(--panel-height) * 0.3); --chat--message--pre--background: var(--color-foreground-light); - --chat--textarea--height: 2.5rem; + --chat--textarea--height: calc( + var(--chat--input--padding) * 2 + var(--chat--input--font-size) * + var(--chat--input--line-height) + ); height: 100%; display: flex; flex-direction: column; @@ -381,4 +384,12 @@ async function copySessionId() { --input-border-color: #4538a3; } } + +.messagesHistory { + height: var(--chat--textarea--height); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/packages/frontend/editor-ui/src/features/logs/components/LogsOverviewPanel.test.ts b/packages/frontend/editor-ui/src/features/logs/components/LogsOverviewPanel.test.ts index a0afcafd20..19e743f44f 100644 --- a/packages/frontend/editor-ui/src/features/logs/components/LogsOverviewPanel.test.ts +++ b/packages/frontend/editor-ui/src/features/logs/components/LogsOverviewPanel.test.ts @@ -93,7 +93,8 @@ describe('LogsOverviewPanel', () => { const row1 = within(tree.queryAllByRole('treeitem')[0]); expect(row1.queryByText('AI Agent')).toBeInTheDocument(); - expect(row1.queryByText('Success in 1.778s')).toBeInTheDocument(); + expect(row1.queryByText('Success')).toBeInTheDocument(); + expect(row1.queryByText('in 1.778s')).toBeInTheDocument(); expect(row1.queryByText('Started 00:00:00.002, 26 Mar')).toBeInTheDocument(); const row2 = within(tree.queryAllByRole('treeitem')[1]); diff --git a/packages/frontend/editor-ui/src/features/logs/components/LogsOverviewPanel.vue b/packages/frontend/editor-ui/src/features/logs/components/LogsOverviewPanel.vue index 94605b6ceb..2118be84b4 100644 --- a/packages/frontend/editor-ui/src/features/logs/components/LogsOverviewPanel.vue +++ b/packages/frontend/editor-ui/src/features/logs/components/LogsOverviewPanel.vue @@ -8,14 +8,11 @@ import LogsOverviewRow from '@/features/logs/components/LogsOverviewRow.vue'; import { useRunWorkflow } from '@/composables/useRunWorkflow'; import { useRouter } from 'vue-router'; import LogsViewExecutionSummary from '@/features/logs/components/LogsViewExecutionSummary.vue'; -import { - getSubtreeTotalConsumedTokens, - getTotalConsumedTokens, - hasSubExecution, -} from '@/features/logs/logs.utils'; +import { getSubtreeTotalConsumedTokens, getTotalConsumedTokens } from '@/features/logs/logs.utils'; import { useVirtualList } from '@vueuse/core'; import { type IExecutionResponse } from '@/Interface'; import type { LatestNodeInfo, LogEntry } from '@/features/logs/logs.types'; +import { getScrollbarWidth } from '@/utils/htmlUtils'; const { isOpen, @@ -43,7 +40,6 @@ const emit = defineEmits<{ clearExecutionData: []; openNdv: [LogEntry]; toggleExpanded: [LogEntry]; - loadSubExecution: [LogEntry]; }>(); defineSlots<{ actions: {} }>(); @@ -57,6 +53,7 @@ const switchViewOptions = computed(() => [ { label: locale.baseText('logs.overview.header.switch.overview'), value: 'overview' as const }, { label: locale.baseText('logs.overview.header.switch.details'), value: 'details' as const }, ]); +const hasStaticScrollbar = getScrollbarWidth() > 0; const consumedTokens = computed(() => getTotalConsumedTokens( ...entries.map((entry) => @@ -73,6 +70,12 @@ const shouldShowTokenCountColumn = computed( consumedTokens.value.totalTokens > 0 || entries.some((entry) => getSubtreeTotalConsumedTokens(entry, true).totalTokens > 0), ); +const isExpanded = computed(() => + flatLogEntries.reduce>((acc, entry, index, arr) => { + acc[entry.id] = arr[index + 1]?.parent?.id === entry.id; + return acc; + }, {}), +); const virtualList = useVirtualList( toRef(() => flatLogEntries), { itemHeight: 32 }, @@ -82,14 +85,6 @@ function handleSwitchView(value: 'overview' | 'details') { emit('select', value === 'overview' ? undefined : flatLogEntries[0]); } -function handleToggleExpanded(treeNode: LogEntry) { - if (hasSubExecution(treeNode) && treeNode.children.length === 0) { - emit('loadSubExecution', treeNode); - return; - } - emit('toggleExpanded', treeNode); -} - async function handleTriggerPartialExecution(treeNode: LogEntry) { const latestName = latestNodeInfo[treeNode.node.id]?.name ?? treeNode.node.name; @@ -98,25 +93,46 @@ async function handleTriggerPartialExecution(treeNode: LogEntry) { } } +// While executing, scroll to the bottom if there's no selection +watch( + [() => execution?.status === 'running', () => flatLogEntries.length], + async ([isRunning, flatEntryCount], [wasRunning]) => { + await nextTick(() => { + if (selected === undefined && (isRunning || wasRunning)) { + virtualList.scrollTo(flatEntryCount - 1); + } + }); + }, + { immediate: true }, +); + // Scroll selected row into view watch( - () => selected, - async (selection) => { - if (selection && virtualList.list.value.every((e) => e.data.id !== selection.id)) { - const index = flatLogEntries.findIndex((e) => e.id === selection?.id); + () => selected?.id, + async (selectedId) => { + await nextTick(() => { + if (selectedId === undefined) { + return; + } + + const index = virtualList.list.value.some((e) => e.data.id === selectedId) + ? -1 + : flatLogEntries.findIndex((e) => e.id === selectedId); if (index >= 0) { - // Wait for the node to be added to the list, and then scroll - await nextTick(() => virtualList.scrollTo(index)); + virtualList.scrollTo(index); } - } + }); }, { immediate: true }, );