mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 21:07:28 -08:00
Merge branch 'master' into launcher-handshake
This commit is contained in:
commit
0715ec2512
|
@ -72,6 +72,10 @@ export function getOutputPanelTable() {
|
||||||
return getOutputPanelDataContainer().get('table');
|
return getOutputPanelDataContainer().get('table');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRunDataInfoCallout() {
|
||||||
|
return cy.getByTestId('run-data-callout');
|
||||||
|
}
|
||||||
|
|
||||||
export function getOutputPanelItemsCount() {
|
export function getOutputPanelItemsCount() {
|
||||||
return getOutputPanel().getByTestId('ndv-items-count');
|
return getOutputPanel().getByTestId('ndv-items-count');
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,8 @@ import {
|
||||||
clickCreateNewCredential,
|
clickCreateNewCredential,
|
||||||
clickExecuteNode,
|
clickExecuteNode,
|
||||||
clickGetBackToCanvas,
|
clickGetBackToCanvas,
|
||||||
|
getRunDataInfoCallout,
|
||||||
|
getOutputPanelTable,
|
||||||
toggleParameterCheckboxInputByName,
|
toggleParameterCheckboxInputByName,
|
||||||
} from '../composables/ndv';
|
} from '../composables/ndv';
|
||||||
import {
|
import {
|
||||||
|
@ -418,4 +420,102 @@ describe('Langchain Integration', () => {
|
||||||
assertInputOutputText('Berlin', 'not.exist');
|
assertInputOutputText('Berlin', 'not.exist');
|
||||||
assertInputOutputText('Kyiv', 'not.exist');
|
assertInputOutputText('Kyiv', 'not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show tool info notice if no existing tools were used during execution', () => {
|
||||||
|
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
|
||||||
|
addNodeToCanvas(AGENT_NODE_NAME, true);
|
||||||
|
|
||||||
|
addLanguageModelNodeToParent(
|
||||||
|
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||||
|
AGENT_NODE_NAME,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
clickCreateNewCredential();
|
||||||
|
setCredentialValues({
|
||||||
|
apiKey: 'sk_test_123',
|
||||||
|
});
|
||||||
|
clickGetBackToCanvas();
|
||||||
|
|
||||||
|
addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME);
|
||||||
|
clickGetBackToCanvas();
|
||||||
|
openNode(AGENT_NODE_NAME);
|
||||||
|
|
||||||
|
const inputMessage = 'Hello!';
|
||||||
|
const outputMessage = 'Hi there! How can I assist you today?';
|
||||||
|
|
||||||
|
clickExecuteNode();
|
||||||
|
|
||||||
|
runMockWorkflowExecution({
|
||||||
|
trigger: () => sendManualChatMessage(inputMessage),
|
||||||
|
runData: [
|
||||||
|
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||||
|
jsonData: {
|
||||||
|
main: { output: outputMessage },
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
lastNodeExecuted: AGENT_NODE_NAME,
|
||||||
|
});
|
||||||
|
closeManualChatModal();
|
||||||
|
openNode(AGENT_NODE_NAME);
|
||||||
|
|
||||||
|
getRunDataInfoCallout().should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show tool info notice if tools were used during execution', () => {
|
||||||
|
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
|
||||||
|
addNodeToCanvas(AGENT_NODE_NAME, true, true);
|
||||||
|
getRunDataInfoCallout().should('not.exist');
|
||||||
|
clickGetBackToCanvas();
|
||||||
|
|
||||||
|
addLanguageModelNodeToParent(
|
||||||
|
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||||
|
AGENT_NODE_NAME,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
clickCreateNewCredential();
|
||||||
|
setCredentialValues({
|
||||||
|
apiKey: 'sk_test_123',
|
||||||
|
});
|
||||||
|
clickGetBackToCanvas();
|
||||||
|
|
||||||
|
addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME);
|
||||||
|
clickGetBackToCanvas();
|
||||||
|
openNode(AGENT_NODE_NAME);
|
||||||
|
|
||||||
|
getRunDataInfoCallout().should('not.exist');
|
||||||
|
|
||||||
|
const inputMessage = 'Hello!';
|
||||||
|
const outputMessage = 'Hi there! How can I assist you today?';
|
||||||
|
|
||||||
|
clickExecuteNode();
|
||||||
|
|
||||||
|
runMockWorkflowExecution({
|
||||||
|
trigger: () => sendManualChatMessage(inputMessage),
|
||||||
|
runData: [
|
||||||
|
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||||
|
jsonData: {
|
||||||
|
main: { output: outputMessage },
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
createMockNodeExecutionData(AI_TOOL_CALCULATOR_NODE_NAME, {}),
|
||||||
|
],
|
||||||
|
lastNodeExecuted: AGENT_NODE_NAME,
|
||||||
|
});
|
||||||
|
|
||||||
|
closeManualChatModal();
|
||||||
|
openNode(AGENT_NODE_NAME);
|
||||||
|
// This waits to ensure the output panel is rendered
|
||||||
|
getOutputPanelTable();
|
||||||
|
|
||||||
|
getRunDataInfoCallout().should('not.exist');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { WorkflowPage } from '../pages/workflow';
|
||||||
|
|
||||||
const workflowPage = new WorkflowPage();
|
const workflowPage = new WorkflowPage();
|
||||||
|
|
||||||
const NOW = 1717771477012;
|
const NOW = Date.now();
|
||||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||||
const THREE_DAYS = ONE_DAY * 3;
|
const THREE_DAYS = ONE_DAY * 3;
|
||||||
const SEVEN_DAYS = ONE_DAY * 7;
|
const SEVEN_DAYS = ONE_DAY * 7;
|
||||||
|
|
|
@ -557,6 +557,8 @@ describe('General help', () => {
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
|
|
||||||
aiAssistant.getters.askAssistantFloatingButton().click();
|
aiAssistant.getters.askAssistantFloatingButton().click();
|
||||||
|
wf.getters.zoomToFitButton().click();
|
||||||
|
|
||||||
aiAssistant.actions.sendMessage('What is wrong with this workflow?');
|
aiAssistant.actions.sendMessage('What is wrong with this workflow?');
|
||||||
cy.wait('@chatRequest');
|
cy.wait('@chatRequest');
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,13 @@ export class AzureOpenAiApi implements ICredentialType {
|
||||||
required: true,
|
required: true,
|
||||||
default: '2023-07-01-preview',
|
default: '2023-07-01-preview',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Endpoint',
|
||||||
|
name: 'endpoint',
|
||||||
|
type: 'string',
|
||||||
|
default: undefined,
|
||||||
|
placeholder: 'https://westeurope.api.cognitive.microsoft.com',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
authenticate: IAuthenticateGeneric = {
|
authenticate: IAuthenticateGeneric = {
|
||||||
|
|
|
@ -128,6 +128,7 @@ export class EmbeddingsAzureOpenAi implements INodeType {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
resourceName: string;
|
resourceName: string;
|
||||||
apiVersion: string;
|
apiVersion: string;
|
||||||
|
endpoint?: string;
|
||||||
}>('azureOpenAiApi');
|
}>('azureOpenAiApi');
|
||||||
const modelName = this.getNodeParameter('model', itemIndex) as string;
|
const modelName = this.getNodeParameter('model', itemIndex) as string;
|
||||||
|
|
||||||
|
@ -144,9 +145,15 @@ export class EmbeddingsAzureOpenAi implements INodeType {
|
||||||
|
|
||||||
const embeddings = new OpenAIEmbeddings({
|
const embeddings = new OpenAIEmbeddings({
|
||||||
azureOpenAIApiDeploymentName: modelName,
|
azureOpenAIApiDeploymentName: modelName,
|
||||||
azureOpenAIApiInstanceName: credentials.resourceName,
|
// instance name only needed to set base url
|
||||||
|
azureOpenAIApiInstanceName: !credentials.endpoint ? credentials.resourceName : undefined,
|
||||||
azureOpenAIApiKey: credentials.apiKey,
|
azureOpenAIApiKey: credentials.apiKey,
|
||||||
azureOpenAIApiVersion: credentials.apiVersion,
|
azureOpenAIApiVersion: credentials.apiVersion,
|
||||||
|
// azureOpenAIEndpoint and configuration.baseURL are both ignored here
|
||||||
|
// only setting azureOpenAIBasePath worked
|
||||||
|
azureOpenAIBasePath: credentials.endpoint
|
||||||
|
? `${credentials.endpoint}/openai/deployments`
|
||||||
|
: undefined,
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -168,6 +168,7 @@ export class LmChatAzureOpenAi implements INodeType {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
resourceName: string;
|
resourceName: string;
|
||||||
apiVersion: string;
|
apiVersion: string;
|
||||||
|
endpoint?: string;
|
||||||
}>('azureOpenAiApi');
|
}>('azureOpenAiApi');
|
||||||
|
|
||||||
const modelName = this.getNodeParameter('model', itemIndex) as string;
|
const modelName = this.getNodeParameter('model', itemIndex) as string;
|
||||||
|
@ -184,9 +185,11 @@ export class LmChatAzureOpenAi implements INodeType {
|
||||||
|
|
||||||
const model = new ChatOpenAI({
|
const model = new ChatOpenAI({
|
||||||
azureOpenAIApiDeploymentName: modelName,
|
azureOpenAIApiDeploymentName: modelName,
|
||||||
azureOpenAIApiInstanceName: credentials.resourceName,
|
// instance name only needed to set base url
|
||||||
|
azureOpenAIApiInstanceName: !credentials.endpoint ? credentials.resourceName : undefined,
|
||||||
azureOpenAIApiKey: credentials.apiKey,
|
azureOpenAIApiKey: credentials.apiKey,
|
||||||
azureOpenAIApiVersion: credentials.apiVersion,
|
azureOpenAIApiVersion: credentials.apiVersion,
|
||||||
|
azureOpenAIEndpoint: credentials.endpoint,
|
||||||
...options,
|
...options,
|
||||||
timeout: options.timeout ?? 60000,
|
timeout: options.timeout ?? 60000,
|
||||||
maxRetries: options.maxRetries ?? 2,
|
maxRetries: options.maxRetries ?? 2,
|
||||||
|
|
|
@ -93,7 +93,7 @@ export class FileSystemManager implements BinaryData.Manager {
|
||||||
// binary files stored in nested dirs - `filesystem-v2`
|
// binary files stored in nested dirs - `filesystem-v2`
|
||||||
|
|
||||||
const binaryDataDirs = ids.map(({ workflowId, executionId }) =>
|
const binaryDataDirs = ids.map(({ workflowId, executionId }) =>
|
||||||
this.resolvePath(`workflows/${workflowId}/executions/${executionId}/binary_data/`),
|
this.resolvePath(`workflows/${workflowId}/executions/${executionId}`),
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
|
|
@ -147,6 +147,11 @@ describe('copyByFilePath()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteMany()', () => {
|
describe('deleteMany()', () => {
|
||||||
|
const rmOptions = {
|
||||||
|
force: true,
|
||||||
|
recursive: true,
|
||||||
|
};
|
||||||
|
|
||||||
it('should delete many files by workflow ID and execution ID', async () => {
|
it('should delete many files by workflow ID and execution ID', async () => {
|
||||||
const ids = [
|
const ids = [
|
||||||
{ workflowId, executionId },
|
{ workflowId, executionId },
|
||||||
|
@ -160,6 +165,16 @@ describe('deleteMany()', () => {
|
||||||
await expect(promise).resolves.not.toThrow();
|
await expect(promise).resolves.not.toThrow();
|
||||||
|
|
||||||
expect(fsp.rm).toHaveBeenCalledTimes(2);
|
expect(fsp.rm).toHaveBeenCalledTimes(2);
|
||||||
|
expect(fsp.rm).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
`${storagePath}/workflows/${workflowId}/executions/${executionId}`,
|
||||||
|
rmOptions,
|
||||||
|
);
|
||||||
|
expect(fsp.rm).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
`${storagePath}/workflows/${otherWorkflowId}/executions/${otherExecutionId}`,
|
||||||
|
rmOptions,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should suppress error on non-existing filepath', async () => {
|
it('should suppress error on non-existing filepath', async () => {
|
||||||
|
|
|
@ -344,6 +344,8 @@ async function onCopyButtonClick(content: string, e: MouseEvent) {
|
||||||
.container {
|
.container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
@ -373,10 +375,6 @@ p {
|
||||||
background-color: var(--color-background-light);
|
background-color: var(--color-background-light);
|
||||||
border: var(--border-base);
|
border: var(--border-base);
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
height: 100%;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-bottom: 250px; // make scrollable at the end
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
pre,
|
pre,
|
||||||
|
@ -390,7 +388,13 @@ p {
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages {
|
.messages {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
padding: var(--spacing-xs);
|
padding: var(--spacing-xs);
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
& + & {
|
& + & {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
|
|
@ -128,7 +128,8 @@ watch(defaultLocale, (newLocale) => {
|
||||||
.container {
|
.container {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// App grid is the main app layout including modals and other absolute positioned elements
|
// App grid is the main app layout including modals and other absolute positioned elements
|
||||||
|
@ -136,13 +137,12 @@ watch(defaultLocale, (newLocale) => {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
flex-basis: 100%;
|
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'banners banners'
|
'banners banners'
|
||||||
'sidebar header'
|
'sidebar header'
|
||||||
'sidebar content';
|
'sidebar content';
|
||||||
grid-auto-columns: minmax(0, max-content) 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
grid-template-rows: auto fit-content($header-height) 1fr;
|
grid-template-rows: auto auto 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banners {
|
.banners {
|
||||||
|
|
|
@ -103,12 +103,6 @@ function onClose() {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style module>
|
<style module>
|
||||||
.container {
|
|
||||||
height: 100%;
|
|
||||||
flex-basis: content;
|
|
||||||
z-index: var(--z-index-ask-assistant-chat);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -211,7 +211,6 @@ async function navigateToExecutionsView(openInNewTab: boolean) {
|
||||||
|
|
||||||
.main-header {
|
.main-header {
|
||||||
background-color: var(--color-background-xlight);
|
background-color: var(--color-background-xlight);
|
||||||
height: $header-height;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-bottom: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
border-bottom: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
||||||
|
@ -222,16 +221,15 @@ async function navigateToExecutionsView(openInNewTab: boolean) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
height: $header-height;
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
padding: 0 var(--spacing-m) 0 var(--spacing-m);
|
padding: var(--spacing-xs) var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.github-button {
|
.github-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: $header-height;
|
align-self: stretch;
|
||||||
padding-left: var(--spacing-m);
|
padding-left: var(--spacing-m);
|
||||||
padding-right: var(--spacing-m);
|
padding-right: var(--spacing-m);
|
||||||
background-color: var(--color-background-xlight);
|
background-color: var(--color-background-xlight);
|
||||||
|
|
|
@ -40,15 +40,16 @@ function onUpdateModelValue(tab: MAIN_HEADER_TABS, event: MouseEvent): void {
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.container {
|
.container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 47px;
|
bottom: 0;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%) translateY(50%);
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: var(--spacing-5xs);
|
padding: var(--spacing-5xs);
|
||||||
background-color: var(--color-foreground-base);
|
background-color: var(--color-foreground-base);
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
transition: all 150ms ease-in-out;
|
transition: all 150ms ease-in-out;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 430px) {
|
@media screen and (max-width: 430px) {
|
||||||
|
|
|
@ -777,6 +777,10 @@ $--header-spacing: 20px;
|
||||||
|
|
||||||
.name-container {
|
.name-container {
|
||||||
margin-right: $--header-spacing;
|
margin-right: $--header-spacing;
|
||||||
|
|
||||||
|
:deep(.el-input) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
|
@ -827,6 +831,7 @@ $--header-spacing: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
@ -837,6 +842,7 @@ $--header-spacing: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group {
|
.group {
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
import type { IRunData, IRunExecutionData, NodeError, Workflow } from 'n8n-workflow';
|
import {
|
||||||
|
NodeConnectionType,
|
||||||
|
type IRunData,
|
||||||
|
type IRunExecutionData,
|
||||||
|
type NodeError,
|
||||||
|
type Workflow,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import RunData from './RunData.vue';
|
import RunData from './RunData.vue';
|
||||||
import RunInfo from './RunInfo.vue';
|
import RunInfo from './RunInfo.vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
@ -209,6 +215,29 @@ const canPinData = computed(() => {
|
||||||
return pinnedData.isValidNodeType.value && !props.isReadOnly;
|
return pinnedData.isValidNodeType.value && !props.isReadOnly;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const allToolsWereUnusedNotice = computed(() => {
|
||||||
|
if (!node.value || runsCount.value === 0) return undefined;
|
||||||
|
|
||||||
|
// With pinned data there's no clear correct answer for whether
|
||||||
|
// we should use historic or current parents, so we don't show the notice,
|
||||||
|
// as it likely ends up unactionable noise to the user
|
||||||
|
if (pinnedData.hasData.value) return undefined;
|
||||||
|
|
||||||
|
const toolsAvailable = props.workflow.getParentNodes(
|
||||||
|
node.value.name,
|
||||||
|
NodeConnectionType.AiTool,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
const toolsUsedInLatestRun = toolsAvailable.filter(
|
||||||
|
(tool) => !!workflowRunData.value?.[tool]?.[props.runIndex],
|
||||||
|
);
|
||||||
|
if (toolsAvailable.length > 0 && toolsUsedInLatestRun.length === 0) {
|
||||||
|
return i18n.baseText('ndv.output.noToolUsedInfo');
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
|
|
||||||
const insertTestData = () => {
|
const insertTestData = () => {
|
||||||
|
@ -298,6 +327,7 @@ const activatePane = () => {
|
||||||
:hide-pagination="outputMode === 'logs'"
|
:hide-pagination="outputMode === 'logs'"
|
||||||
pane-type="output"
|
pane-type="output"
|
||||||
:data-output-type="outputMode"
|
:data-output-type="outputMode"
|
||||||
|
:callout-message="allToolsWereUnusedNotice"
|
||||||
@activate-pane="activatePane"
|
@activate-pane="activatePane"
|
||||||
@run-change="onRunIndexChange"
|
@run-change="onRunIndexChange"
|
||||||
@link-run="onLinkRun"
|
@link-run="onLinkRun"
|
||||||
|
|
|
@ -120,6 +120,7 @@ type Props = {
|
||||||
isProductionExecutionPreview?: boolean;
|
isProductionExecutionPreview?: boolean;
|
||||||
isPaneActive?: boolean;
|
isPaneActive?: boolean;
|
||||||
hidePagination?: boolean;
|
hidePagination?: boolean;
|
||||||
|
calloutMessage?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
@ -133,6 +134,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
mappingEnabled: false,
|
mappingEnabled: false,
|
||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
hidePagination: false,
|
hidePagination: false,
|
||||||
|
calloutMessage: undefined,
|
||||||
});
|
});
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
search: [search: string];
|
search: [search: string];
|
||||||
|
@ -1426,6 +1428,12 @@ defineExpose({ enterEditMode });
|
||||||
|
|
||||||
<slot v-if="!displaysMultipleNodes" name="before-data" />
|
<slot v-if="!displaysMultipleNodes" name="before-data" />
|
||||||
|
|
||||||
|
<div v-if="props.calloutMessage" :class="$style.hintCallout">
|
||||||
|
<N8nCallout theme="secondary" data-test-id="run-data-callout">
|
||||||
|
<N8nText v-n8n-html="props.calloutMessage" size="small"></N8nText>
|
||||||
|
</N8nCallout>
|
||||||
|
</div>
|
||||||
|
|
||||||
<N8nCallout
|
<N8nCallout
|
||||||
v-for="hint in getNodeHints()"
|
v-for="hint in getNodeHints()"
|
||||||
:key="hint.message"
|
:key="hint.message"
|
||||||
|
|
|
@ -247,6 +247,7 @@ exports[`InputPanel > should render 1`] = `
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type {
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
IWebhookDescription,
|
IWebhookDescription,
|
||||||
Workflow,
|
Workflow,
|
||||||
|
INodeConnections,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
||||||
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||||||
|
@ -2049,7 +2050,6 @@ describe('useCanvasOperations', () => {
|
||||||
expect(workflowsStore.setNodes).toHaveBeenCalled();
|
expect(workflowsStore.setNodes).toHaveBeenCalled();
|
||||||
expect(workflowsStore.setConnections).toHaveBeenCalled();
|
expect(workflowsStore.setConnections).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('should initialize node data from node type description', () => {
|
it('should initialize node data from node type description', () => {
|
||||||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||||
|
@ -2080,6 +2080,100 @@ describe('useCanvasOperations', () => {
|
||||||
|
|
||||||
expect(workflow.nodes[0].parameters).toEqual({ value: true });
|
expect(workflow.nodes[0].parameters).toEqual({ value: true });
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filterConnectionsByNodes', () => {
|
||||||
|
it('should return filtered connections when all nodes are included', () => {
|
||||||
|
const connections: INodeConnections = {
|
||||||
|
[NodeConnectionType.Main]: [
|
||||||
|
[
|
||||||
|
{ node: 'node1', type: NodeConnectionType.Main, index: 0 },
|
||||||
|
{ node: 'node2', type: NodeConnectionType.Main, index: 0 },
|
||||||
|
],
|
||||||
|
[{ node: 'node3', type: NodeConnectionType.Main, index: 0 }],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const includeNodeNames = new Set<string>(['node1', 'node2', 'node3']);
|
||||||
|
|
||||||
|
const { filterConnectionsByNodes } = useCanvasOperations({ router });
|
||||||
|
const result = filterConnectionsByNodes(connections, includeNodeNames);
|
||||||
|
|
||||||
|
expect(result).toEqual(connections);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty connections when no nodes are included', () => {
|
||||||
|
const connections: INodeConnections = {
|
||||||
|
[NodeConnectionType.Main]: [
|
||||||
|
[
|
||||||
|
{ node: 'node1', type: NodeConnectionType.Main, index: 0 },
|
||||||
|
{ node: 'node2', type: NodeConnectionType.Main, index: 0 },
|
||||||
|
],
|
||||||
|
[{ node: 'node3', type: NodeConnectionType.Main, index: 0 }],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const includeNodeNames = new Set<string>();
|
||||||
|
|
||||||
|
const { filterConnectionsByNodes } = useCanvasOperations({ router });
|
||||||
|
const result = filterConnectionsByNodes(connections, includeNodeNames);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
[NodeConnectionType.Main]: [[], []],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return partially filtered connections when some nodes are included', () => {
|
||||||
|
const connections: INodeConnections = {
|
||||||
|
[NodeConnectionType.Main]: [
|
||||||
|
[
|
||||||
|
{ node: 'node1', type: NodeConnectionType.Main, index: 0 },
|
||||||
|
{ node: 'node2', type: NodeConnectionType.Main, index: 0 },
|
||||||
|
],
|
||||||
|
[{ node: 'node3', type: NodeConnectionType.Main, index: 0 }],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const includeNodeNames = new Set<string>(['node1']);
|
||||||
|
|
||||||
|
const { filterConnectionsByNodes } = useCanvasOperations({ router });
|
||||||
|
const result = filterConnectionsByNodes(connections, includeNodeNames);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
[NodeConnectionType.Main]: [
|
||||||
|
[{ node: 'node1', type: NodeConnectionType.Main, index: 0 }],
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty connections input', () => {
|
||||||
|
const connections: INodeConnections = {};
|
||||||
|
const includeNodeNames = new Set<string>(['node1']);
|
||||||
|
|
||||||
|
const { filterConnectionsByNodes } = useCanvasOperations({ router });
|
||||||
|
const result = filterConnectionsByNodes(connections, includeNodeNames);
|
||||||
|
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle connections with no valid nodes', () => {
|
||||||
|
const connections: INodeConnections = {
|
||||||
|
[NodeConnectionType.Main]: [
|
||||||
|
[
|
||||||
|
{ node: 'node4', type: NodeConnectionType.Main, index: 0 },
|
||||||
|
{ node: 'node5', type: NodeConnectionType.Main, index: 0 },
|
||||||
|
],
|
||||||
|
[{ node: 'node6', type: NodeConnectionType.Main, index: 0 }],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const includeNodeNames = new Set<string>(['node1', 'node2', 'node3']);
|
||||||
|
|
||||||
|
const { filterConnectionsByNodes } = useCanvasOperations({ router });
|
||||||
|
const result = filterConnectionsByNodes(connections, includeNodeNames);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
[NodeConnectionType.Main]: [[], []],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function buildImportNodes() {
|
function buildImportNodes() {
|
||||||
|
|
|
@ -1809,17 +1809,15 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterConnectionsByNodes(
|
function filterConnectionsByNodes(
|
||||||
connections: Record<string, IConnection[][]>,
|
connections: INodeConnections,
|
||||||
includeNodeNames: Set<string>,
|
includeNodeNames: Set<string>,
|
||||||
): INodeConnections {
|
): INodeConnections {
|
||||||
const filteredConnections: INodeConnections = {};
|
const filteredConnections: INodeConnections = {};
|
||||||
|
|
||||||
for (const [type, typeConnections] of Object.entries(connections)) {
|
for (const [type, typeConnections] of Object.entries(connections)) {
|
||||||
const validConnections = typeConnections
|
const validConnections = typeConnections.map((sourceConnections) =>
|
||||||
.map((sourceConnections) =>
|
|
||||||
sourceConnections.filter((connection) => includeNodeNames.has(connection.node)),
|
sourceConnections.filter((connection) => includeNodeNames.has(connection.node)),
|
||||||
)
|
);
|
||||||
.filter((sourceConnections) => sourceConnections.length > 0);
|
|
||||||
|
|
||||||
if (validConnections.length) {
|
if (validConnections.length) {
|
||||||
filteredConnections[type] = validConnections;
|
filteredConnections[type] = validConnections;
|
||||||
|
@ -1888,6 +1886,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
revertDeleteConnection,
|
revertDeleteConnection,
|
||||||
deleteConnectionsByNodeId,
|
deleteConnectionsByNodeId,
|
||||||
isConnectionAllowed,
|
isConnectionAllowed,
|
||||||
|
filterConnectionsByNodes,
|
||||||
importWorkflowData,
|
importWorkflowData,
|
||||||
fetchWorkflowDataFromUrl,
|
fetchWorkflowDataFromUrl,
|
||||||
resetWorkspace,
|
resetWorkspace,
|
||||||
|
|
|
@ -988,6 +988,7 @@
|
||||||
"ndv.output.tooMuchData.showDataAnyway": "Show data",
|
"ndv.output.tooMuchData.showDataAnyway": "Show data",
|
||||||
"ndv.output.tooMuchData.title": "Display data?",
|
"ndv.output.tooMuchData.title": "Display data?",
|
||||||
"ndv.output.waitingToRun": "Waiting to execute...",
|
"ndv.output.waitingToRun": "Waiting to execute...",
|
||||||
|
"ndv.output.noToolUsedInfo": "None of your tools were used in this run. Try giving your tools clearer names and descriptions to help the AI",
|
||||||
"ndv.title.cancel": "Cancel",
|
"ndv.title.cancel": "Cancel",
|
||||||
"ndv.title.rename": "Rename",
|
"ndv.title.rename": "Rename",
|
||||||
"ndv.title.renameNode": "Rename node",
|
"ndv.title.renameNode": "Rename node",
|
||||||
|
|
Loading…
Reference in a new issue