mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Add evaluation workflow and enhance workflow selector with pinned data support (#12773)
This commit is contained in:
parent
05b5f95331
commit
be967ebec0
|
@ -556,6 +556,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
||||||
|
|
||||||
selectResourceLocatorItem('workflowId', 0, 'Create a');
|
selectResourceLocatorItem('workflowId', 0, 'Create a');
|
||||||
|
|
||||||
|
cy.get('body').type('{esc}');
|
||||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||||
clickCreateNewCredential();
|
clickCreateNewCredential();
|
||||||
setCredentialValues({
|
setCredentialValues({
|
||||||
|
|
|
@ -86,6 +86,8 @@ describe('Workflow Selector Parameter', () => {
|
||||||
cy.stub(win, 'open').as('windowOpen');
|
cy.stub(win, 'open').as('windowOpen');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cy.intercept('POST', '/rest/workflows*').as('createSubworkflow');
|
||||||
|
|
||||||
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||||
ndv.getters.resourceLocatorInput('workflowId').click();
|
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||||
|
|
||||||
|
@ -98,10 +100,20 @@ describe('Workflow Selector Parameter', () => {
|
||||||
|
|
||||||
getVisiblePopper().findChildByTestId('rlc-item').eq(0).click();
|
getVisiblePopper().findChildByTestId('rlc-item').eq(0).click();
|
||||||
|
|
||||||
const SAMPLE_SUBWORKFLOW_TEMPLATE_ID = 'VMiAxXa3lCAizGB5f7dVZQSFfg3FtHkdTKvLuupqBls=';
|
cy.wait('@createSubworkflow').then((interception) => {
|
||||||
cy.get('@windowOpen').should(
|
expect(interception.request.body).to.have.property('name').that.includes('Sub-Workflow');
|
||||||
'be.calledWith',
|
expect(interception.request.body.nodes).to.be.an('array');
|
||||||
`/workflows/onboarding/${SAMPLE_SUBWORKFLOW_TEMPLATE_ID}?sampleSubWorkflows=0`,
|
expect(interception.request.body.nodes).to.have.length(2);
|
||||||
);
|
expect(interception.request.body.nodes[0]).to.have.property(
|
||||||
|
'name',
|
||||||
|
'When Executed by Another Workflow',
|
||||||
|
);
|
||||||
|
expect(interception.request.body.nodes[1]).to.have.property(
|
||||||
|
'name',
|
||||||
|
'Replace me with your logic',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('@windowOpen').should('be.calledWithMatch', /\/workflow\/.+/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -65,7 +65,8 @@ describe('Sub-workflow creation and typed usage', () => {
|
||||||
// **************************
|
// **************************
|
||||||
// NAVIGATE TO CHILD WORKFLOW
|
// NAVIGATE TO CHILD WORKFLOW
|
||||||
// **************************
|
// **************************
|
||||||
|
// Close NDV before opening the node creator
|
||||||
|
cy.get('body').type('{esc}');
|
||||||
openNode('When Executed by Another Workflow');
|
openNode('When Executed by Another Workflow');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -138,41 +139,41 @@ describe('Sub-workflow creation and typed usage', () => {
|
||||||
cy.window().then((win) => {
|
cy.window().then((win) => {
|
||||||
cy.stub(win, 'open').callsFake((url) => {
|
cy.stub(win, 'open').callsFake((url) => {
|
||||||
cy.visit(url);
|
cy.visit(url);
|
||||||
|
selectResourceLocatorItem('workflowId', 0, 'Create a');
|
||||||
|
|
||||||
|
openNode('When Executed by Another Workflow');
|
||||||
|
|
||||||
|
getParameterInputByName('inputSource').click();
|
||||||
|
|
||||||
|
getVisiblePopper()
|
||||||
|
.getByTestId('parameter-input')
|
||||||
|
.eq(0)
|
||||||
|
.type('Using JSON Example{downArrow}{enter}');
|
||||||
|
|
||||||
|
const exampleJson =
|
||||||
|
'{{}' + EXAMPLE_FIELDS.map((x) => `"${x[0]}": ${makeExample(x[1])}`).join(',') + '}';
|
||||||
|
getParameterInputByName('jsonExample')
|
||||||
|
.find('.cm-line')
|
||||||
|
.eq(0)
|
||||||
|
.type(`{selectAll}{backspace}${exampleJson}{enter}`);
|
||||||
|
|
||||||
|
// first one doesn't work for some reason, might need to wait for something?
|
||||||
|
clickExecuteNode();
|
||||||
|
|
||||||
|
validateAndReturnToParent(
|
||||||
|
DEFAULT_SUBWORKFLOW_NAME_2,
|
||||||
|
2,
|
||||||
|
EXAMPLE_FIELDS.map((f) => f[0]),
|
||||||
|
);
|
||||||
|
|
||||||
|
assertOutputTableContent([
|
||||||
|
['[null]', '[null]', '[null]', '[null]', '[null]', 'false'],
|
||||||
|
['[null]', '[null]', '[null]', '[null]', '[null]', 'false'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
clickExecuteNode();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
selectResourceLocatorItem('workflowId', 0, 'Create a');
|
|
||||||
|
|
||||||
openNode('When Executed by Another Workflow');
|
|
||||||
|
|
||||||
getParameterInputByName('inputSource').click();
|
|
||||||
|
|
||||||
getVisiblePopper()
|
|
||||||
.getByTestId('parameter-input')
|
|
||||||
.eq(0)
|
|
||||||
.type('Using JSON Example{downArrow}{enter}');
|
|
||||||
|
|
||||||
const exampleJson =
|
|
||||||
'{{}' + EXAMPLE_FIELDS.map((x) => `"${x[0]}": ${makeExample(x[1])}`).join(',') + '}';
|
|
||||||
getParameterInputByName('jsonExample')
|
|
||||||
.find('.cm-line')
|
|
||||||
.eq(0)
|
|
||||||
.type(`{selectAll}{backspace}${exampleJson}{enter}`);
|
|
||||||
|
|
||||||
// first one doesn't work for some reason, might need to wait for something?
|
|
||||||
clickExecuteNode();
|
|
||||||
|
|
||||||
validateAndReturnToParent(
|
|
||||||
DEFAULT_SUBWORKFLOW_NAME_2,
|
|
||||||
2,
|
|
||||||
EXAMPLE_FIELDS.map((f) => f[0]),
|
|
||||||
);
|
|
||||||
|
|
||||||
assertOutputTableContent([
|
|
||||||
['[null]', '[null]', '[null]', '[null]', '[null]', 'false'],
|
|
||||||
['[null]', '[null]', '[null]', '[null]', '[null]', 'false'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
clickExecuteNode();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show node issue when no fields are defined in manual mode', () => {
|
it('should show node issue when no fields are defined in manual mode', () => {
|
||||||
|
|
|
@ -1,20 +1,45 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import type { INodeParameterResourceLocator } from 'n8n-workflow';
|
import { SAMPLE_EVALUATION_WORKFLOW } from '@/constants.workflows';
|
||||||
|
import type { IWorkflowDataCreate } from '@/Interface';
|
||||||
|
import type { INodeParameterResourceLocator, IPinData } from 'n8n-workflow';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
interface WorkflowSelectorProps {
|
interface WorkflowSelectorProps {
|
||||||
modelValue: INodeParameterResourceLocator;
|
modelValue: INodeParameterResourceLocator;
|
||||||
|
examplePinnedData?: IPinData;
|
||||||
|
sampleWorkflowName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<WorkflowSelectorProps>(), {
|
const props = withDefaults(defineProps<WorkflowSelectorProps>(), {
|
||||||
modelValue: () => ({
|
modelValue: () => ({
|
||||||
mode: 'id',
|
mode: 'id',
|
||||||
value: '',
|
value: '',
|
||||||
__rl: true,
|
__rl: true,
|
||||||
}),
|
}),
|
||||||
|
examplePinnedData: () => ({}),
|
||||||
|
sampleWorkflowName: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits<{ 'update:modelValue': [value: WorkflowSelectorProps['modelValue']] }>();
|
defineEmits<{ 'update:modelValue': [value: WorkflowSelectorProps['modelValue']] }>();
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
|
|
||||||
|
const subworkflowName = computed(() => {
|
||||||
|
if (props.sampleWorkflowName) {
|
||||||
|
return locale.baseText('testDefinition.workflowInput.subworkflowName', {
|
||||||
|
interpolate: { name: props.sampleWorkflowName },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return locale.baseText('testDefinition.workflowInput.subworkflowName.default');
|
||||||
|
});
|
||||||
|
|
||||||
|
const sampleWorkflow = computed<IWorkflowDataCreate>(() => {
|
||||||
|
return {
|
||||||
|
...SAMPLE_EVALUATION_WORKFLOW,
|
||||||
|
name: subworkflowName.value,
|
||||||
|
pinData: props.examplePinnedData,
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
@ -36,6 +61,7 @@ const locale = useI18n();
|
||||||
:expression-edit-dialog-visible="false"
|
:expression-edit-dialog-visible="false"
|
||||||
:path="'workflows'"
|
:path="'workflows'"
|
||||||
allow-new
|
allow-new
|
||||||
|
:sample-workflow="sampleWorkflow"
|
||||||
@update:model-value="$emit('update:modelValue', $event)"
|
@update:model-value="$emit('update:modelValue', $event)"
|
||||||
/>
|
/>
|
||||||
</n8n-input-label>
|
</n8n-input-label>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type { EditableFormState, EvaluationFormState } from '@/components/TestDe
|
||||||
import type { ITag, ModalState } from '@/Interface';
|
import type { ITag, ModalState } from '@/Interface';
|
||||||
import { NODE_PINNING_MODAL_KEY } from '@/constants';
|
import { NODE_PINNING_MODAL_KEY } from '@/constants';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import type { IPinData } from 'n8n-workflow';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
showConfig: boolean;
|
showConfig: boolean;
|
||||||
|
@ -16,6 +17,8 @@ defineProps<{
|
||||||
allTags: ITag[];
|
allTags: ITag[];
|
||||||
tagsById: Record<string, ITag>;
|
tagsById: Record<string, ITag>;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
examplePinnedData?: IPinData;
|
||||||
|
sampleWorkflowName?: string;
|
||||||
getFieldIssues: (key: string) => Array<{ field: string; message: string }>;
|
getFieldIssues: (key: string) => Array<{ field: string; message: string }>;
|
||||||
startEditing: (field: keyof EditableFormState) => void;
|
startEditing: (field: keyof EditableFormState) => void;
|
||||||
saveChanges: (field: keyof EditableFormState) => void;
|
saveChanges: (field: keyof EditableFormState) => void;
|
||||||
|
@ -23,6 +26,12 @@ defineProps<{
|
||||||
createTag?: (name: string) => Promise<ITag>;
|
createTag?: (name: string) => Promise<ITag>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
openPinningModal: [];
|
||||||
|
deleteMetric: [metric: Partial<TestMetricRecord>];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const locale = useI18n();
|
||||||
const changedFieldsKeys = ref<string[]>([]);
|
const changedFieldsKeys = ref<string[]>([]);
|
||||||
const tags = defineModel<EvaluationFormState['tags']>('tags', { required: true });
|
const tags = defineModel<EvaluationFormState['tags']>('tags', { required: true });
|
||||||
const evaluationWorkflow = defineModel<EvaluationFormState['evaluationWorkflow']>(
|
const evaluationWorkflow = defineModel<EvaluationFormState['evaluationWorkflow']>(
|
||||||
|
@ -35,12 +44,6 @@ const mockedNodes = defineModel<EvaluationFormState['mockedNodes']>('mockedNodes
|
||||||
});
|
});
|
||||||
|
|
||||||
const nodePinningModal = ref<ModalState | null>(null);
|
const nodePinningModal = ref<ModalState | null>(null);
|
||||||
const emit = defineEmits<{
|
|
||||||
openPinningModal: [];
|
|
||||||
deleteMetric: [metric: Partial<TestMetricRecord>];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const locale = useI18n();
|
|
||||||
|
|
||||||
function updateChangedFieldsKeys(key: string) {
|
function updateChangedFieldsKeys(key: string) {
|
||||||
changedFieldsKeys.value.push(key);
|
changedFieldsKeys.value.push(key);
|
||||||
|
@ -138,7 +141,9 @@ function showFieldIssues(fieldKey: string) {
|
||||||
<template #cardContent>
|
<template #cardContent>
|
||||||
<WorkflowSelector
|
<WorkflowSelector
|
||||||
v-model="evaluationWorkflow"
|
v-model="evaluationWorkflow"
|
||||||
|
:example-pinned-data="examplePinnedData"
|
||||||
:class="{ 'has-issues': getFieldIssues('evaluationWorkflow').length > 0 }"
|
:class="{ 'has-issues': getFieldIssues('evaluationWorkflow').length > 0 }"
|
||||||
|
:sample-workflow-name="sampleWorkflowName"
|
||||||
@update:model-value="updateChangedFieldsKeys('evaluationWorkflow')"
|
@update:model-value="updateChangedFieldsKeys('evaluationWorkflow')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -20,8 +20,9 @@ import { useWorkflowResourceLocatorModes } from './useWorkflowResourceLocatorMod
|
||||||
import { useWorkflowResourcesLocator } from './useWorkflowResourcesLocator';
|
import { useWorkflowResourcesLocator } from './useWorkflowResourcesLocator';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import { SAMPLE_SUBWORKFLOW_WORKFLOW } from '@/constants.workflows';
|
import { SAMPLE_SUBWORKFLOW_WORKFLOW } from '@/constants.workflows';
|
||||||
|
import type { IWorkflowDataCreate } from '@/Interface';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: INodeParameterResourceLocator;
|
modelValue: INodeParameterResourceLocator;
|
||||||
|
@ -34,6 +35,7 @@ interface Props {
|
||||||
forceShowExpression?: boolean;
|
forceShowExpression?: boolean;
|
||||||
parameterIssues?: string[];
|
parameterIssues?: string[];
|
||||||
parameter: INodeProperties;
|
parameter: INodeProperties;
|
||||||
|
sampleWorkflow?: IWorkflowDataCreate;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
@ -44,6 +46,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
forceShowExpression: false,
|
forceShowExpression: false,
|
||||||
expressionDisplayValue: '',
|
expressionDisplayValue: '',
|
||||||
parameterIssues: () => [],
|
parameterIssues: () => [],
|
||||||
|
sampleWorkflow: () => SAMPLE_SUBWORKFLOW_WORKFLOW,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -205,36 +208,30 @@ onClickOutside(dropdown, () => {
|
||||||
isDropdownVisible.value = false;
|
isDropdownVisible.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const onAddResourceClicked = () => {
|
const onAddResourceClicked = async () => {
|
||||||
const subWorkflowNameRegex = /My\s+Sub-Workflow\s+\d+/;
|
const projectId = projectStore.currentProjectId;
|
||||||
|
const sampleWorkflow = props.sampleWorkflow;
|
||||||
const urlSearchParams = new URLSearchParams();
|
const workflowName = sampleWorkflow.name ?? 'My Sub-Workflow';
|
||||||
|
|
||||||
if (projectStore.currentProjectId) {
|
|
||||||
urlSearchParams.set('projectId', projectStore.currentProjectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sampleSubWorkflows = workflowsStore.allWorkflows.filter(
|
const sampleSubWorkflows = workflowsStore.allWorkflows.filter(
|
||||||
(w) => w.name && subWorkflowNameRegex.test(w.name),
|
(w) => w.name && new RegExp(workflowName).test(w.name),
|
||||||
);
|
);
|
||||||
|
|
||||||
urlSearchParams.set('sampleSubWorkflows', sampleSubWorkflows.length.toString());
|
const workflow: IWorkflowDataCreate = {
|
||||||
|
...sampleWorkflow,
|
||||||
|
name: `${workflowName} ${sampleSubWorkflows.length + 1}`,
|
||||||
|
};
|
||||||
|
if (projectId) {
|
||||||
|
workflow.projectId = projectId;
|
||||||
|
}
|
||||||
telemetry.track('User clicked create new sub-workflow button', {}, { withPostHog: true });
|
telemetry.track('User clicked create new sub-workflow button', {}, { withPostHog: true });
|
||||||
|
|
||||||
const sampleSubworkflowChannel = new BroadcastChannel(NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL);
|
const newWorkflow = await workflowsStore.createNewWorkflow(workflow);
|
||||||
|
const { href } = router.resolve({ name: VIEWS.WORKFLOW, params: { name: newWorkflow.id } });
|
||||||
|
await reloadWorkflows();
|
||||||
|
onInputChange(newWorkflow.id);
|
||||||
|
hideDropdown();
|
||||||
|
|
||||||
sampleSubworkflowChannel.onmessage = async (event: MessageEvent<{ workflowId: string }>) => {
|
window.open(href, '_blank');
|
||||||
const workflowId = event.data.workflowId;
|
|
||||||
await reloadWorkflows();
|
|
||||||
onInputChange(workflowId);
|
|
||||||
hideDropdown();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.open(
|
|
||||||
`/workflows/onboarding/${SAMPLE_SUBWORKFLOW_WORKFLOW.meta.templateId}?${urlSearchParams.toString()}`,
|
|
||||||
'_blank',
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -904,8 +904,6 @@ export const BROWSER_ID_STORAGE_KEY = 'n8n-browserId';
|
||||||
|
|
||||||
export const APP_MODALS_ELEMENT_ID = 'app-modals';
|
export const APP_MODALS_ELEMENT_ID = 'app-modals';
|
||||||
|
|
||||||
export const NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL = 'new-sample-sub-workflow-created';
|
|
||||||
|
|
||||||
export const AI_NODES_PACKAGE_NAME = '@n8n/n8n-nodes-langchain';
|
export const AI_NODES_PACKAGE_NAME = '@n8n/n8n-nodes-langchain';
|
||||||
|
|
||||||
export const AI_ASSISTANT_MAX_CONTENT_LENGTH = 100; // in kilobytes
|
export const AI_ASSISTANT_MAX_CONTENT_LENGTH = 100; // in kilobytes
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import type { INodeUi, WorkflowDataWithTemplateId } from './Interface';
|
import type { INodeUi, IWorkflowDataCreate } from './Interface';
|
||||||
|
|
||||||
export const SAMPLE_SUBWORKFLOW_WORKFLOW: WorkflowDataWithTemplateId = {
|
export const SAMPLE_SUBWORKFLOW_WORKFLOW: IWorkflowDataCreate = {
|
||||||
name: 'My Sub-Workflow',
|
name: 'My Sub-Workflow',
|
||||||
meta: {
|
|
||||||
templateId: 'VMiAxXa3lCAizGB5f7dVZQSFfg3FtHkdTKvLuupqBls=',
|
|
||||||
},
|
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
id: 'c055762a-8fe7-4141-a639-df2372f30060',
|
id: 'c055762a-8fe7-4141-a639-df2372f30060',
|
||||||
|
@ -41,3 +38,137 @@ export const SAMPLE_SUBWORKFLOW_WORKFLOW: WorkflowDataWithTemplateId = {
|
||||||
},
|
},
|
||||||
pinData: {},
|
pinData: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SAMPLE_EVALUATION_WORKFLOW: IWorkflowDataCreate = {
|
||||||
|
name: 'My Evaluation Sub-Workflow',
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
inputSource: 'passthrough',
|
||||||
|
},
|
||||||
|
id: 'ad3156ed-3007-4a09-8527-920505339812',
|
||||||
|
name: 'When called by a test run',
|
||||||
|
type: 'n8n-nodes-base.executeWorkflowTrigger',
|
||||||
|
typeVersion: 1.1,
|
||||||
|
position: [620, 380],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {},
|
||||||
|
id: '5ff0deaf-6ec9-4a0f-a906-70f1d8375e7c',
|
||||||
|
name: 'Replace me',
|
||||||
|
type: 'n8n-nodes-base.noOp',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [860, 380],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
assignments: {
|
||||||
|
assignments: [
|
||||||
|
{
|
||||||
|
id: 'a748051d-ebdb-4fcf-aaed-02756130ce2a',
|
||||||
|
name: 'my_metric',
|
||||||
|
value: 1,
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
id: '2cae7e85-7808-4cab-85c0-d233f47701a1',
|
||||||
|
name: 'Return metric(s)',
|
||||||
|
type: 'n8n-nodes-base.set',
|
||||||
|
typeVersion: 3.4,
|
||||||
|
position: [1100, 380],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
content:
|
||||||
|
"### 1. Receive execution data\n\nThis workflow will be passed:\n- A past execution from the test\n- The execution produced by re-running it\n\n\nWe've pinned some example data to get you started",
|
||||||
|
height: 438,
|
||||||
|
width: 217,
|
||||||
|
color: 7,
|
||||||
|
},
|
||||||
|
id: 'ecb90156-30a3-4a90-93d5-6aca702e2f6b',
|
||||||
|
name: 'Sticky Note',
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [560, 105],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
content: '### 2. Compare actual and expected result\n',
|
||||||
|
height: 439,
|
||||||
|
width: 217,
|
||||||
|
color: 7,
|
||||||
|
},
|
||||||
|
id: '556464f8-b86d-41e2-9249-ca6d541c9147',
|
||||||
|
name: 'Sticky Note1',
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [800, 104],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
content: '### 3. Return metrics\n\nMetrics should always be numerical',
|
||||||
|
height: 439,
|
||||||
|
width: 217,
|
||||||
|
color: 7,
|
||||||
|
},
|
||||||
|
id: '04c96a00-b360-423a-90a6-b3943c7d832f',
|
||||||
|
name: 'Sticky Note2',
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [1040, 104],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
content:
|
||||||
|
'## Evaluation workflow\nThis workflow is used to check whether a single past execution being tested gives similar results when re-run',
|
||||||
|
height: 105,
|
||||||
|
width: 694,
|
||||||
|
},
|
||||||
|
id: '2250a6ec-7c4f-45e4-8dfe-c4b50c98b34b',
|
||||||
|
name: 'Sticky Note3',
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [560, -25],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pinData: {
|
||||||
|
'When called by a test run': [
|
||||||
|
{
|
||||||
|
json: {
|
||||||
|
newExecution: {},
|
||||||
|
originalExecution: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
connections: {
|
||||||
|
'When called by a test run': {
|
||||||
|
[NodeConnectionType.Main]: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'Replace me',
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Replace me': {
|
||||||
|
[NodeConnectionType.Main]: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'Return metric(s)',
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
executionOrder: 'v1',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -2359,6 +2359,7 @@
|
||||||
"workflows.create.personal.toast.text": "This workflow has been created inside your personal space.",
|
"workflows.create.personal.toast.text": "This workflow has been created inside your personal space.",
|
||||||
"workflows.create.project.toast.title": "Workflow successfully created in {projectName}",
|
"workflows.create.project.toast.title": "Workflow successfully created in {projectName}",
|
||||||
"workflows.create.project.toast.text": "All members from {projectName} will have access to this workflow.",
|
"workflows.create.project.toast.text": "All members from {projectName} will have access to this workflow.",
|
||||||
|
"workflowSelectorParameterInput.createNewSubworkflow.name": "My Sub-Workflow",
|
||||||
"importCurlModal.title": "Import cURL command",
|
"importCurlModal.title": "Import cURL command",
|
||||||
"importCurlModal.input.label": "cURL Command",
|
"importCurlModal.input.label": "cURL Command",
|
||||||
"importCurlModal.input.placeholder": "Paste the cURL command here",
|
"importCurlModal.input.placeholder": "Paste the cURL command here",
|
||||||
|
@ -2869,6 +2870,8 @@
|
||||||
"testDefinition.configError.noExecutionsAddedToTag": "No executions added to this tag",
|
"testDefinition.configError.noExecutionsAddedToTag": "No executions added to this tag",
|
||||||
"testDefinition.configError.noEvaluationWorkflow": "No evaluation workflow set",
|
"testDefinition.configError.noEvaluationWorkflow": "No evaluation workflow set",
|
||||||
"testDefinition.configError.noMetrics": "No metrics set",
|
"testDefinition.configError.noMetrics": "No metrics set",
|
||||||
|
"testDefinition.workflowInput.subworkflowName": "Evaluation workflow for {name}",
|
||||||
|
"testDefinition.workflowInput.subworkflowName.default": "My Evaluation Sub-Workflow",
|
||||||
"freeAi.credits.callout.claim.title": "Get {credits} free OpenAI API credits",
|
"freeAi.credits.callout.claim.title": "Get {credits} free OpenAI API credits",
|
||||||
"freeAi.credits.callout.claim.button.label": "Claim credits",
|
"freeAi.credits.callout.claim.button.label": "Claim credits",
|
||||||
"freeAi.credits.callout.success.title.part1": "Claimed {credits} free OpenAI API credits! Please note these free credits are only for the following models:",
|
"freeAi.credits.callout.success.title.part1": "Claimed {credits} free OpenAI API credits! Please note these free credits are only for the following models:",
|
||||||
|
|
|
@ -14,6 +14,10 @@ import type { TestMetricRecord, TestRunRecord } from '@/api/testDefinition.ee';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useTestDefinitionStore } from '@/stores/testDefinition.store.ee';
|
import { useTestDefinitionStore } from '@/stores/testDefinition.store.ee';
|
||||||
import ConfigSection from '@/components/TestDefinition/EditDefinition/sections/ConfigSection.vue';
|
import ConfigSection from '@/components/TestDefinition/EditDefinition/sections/ConfigSection.vue';
|
||||||
|
import { useExecutionsStore } from '@/stores/executions.store';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import type { IPinData } from 'n8n-workflow';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
testId?: string;
|
testId?: string;
|
||||||
}>();
|
}>();
|
||||||
|
@ -26,6 +30,8 @@ const toast = useToast();
|
||||||
const testDefinitionStore = useTestDefinitionStore();
|
const testDefinitionStore = useTestDefinitionStore();
|
||||||
const tagsStore = useAnnotationTagsStore();
|
const tagsStore = useAnnotationTagsStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
|
const executionsStore = useExecutionsStore();
|
||||||
|
const workflowStore = useWorkflowsStore();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state,
|
state,
|
||||||
|
@ -50,11 +56,13 @@ const appliedTheme = computed(() => uiStore.appliedTheme);
|
||||||
const tagUsageCount = computed(
|
const tagUsageCount = computed(
|
||||||
() => tagsStore.tagsById[state.value.tags.value[0]]?.usageCount ?? 0,
|
() => tagsStore.tagsById[state.value.tags.value[0]]?.usageCount ?? 0,
|
||||||
);
|
);
|
||||||
|
const workflowName = computed(() => workflowStore.workflow.name);
|
||||||
const hasRuns = computed(() => runs.value.length > 0);
|
const hasRuns = computed(() => runs.value.length > 0);
|
||||||
|
const fieldsIssues = computed(() => testDefinitionStore.getFieldIssues(testId.value) ?? []);
|
||||||
|
|
||||||
const showConfig = ref(true);
|
const showConfig = ref(true);
|
||||||
const selectedMetric = ref<string>('');
|
const selectedMetric = ref<string>('');
|
||||||
|
const examplePinnedData = ref<IPinData>({});
|
||||||
const fieldsIssues = computed(() => testDefinitionStore.getFieldIssues(testId.value) ?? []);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!testDefinitionStore.isFeatureEnabled) {
|
if (!testDefinitionStore.isFeatureEnabled) {
|
||||||
|
@ -145,13 +153,36 @@ function toggleConfig() {
|
||||||
showConfig.value = !showConfig.value;
|
showConfig.value = !showConfig.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getExamplePinnedDataForTags() {
|
||||||
|
const evaluationWorkflowExecutions = await executionsStore.fetchExecutions({
|
||||||
|
workflowId: currentWorkflowId.value,
|
||||||
|
annotationTags: state.value.tags.value,
|
||||||
|
});
|
||||||
|
if (evaluationWorkflowExecutions.count > 0) {
|
||||||
|
const firstExecution = evaluationWorkflowExecutions.results[0];
|
||||||
|
const executionData = await executionsStore.fetchExecution(firstExecution.id);
|
||||||
|
const resultData = executionData?.data?.resultData.runData;
|
||||||
|
|
||||||
|
examplePinnedData.value = {
|
||||||
|
'When called by a test run': [
|
||||||
|
{
|
||||||
|
json: {
|
||||||
|
originalExecution: resultData,
|
||||||
|
newExecution: resultData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Debounced watchers for auto-saving
|
// Debounced watchers for auto-saving
|
||||||
watch(
|
watch(
|
||||||
() => state.value.metrics,
|
() => state.value.metrics,
|
||||||
debounce(async () => await updateMetrics(testId.value), { debounceTime: 400 }),
|
debounce(async () => await updateMetrics(testId.value), { debounceTime: 400 }),
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
watch(() => state.value.tags, getExamplePinnedDataForTags);
|
||||||
watch(
|
watch(
|
||||||
() => [
|
() => [
|
||||||
state.value.description,
|
state.value.description,
|
||||||
|
@ -219,6 +250,8 @@ watch(
|
||||||
:start-editing="startEditing"
|
:start-editing="startEditing"
|
||||||
:save-changes="saveChanges"
|
:save-changes="saveChanges"
|
||||||
:create-tag="handleCreateTag"
|
:create-tag="handleCreateTag"
|
||||||
|
:example-pinned-data="examplePinnedData"
|
||||||
|
:sample-workflow-name="workflowName"
|
||||||
@open-pinning-modal="openPinningModal"
|
@open-pinning-modal="openPinningModal"
|
||||||
@delete-metric="onDeleteMetric"
|
@delete-metric="onDeleteMetric"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useLoadingService } from '@/composables/useLoadingService';
|
import { useLoadingService } from '@/composables/useLoadingService';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL, VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import type { IWorkflowDataCreate } from '@/Interface';
|
|
||||||
import { SAMPLE_SUBWORKFLOW_WORKFLOW } from '@/constants.workflows';
|
|
||||||
|
|
||||||
const loadingService = useLoadingService();
|
const loadingService = useLoadingService();
|
||||||
const templateStore = useTemplatesStore();
|
const templateStore = useTemplatesStore();
|
||||||
|
@ -17,10 +15,6 @@ const route = useRoute();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const openWorkflowTemplate = async (templateId: string) => {
|
const openWorkflowTemplate = async (templateId: string) => {
|
||||||
if (templateId === SAMPLE_SUBWORKFLOW_WORKFLOW.meta.templateId) {
|
|
||||||
await openSampleSubworkflow();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
loadingService.startLoading();
|
loadingService.startLoading();
|
||||||
const template = await templateStore.getFixedWorkflowTemplate(templateId);
|
const template = await templateStore.getFixedWorkflowTemplate(templateId);
|
||||||
|
@ -58,42 +52,6 @@ const openWorkflowTemplate = async (templateId: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openSampleSubworkflow = async () => {
|
|
||||||
try {
|
|
||||||
loadingService.startLoading();
|
|
||||||
|
|
||||||
const projectId = route.query?.projectId;
|
|
||||||
|
|
||||||
const sampleSubWorkflows = Number(route.query?.sampleSubWorkflows ?? 0);
|
|
||||||
|
|
||||||
const workflowName = `${SAMPLE_SUBWORKFLOW_WORKFLOW.name} ${sampleSubWorkflows + 1}`;
|
|
||||||
|
|
||||||
const workflow: IWorkflowDataCreate = {
|
|
||||||
...SAMPLE_SUBWORKFLOW_WORKFLOW,
|
|
||||||
name: workflowName,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (projectId) {
|
|
||||||
workflow.projectId = projectId as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newWorkflow = await workflowsStore.createNewWorkflow(workflow);
|
|
||||||
|
|
||||||
const sampleSubworkflowChannel = new BroadcastChannel(NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL);
|
|
||||||
|
|
||||||
sampleSubworkflowChannel.postMessage({ workflowId: newWorkflow.id });
|
|
||||||
|
|
||||||
await router.replace({
|
|
||||||
name: VIEWS.WORKFLOW,
|
|
||||||
params: { name: newWorkflow.id },
|
|
||||||
});
|
|
||||||
loadingService.stopLoading();
|
|
||||||
} catch (e) {
|
|
||||||
await router.replace({ name: VIEWS.NEW_WORKFLOW });
|
|
||||||
loadingService.stopLoading();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const templateId = route.params.id;
|
const templateId = route.params.id;
|
||||||
if (!templateId || typeof templateId !== 'string') {
|
if (!templateId || typeof templateId !== 'string') {
|
||||||
|
|
Loading…
Reference in a new issue