diff --git a/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts b/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts index eca3af81fb..79f33b841c 100644 --- a/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts +++ b/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts @@ -90,6 +90,14 @@ function createRunDataWithError(inputMessage: string) { routine: 'InitPostgres', } as unknown as Error, } as ExecutionError, + metadata: { + subRun: [ + { + node: 'Postgres Chat Memory', + runIndex: 0, + }, + ], + }, }), createMockNodeExecutionData(AGENT_NODE_NAME, { executionStatus: 'error', @@ -124,14 +132,6 @@ function createRunDataWithError(inputMessage: string) { description: 'Internal error', message: 'Internal error', } as unknown as ExecutionError, - metadata: { - subRun: [ - { - node: 'Postgres Chat Memory', - runIndex: 0, - }, - ], - }, }), ]; } diff --git a/cypress/e2e/30-langchain.cy.ts b/cypress/e2e/30-langchain.cy.ts index 0deec76e9f..e23b7e4da3 100644 --- a/cypress/e2e/30-langchain.cy.ts +++ b/cypress/e2e/30-langchain.cy.ts @@ -278,6 +278,9 @@ describe('Langchain Integration', () => { }, }, }, + metadata: { + subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }], + }, inputOverride: { ai_languageModel: [ [ @@ -316,9 +319,6 @@ describe('Langchain Integration', () => { jsonData: { main: { output: 'Hi there! How can I assist you today?' }, }, - metadata: { - subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }], - }, }), ], lastNodeExecuted: AGENT_NODE_NAME, diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts index 416a28b655..77df60da79 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts @@ -20,6 +20,10 @@ const modelField: INodeProperties = { type: 'options', // eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items options: [ + { + name: 'Claude 3.5 Sonnet(20241022)', + value: 'claude-3-5-sonnet-20241022', + }, { name: 'Claude 3 Opus(20240229)', value: 'claude-3-opus-20240229', diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolVectorStore/ToolVectorStore.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolVectorStore/ToolVectorStore.node.ts index b4016f06ca..6f4aa19fb3 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolVectorStore/ToolVectorStore.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolVectorStore/ToolVectorStore.node.ts @@ -63,7 +63,7 @@ export class ToolVectorStore implements INodeType { name: 'name', type: 'string', default: '', - placeholder: 'e.g. state_of_union_address', + placeholder: 'e.g. company_knowledge_base', validateType: 'string-alphanumeric', description: 'Name of the vector store', }, @@ -72,7 +72,7 @@ export class ToolVectorStore implements INodeType { name: 'description', type: 'string', default: '', - placeholder: 'The most recent state of the Union address', + placeholder: 'Retrieves data about [insert information about your data here]...', typeOptions: { rows: 3, }, diff --git a/packages/cli/src/scaling/__tests__/publisher.service.test.ts b/packages/cli/src/scaling/__tests__/publisher.service.test.ts index f69ad08cb5..fb0a340c11 100644 --- a/packages/cli/src/scaling/__tests__/publisher.service.test.ts +++ b/packages/cli/src/scaling/__tests__/publisher.service.test.ts @@ -65,6 +65,42 @@ describe('Publisher', () => { JSON.stringify({ ...msg, senderId: hostId, selfSend: false, debounce: true }), ); }); + + it('should not debounce `add-webhooks-triggers-and-pollers`', async () => { + const publisher = new Publisher(logger, redisClientService, instanceSettings); + const msg = mock({ command: 'add-webhooks-triggers-and-pollers' }); + + await publisher.publishCommand(msg); + + expect(client.publish).toHaveBeenCalledWith( + 'n8n.commands', + JSON.stringify({ + ...msg, + _isMockObject: true, + senderId: hostId, + selfSend: true, + debounce: false, + }), + ); + }); + + it('should not debounce `remove-triggers-and-pollers`', async () => { + const publisher = new Publisher(logger, redisClientService, instanceSettings); + const msg = mock({ command: 'remove-triggers-and-pollers' }); + + await publisher.publishCommand(msg); + + expect(client.publish).toHaveBeenCalledWith( + 'n8n.commands', + JSON.stringify({ + ...msg, + _isMockObject: true, + senderId: hostId, + selfSend: true, + debounce: false, + }), + ); + }); }); describe('publishWorkerResponse', () => { diff --git a/packages/cli/src/scaling/constants.ts b/packages/cli/src/scaling/constants.ts index e56596e4a0..336da006d8 100644 --- a/packages/cli/src/scaling/constants.ts +++ b/packages/cli/src/scaling/constants.ts @@ -1,3 +1,5 @@ +import type { PubSub } from './pubsub/pubsub.types'; + export const QUEUE_NAME = 'jobs'; export const JOB_TYPE_NAME = 'job'; @@ -11,7 +13,7 @@ export const WORKER_RESPONSE_PUBSUB_CHANNEL = 'n8n.worker-response'; /** * Commands that should be sent to the sender as well, e.g. during workflow activation and * deactivation in multi-main setup. */ -export const SELF_SEND_COMMANDS = new Set([ +export const SELF_SEND_COMMANDS = new Set([ 'add-webhooks-triggers-and-pollers', 'remove-triggers-and-pollers', ]); @@ -20,7 +22,8 @@ export const SELF_SEND_COMMANDS = new Set([ * Commands that should not be debounced when received, e.g. during webhook handling in * multi-main setup. */ -export const IMMEDIATE_COMMANDS = new Set([ +export const IMMEDIATE_COMMANDS = new Set([ 'add-webhooks-triggers-and-pollers', + 'remove-triggers-and-pollers', 'relay-execution-lifecycle-event', ]); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue b/packages/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue index 0c2816e2d8..728215c02c 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue @@ -29,6 +29,7 @@ import CategorizedItemsRenderer from '../Renderers/CategorizedItemsRenderer.vue' import type { IDataObject } from 'n8n-workflow'; import { useTelemetry } from '@/composables/useTelemetry'; import { useI18n } from '@/composables/useI18n'; +import { useNodeCreatorStore } from '@/stores/nodeCreator.store'; const emit = defineEmits<{ nodeTypeSelected: [value: [actionKey: string, nodeName: string] | [nodeName: string]]; @@ -47,6 +48,8 @@ const { actionsCategoryLocales, } = useActions(); +const nodeCreatorStore = useNodeCreatorStore(); + // We only inject labels if search is empty const parsedTriggerActions = computed(() => parseActions(actions.value, actionsCategoryLocales.value.triggers, false), @@ -182,7 +185,7 @@ function trackActionsView() { }; void useExternalHooks().run('nodeCreateList.onViewActions', trackingPayload); - telemetry?.trackNodesPanel('nodeCreateList.onViewActions', trackingPayload); + nodeCreatorStore.onViewActions(trackingPayload); } function resetSearch() { @@ -206,7 +209,7 @@ function addHttpNode() { void useExternalHooks().run('nodeCreateList.onActionsCustmAPIClicked', { app_identifier, }); - telemetry?.trackNodesPanel('nodeCreateList.onActionsCustmAPIClicked', { app_identifier }); + nodeCreatorStore.onActionsCustomAPIClicked({ app_identifier }); } // Anonymous component to handle triggers and actions rendering order diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue b/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue index 08f8d14cbb..ce833afa17 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue @@ -23,7 +23,6 @@ import ItemsRenderer from '../Renderers/ItemsRenderer.vue'; import CategorizedItemsRenderer from '../Renderers/CategorizedItemsRenderer.vue'; import NoResults from '../Panel/NoResults.vue'; import { useI18n } from '@/composables/useI18n'; -import { useTelemetry } from '@/composables/useTelemetry'; import { getNodeIcon, getNodeIconColor, getNodeIconUrl } from '@/utils/nodeTypesUtils'; import { useUIStore } from '@/stores/ui.store'; @@ -36,11 +35,10 @@ const emit = defineEmits<{ }>(); const i18n = useI18n(); -const telemetry = useTelemetry(); const uiStore = useUIStore(); const rootStore = useRootStore(); -const { mergedNodes, actions } = useNodeCreatorStore(); +const { mergedNodes, actions, onSubcategorySelected } = useNodeCreatorStore(); const { pushViewStack, popViewStack } = useViewStacks(); const { registerKeyHook } = useKeyboardNavigation(); @@ -83,7 +81,7 @@ function onSelected(item: INodeCreateElement) { sections: item.properties.sections, }); - telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', { + onSubcategorySelected({ subcategory: item.key, }); } @@ -153,9 +151,6 @@ function onSelected(item: INodeCreateElement) { if (item.type === 'link') { window.open(item.properties.url, '_blank'); - telemetry.trackNodesPanel('nodeCreateList.onLinkSelected', { - link: item.properties.url, - }); } } diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue b/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue index 72e31851f9..6cb414515d 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue @@ -17,12 +17,15 @@ import SearchBar from './SearchBar.vue'; import ActionsRenderer from '../Modes/ActionsMode.vue'; import NodesRenderer from '../Modes/NodesMode.vue'; import { useI18n } from '@/composables/useI18n'; +import { useDebounce } from '@/composables/useDebounce'; const i18n = useI18n(); +const { callDebounced } = useDebounce(); const { mergedNodes } = useNodeCreatorStore(); const { pushViewStack, popViewStack, updateCurrentViewStack } = useViewStacks(); const { setActiveItemIndex, attachKeydownEvent, detachKeydownEvent } = useKeyboardNavigation(); +const nodeCreatorStore = useNodeCreatorStore(); const activeViewStack = computed(() => useViewStacks().activeViewStack); @@ -55,6 +58,19 @@ function onSearch(value: string) { if (activeViewStack.value.uuid) { updateCurrentViewStack({ search: value }); void setActiveItemIndex(getDefaultActiveIndex(value)); + if (value.length) { + callDebounced( + nodeCreatorStore.onNodeFilterChanged, + { trailing: true, debounceTime: 2000 }, + { + newValue: value, + filteredNodes: activeViewStack.value.items ?? [], + filterMode: activeViewStack.value.rootView ?? 'Regular', + subcategory: activeViewStack.value.subcategory, + title: activeViewStack.value.title, + }, + ); + } } } @@ -299,6 +315,7 @@ function onBackButton() { margin-top: var(--spacing-4xs); font-size: var(--font-size-s); line-height: 19px; + color: var(--color-text-base); font-weight: var(--font-weight-regular); } diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Renderers/CategorizedItemsRenderer.vue b/packages/editor-ui/src/components/Node/NodeCreator/Renderers/CategorizedItemsRenderer.vue index 93b542c72a..b493d53680 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/Renderers/CategorizedItemsRenderer.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Renderers/CategorizedItemsRenderer.vue @@ -8,7 +8,7 @@ import { useKeyboardNavigation } from '../composables/useKeyboardNavigation'; import { useViewStacks } from '../composables/useViewStacks'; import ItemsRenderer from './ItemsRenderer.vue'; import CategoryItem from '../ItemTypes/CategoryItem.vue'; -import { useTelemetry } from '@/composables/useTelemetry'; +import { useNodeCreatorStore } from '@/stores/nodeCreator.store'; export interface Props { elements: INodeCreateElement[]; @@ -24,10 +24,10 @@ const props = withDefaults(defineProps(), { elements: () => [], }); -const telemetry = useTelemetry(); const { popViewStack } = useViewStacks(); const { registerKeyHook } = useKeyboardNavigation(); const { workflowId } = useWorkflowsStore(); +const nodeCreatorStore = useNodeCreatorStore(); const activeItemId = computed(() => useKeyboardNavigation()?.activeItemId); const actionCount = computed(() => props.elements.filter(({ type }) => type === 'action').length); @@ -38,10 +38,11 @@ function toggleExpanded() { } function setExpanded(isExpanded: boolean) { + const prev = expanded.value; expanded.value = isExpanded; - if (expanded.value) { - telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', { + if (expanded.value && !prev) { + nodeCreatorStore.onCategoryExpanded({ category_name: props.category, workflow_id: workflowId, }); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts index e9f7917385..87be1105b7 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts @@ -332,7 +332,11 @@ export const useActions = () => { return storeWatcher; } - function trackActionSelected(action: IUpdateInformation, telemetry: Telemetry, rootView: string) { + function trackActionSelected( + action: IUpdateInformation, + _telemetry: Telemetry, + rootView: string, + ) { const payload = { node_type: action.key, action: action.name, @@ -340,7 +344,7 @@ export const useActions = () => { resource: (action.value as INodeParameters).resource || '', }; void useExternalHooks().run('nodeCreateList.addAction', payload); - telemetry?.trackNodesPanel('nodeCreateList.addAction', payload); + useNodeCreatorStore().onAddActions(payload); } return { diff --git a/packages/editor-ui/src/components/OutputPanel.vue b/packages/editor-ui/src/components/OutputPanel.vue index bf3cbbe6eb..4c513c07d1 100644 --- a/packages/editor-ui/src/components/OutputPanel.vue +++ b/packages/editor-ui/src/components/OutputPanel.vue @@ -100,14 +100,15 @@ const isTriggerNode = computed(() => { }); const hasAiMetadata = computed(() => { + if (isNodeRunning.value || !workflowRunData.value) { + return false; + } + if (node.value) { - const resultData = workflowsStore.getWorkflowResultDataByNodeName(node.value.name); + const connectedSubNodes = props.workflow.getParentNodes(node.value.name, 'ALL_NON_MAIN'); + const resultData = connectedSubNodes.map(workflowsStore.getWorkflowResultDataByNodeName); - if (!resultData || !Array.isArray(resultData) || resultData.length === 0) { - return false; - } - - return !!resultData[resultData.length - 1].metadata; + return resultData && Array.isArray(resultData) && resultData.length > 0; } return false; }); @@ -295,6 +296,7 @@ const activatePane = () => { :block-u-i="blockUI" :is-production-execution-preview="isProductionExecutionPreview" :is-pane-active="isPaneActive" + :hide-pagination="outputMode === 'logs'" pane-type="output" :data-output-type="outputMode" @activate-pane="activatePane" @@ -368,7 +370,7 @@ const activatePane = () => {