mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-19 19:11:13 +00:00
feat(editor): Add workflow evaluation run views (no-changelog) (#12258)
This commit is contained in:
@@ -1,27 +1,10 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import type { ComponentPublicInstance } from 'vue';
|
||||
import type { INodeParameterResourceLocator } from 'n8n-workflow';
|
||||
import type { ComponentPublicInstance, ComputedRef } from 'vue';
|
||||
import { useTestDefinitionStore } from '@/stores/testDefinition.store.ee';
|
||||
import type AnnotationTagsDropdownEe from '@/components/AnnotationTagsDropdown.ee.vue';
|
||||
import type { N8nInput } from 'n8n-design-system';
|
||||
import type { UpdateTestDefinitionParams } from '@/api/testDefinition.ee';
|
||||
|
||||
interface EditableField {
|
||||
value: string;
|
||||
isEditing: boolean;
|
||||
tempValue: string;
|
||||
}
|
||||
|
||||
export interface IEvaluationFormState {
|
||||
name: EditableField;
|
||||
description: string;
|
||||
tags: {
|
||||
isEditing: boolean;
|
||||
appliedTagIds: string[];
|
||||
};
|
||||
evaluationWorkflow: INodeParameterResourceLocator;
|
||||
metrics: string[];
|
||||
}
|
||||
import type { EditableField, EditableFormState, EvaluationFormState } from '../types';
|
||||
|
||||
type FormRefs = {
|
||||
nameInput: ComponentPublicInstance<typeof N8nInput>;
|
||||
@@ -29,64 +12,75 @@ type FormRefs = {
|
||||
};
|
||||
|
||||
export function useTestDefinitionForm() {
|
||||
// Stores
|
||||
const evaluationsStore = useTestDefinitionStore();
|
||||
|
||||
// Form state
|
||||
const state = ref<IEvaluationFormState>({
|
||||
description: '',
|
||||
// State initialization
|
||||
const state = ref<EvaluationFormState>({
|
||||
name: {
|
||||
value: `My Test [${new Date().toLocaleString(undefined, { month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' })}]`,
|
||||
isEditing: false,
|
||||
value: `My Test ${evaluationsStore.allTestDefinitions.length + 1}`,
|
||||
tempValue: '',
|
||||
isEditing: false,
|
||||
},
|
||||
tags: {
|
||||
value: [],
|
||||
tempValue: [],
|
||||
isEditing: false,
|
||||
appliedTagIds: [],
|
||||
},
|
||||
description: '',
|
||||
evaluationWorkflow: {
|
||||
mode: 'list',
|
||||
value: '',
|
||||
__rl: true,
|
||||
},
|
||||
metrics: [''],
|
||||
metrics: [],
|
||||
mockedNodes: [],
|
||||
});
|
||||
|
||||
// Loading states
|
||||
const isSaving = ref(false);
|
||||
const fieldsIssues = ref<Array<{ field: string; message: string }>>([]);
|
||||
|
||||
// Field refs
|
||||
const fields = ref<FormRefs>({} as FormRefs);
|
||||
|
||||
// Methods
|
||||
// A computed mapping of editable fields to their states
|
||||
// This ensures TS knows the exact type of each field.
|
||||
const editableFields: ComputedRef<{
|
||||
name: EditableField<string>;
|
||||
tags: EditableField<string[]>;
|
||||
}> = computed(() => ({
|
||||
name: state.value.name,
|
||||
tags: state.value.tags,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Load test data including metrics.
|
||||
*/
|
||||
const loadTestData = async (testId: string) => {
|
||||
try {
|
||||
await evaluationsStore.fetchAll({ force: true });
|
||||
const testDefinition = evaluationsStore.testDefinitionsById[testId];
|
||||
|
||||
if (testDefinition) {
|
||||
state.value = {
|
||||
description: testDefinition.description ?? '',
|
||||
name: {
|
||||
value: testDefinition.name ?? '',
|
||||
isEditing: false,
|
||||
tempValue: '',
|
||||
},
|
||||
tags: {
|
||||
isEditing: false,
|
||||
appliedTagIds: testDefinition.annotationTagId ? [testDefinition.annotationTagId] : [],
|
||||
},
|
||||
evaluationWorkflow: {
|
||||
mode: 'list',
|
||||
value: testDefinition.evaluationWorkflowId ?? '',
|
||||
__rl: true,
|
||||
},
|
||||
metrics: [''],
|
||||
const metrics = await evaluationsStore.fetchMetrics(testId);
|
||||
|
||||
state.value.description = testDefinition.description ?? '';
|
||||
state.value.name = {
|
||||
value: testDefinition.name ?? '',
|
||||
isEditing: false,
|
||||
tempValue: '',
|
||||
};
|
||||
state.value.tags = {
|
||||
isEditing: false,
|
||||
value: testDefinition.annotationTagId ? [testDefinition.annotationTagId] : [],
|
||||
tempValue: [],
|
||||
};
|
||||
state.value.evaluationWorkflow = {
|
||||
mode: 'list',
|
||||
value: testDefinition.evaluationWorkflowId ?? '',
|
||||
__rl: true,
|
||||
};
|
||||
state.value.metrics = metrics;
|
||||
state.value.mockedNodes = testDefinition.mockedNodes ?? [];
|
||||
}
|
||||
} catch (error) {
|
||||
// TODO: Throw better errors
|
||||
console.error('Failed to load test data', error);
|
||||
}
|
||||
};
|
||||
@@ -98,22 +92,43 @@ export function useTestDefinitionForm() {
|
||||
fieldsIssues.value = [];
|
||||
|
||||
try {
|
||||
// Prepare parameters for creating a new test
|
||||
const params = {
|
||||
name: state.value.name.value,
|
||||
workflowId,
|
||||
description: state.value.description,
|
||||
};
|
||||
|
||||
const newTest = await evaluationsStore.create(params);
|
||||
return newTest;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
return await evaluationsStore.create(params);
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMetric = async (metricId: string, testId: string) => {
|
||||
await evaluationsStore.deleteMetric({ id: metricId, testDefinitionId: testId });
|
||||
state.value.metrics = state.value.metrics.filter((metric) => metric.id !== metricId);
|
||||
};
|
||||
|
||||
const updateMetrics = async (testId: string) => {
|
||||
const promises = state.value.metrics.map(async (metric) => {
|
||||
if (!metric.name) return;
|
||||
if (!metric.id) {
|
||||
const createdMetric = await evaluationsStore.createMetric({
|
||||
name: metric.name,
|
||||
testDefinitionId: testId,
|
||||
});
|
||||
metric.id = createdMetric.id;
|
||||
} else {
|
||||
await evaluationsStore.updateMetric({
|
||||
name: metric.name,
|
||||
id: metric.id,
|
||||
testDefinitionId: testId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
const updateTest = async (testId: string) => {
|
||||
if (isSaving.value) return;
|
||||
|
||||
@@ -121,74 +136,93 @@ export function useTestDefinitionForm() {
|
||||
fieldsIssues.value = [];
|
||||
|
||||
try {
|
||||
// Check if the test ID is provided
|
||||
if (!testId) {
|
||||
throw new Error('Test ID is required for updating a test');
|
||||
}
|
||||
|
||||
// Prepare parameters for updating the existing test
|
||||
const params: UpdateTestDefinitionParams = {
|
||||
name: state.value.name.value,
|
||||
description: state.value.description,
|
||||
};
|
||||
|
||||
if (state.value.evaluationWorkflow.value) {
|
||||
params.evaluationWorkflowId = state.value.evaluationWorkflow.value.toString();
|
||||
}
|
||||
|
||||
const annotationTagId = state.value.tags.appliedTagIds[0];
|
||||
const annotationTagId = state.value.tags.value[0];
|
||||
if (annotationTagId) {
|
||||
params.annotationTagId = annotationTagId;
|
||||
}
|
||||
// Update the existing test
|
||||
if (state.value.mockedNodes.length > 0) {
|
||||
params.mockedNodes = state.value.mockedNodes;
|
||||
}
|
||||
|
||||
return await evaluationsStore.update({ ...params, id: testId });
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startEditing = async (field: string) => {
|
||||
if (field === 'name') {
|
||||
state.value.name.tempValue = state.value.name.value;
|
||||
state.value.name.isEditing = true;
|
||||
} else {
|
||||
state.value.tags.isEditing = true;
|
||||
/**
|
||||
* Start editing an editable field by copying `value` to `tempValue`.
|
||||
*/
|
||||
function startEditing<T extends keyof EditableFormState>(field: T) {
|
||||
const fieldObj = editableFields.value[field];
|
||||
if (fieldObj.isEditing) {
|
||||
// Already editing, do nothing
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const saveChanges = (field: string) => {
|
||||
if (field === 'name') {
|
||||
state.value.name.value = state.value.name.tempValue;
|
||||
state.value.name.isEditing = false;
|
||||
if (Array.isArray(fieldObj.value)) {
|
||||
fieldObj.tempValue = [...fieldObj.value];
|
||||
} else {
|
||||
state.value.tags.isEditing = false;
|
||||
fieldObj.tempValue = fieldObj.value;
|
||||
}
|
||||
};
|
||||
fieldObj.isEditing = true;
|
||||
}
|
||||
/**
|
||||
* Save changes by copying `tempValue` back into `value`.
|
||||
*/
|
||||
function saveChanges<T extends keyof EditableFormState>(field: T) {
|
||||
const fieldObj = editableFields.value[field];
|
||||
fieldObj.value = Array.isArray(fieldObj.tempValue)
|
||||
? [...fieldObj.tempValue]
|
||||
: fieldObj.tempValue;
|
||||
fieldObj.isEditing = false;
|
||||
}
|
||||
|
||||
const cancelEditing = (field: string) => {
|
||||
if (field === 'name') {
|
||||
state.value.name.isEditing = false;
|
||||
state.value.name.tempValue = '';
|
||||
/**
|
||||
* Cancel editing and revert `tempValue` from `value`.
|
||||
*/
|
||||
function cancelEditing<T extends keyof EditableFormState>(field: T) {
|
||||
const fieldObj = editableFields.value[field];
|
||||
if (Array.isArray(fieldObj.value)) {
|
||||
fieldObj.tempValue = [...fieldObj.value];
|
||||
} else {
|
||||
state.value.tags.isEditing = false;
|
||||
fieldObj.tempValue = fieldObj.value;
|
||||
}
|
||||
};
|
||||
fieldObj.isEditing = false;
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent, field: string) => {
|
||||
/**
|
||||
* Handle keyboard events during editing.
|
||||
*/
|
||||
function handleKeydown<T extends keyof EditableFormState>(event: KeyboardEvent, field: T) {
|
||||
if (event.key === 'Escape') {
|
||||
cancelEditing(field);
|
||||
} else if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
saveChanges(field);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
fields,
|
||||
isSaving: computed(() => isSaving.value),
|
||||
fieldsIssues: computed(() => fieldsIssues.value),
|
||||
deleteMetric,
|
||||
updateMetrics,
|
||||
loadTestData,
|
||||
createTest,
|
||||
updateTest,
|
||||
|
||||
Reference in New Issue
Block a user