diff --git a/packages/editor-ui/src/components/TestDefinition/EditDefinition/DescriptionInput.vue b/packages/editor-ui/src/components/TestDefinition/EditDefinition/DescriptionInput.vue index 9b0ec16a7e..c858e04b0c 100644 --- a/packages/editor-ui/src/components/TestDefinition/EditDefinition/DescriptionInput.vue +++ b/packages/editor-ui/src/components/TestDefinition/EditDefinition/DescriptionInput.vue @@ -1,40 +1,84 @@ - - - + + + + + {{ modelValue.value.length > 0 ? modelValue.value : 'Add a description' }} + + + - + + saveChanges('description')" + @keydown="(e: KeyboardEvent) => handleKeydown(e, 'description')" + /> diff --git a/packages/editor-ui/src/components/TestDefinition/EditDefinition/EvaluationHeader.vue b/packages/editor-ui/src/components/TestDefinition/EditDefinition/EvaluationHeader.vue index 58b8e7dc0f..9f5baa66d9 100644 --- a/packages/editor-ui/src/components/TestDefinition/EditDefinition/EvaluationHeader.vue +++ b/packages/editor-ui/src/components/TestDefinition/EditDefinition/EvaluationHeader.vue @@ -18,8 +18,8 @@ const locale = useI18n(); diff --git a/packages/editor-ui/src/components/TestDefinition/EditDefinition/EvaluationStep.vue b/packages/editor-ui/src/components/TestDefinition/EditDefinition/EvaluationStep.vue index d2c6b6b911..806daa36b3 100644 --- a/packages/editor-ui/src/components/TestDefinition/EditDefinition/EvaluationStep.vue +++ b/packages/editor-ui/src/components/TestDefinition/EditDefinition/EvaluationStep.vue @@ -90,6 +90,7 @@ const toggleExpand = async () => { &.small { width: 80%; + margin-left: auto; } } .icon { diff --git a/packages/editor-ui/src/components/TestDefinition/ListDefinition/EmptyState.vue b/packages/editor-ui/src/components/TestDefinition/ListDefinition/EmptyState.vue index 9f507c327f..d2f022777b 100644 --- a/packages/editor-ui/src/components/TestDefinition/ListDefinition/EmptyState.vue +++ b/packages/editor-ui/src/components/TestDefinition/ListDefinition/EmptyState.vue @@ -11,19 +11,28 @@ const locale = useI18n(); {{ locale.baseText('testDefinition.list.tests') }} - + + + + diff --git a/packages/editor-ui/src/components/TestDefinition/ListRuns/MetricsChart.vue b/packages/editor-ui/src/components/TestDefinition/ListRuns/MetricsChart.vue index cd2e58f8de..4e3502dc6b 100644 --- a/packages/editor-ui/src/components/TestDefinition/ListRuns/MetricsChart.vue +++ b/packages/editor-ui/src/components/TestDefinition/ListRuns/MetricsChart.vue @@ -45,11 +45,11 @@ watchEffect(() => { - {{ locale.baseText('testDefinition.listRuns.metricsOverTime') }} { :value="metric" /> + {{ locale.baseText('testDefinition.listRuns.metricsOverTime') }} { diff --git a/packages/editor-ui/src/components/TestDefinition/composables/useMetricsChart.ts b/packages/editor-ui/src/components/TestDefinition/composables/useMetricsChart.ts index 9a792cc8fc..2e1faa2e58 100644 --- a/packages/editor-ui/src/components/TestDefinition/composables/useMetricsChart.ts +++ b/packages/editor-ui/src/components/TestDefinition/composables/useMetricsChart.ts @@ -79,7 +79,7 @@ export function useMetricsChart(mode: AppliedThemeOption = 'light') { color: colors.text.primary, }, title: { - display: true, + display: false, text: params.metric, padding: 16, color: colors.text.primary, @@ -90,14 +90,11 @@ export function useMetricsChart(mode: AppliedThemeOption = 'light') { display: false, }, ticks: { - maxRotation: 45, - minRotation: 45, - color: colors.text.primary, + display: false, }, title: { - display: true, text: params.xTitle, - padding: 16, + padding: 1, color: colors.text.primary, }, }, diff --git a/packages/editor-ui/src/components/TestDefinition/composables/useTestDefinitionForm.ts b/packages/editor-ui/src/components/TestDefinition/composables/useTestDefinitionForm.ts index 1b5fee84a0..fe87606bdd 100644 --- a/packages/editor-ui/src/components/TestDefinition/composables/useTestDefinitionForm.ts +++ b/packages/editor-ui/src/components/TestDefinition/composables/useTestDefinitionForm.ts @@ -26,7 +26,11 @@ export function useTestDefinitionForm() { tempValue: [], isEditing: false, }, - description: '', + description: { + value: '', + tempValue: '', + isEditing: false, + }, evaluationWorkflow: { mode: 'list', value: '', @@ -45,9 +49,11 @@ export function useTestDefinitionForm() { const editableFields: ComputedRef<{ name: EditableField; tags: EditableField; + description: EditableField; }> = computed(() => ({ name: state.value.name, tags: state.value.tags, + description: state.value.description, })); /** @@ -61,7 +67,11 @@ export function useTestDefinitionForm() { if (testDefinition) { const metrics = await evaluationsStore.fetchMetrics(testId); - state.value.description = testDefinition.description ?? ''; + state.value.description = { + value: testDefinition.description ?? '', + isEditing: false, + tempValue: '', + }; state.value.name = { value: testDefinition.name ?? '', isEditing: false, @@ -95,7 +105,7 @@ export function useTestDefinitionForm() { const params = { name: state.value.name.value, workflowId, - description: state.value.description, + description: state.value.description.value, }; return await evaluationsStore.create(params); } finally { @@ -125,8 +135,9 @@ export function useTestDefinitionForm() { }); } }); - + isSaving.value = true; await Promise.all(promises); + isSaving.value = false; }; const updateTest = async (testId: string) => { @@ -142,7 +153,7 @@ export function useTestDefinitionForm() { const params: UpdateTestDefinitionParams = { name: state.value.name.value, - description: state.value.description, + description: state.value.description.value, }; if (state.value.evaluationWorkflow.value) { @@ -157,7 +168,8 @@ export function useTestDefinitionForm() { params.mockedNodes = state.value.mockedNodes; } - return await evaluationsStore.update({ ...params, id: testId }); + const response = await evaluationsStore.update({ ...params, id: testId }); + return response; } finally { isSaving.value = false; } diff --git a/packages/editor-ui/src/components/TestDefinition/shared/TableCell.vue b/packages/editor-ui/src/components/TestDefinition/shared/TableCell.vue index 4555448a58..24913d2f95 100644 --- a/packages/editor-ui/src/components/TestDefinition/shared/TableCell.vue +++ b/packages/editor-ui/src/components/TestDefinition/shared/TableCell.vue @@ -1,10 +1,10 @@ - - - - - - - - - - - diff --git a/packages/editor-ui/src/components/TestDefinition/shared/TestTableBase.vue b/packages/editor-ui/src/components/TestDefinition/shared/TestTableBase.vue new file mode 100644 index 0000000000..8f2001d640 --- /dev/null +++ b/packages/editor-ui/src/components/TestDefinition/shared/TestTableBase.vue @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + diff --git a/packages/editor-ui/src/components/TestDefinition/tests/useTestDefinitionForm.test.ts b/packages/editor-ui/src/components/TestDefinition/tests/useTestDefinitionForm.test.ts index 98d67980d0..05f9d83038 100644 --- a/packages/editor-ui/src/components/TestDefinition/tests/useTestDefinitionForm.test.ts +++ b/packages/editor-ui/src/components/TestDefinition/tests/useTestDefinitionForm.test.ts @@ -42,7 +42,7 @@ describe('useTestDefinitionForm', () => { it('should initialize with default props', () => { const { state } = useTestDefinitionForm(); - expect(state.value.description).toBe(''); + expect(state.value.description.value).toBe(''); expect(state.value.name.value).toContain('My Test'); expect(state.value.tags.value).toEqual([]); expect(state.value.metrics).toEqual([]); @@ -70,7 +70,7 @@ describe('useTestDefinitionForm', () => { expect(fetchSpy).toBeCalled(); expect(fetchMetricsSpy).toBeCalledWith(TEST_DEF_A.id); expect(state.value.name.value).toEqual(TEST_DEF_A.name); - expect(state.value.description).toEqual(TEST_DEF_A.description); + expect(state.value.description.value).toEqual(TEST_DEF_A.description); expect(state.value.tags.value).toEqual([TEST_DEF_A.annotationTagId]); expect(state.value.evaluationWorkflow.value).toEqual(TEST_DEF_A.evaluationWorkflowId); expect(state.value.metrics).toEqual([ @@ -88,7 +88,7 @@ describe('useTestDefinitionForm', () => { await loadTestData('unknown-id'); expect(fetchSpy).toBeCalled(); // Should remain unchanged since no definition found - expect(state.value.description).toBe(''); + expect(state.value.description.value).toBe(''); expect(state.value.name.value).toContain('My Test'); expect(state.value.tags.value).toEqual([]); expect(state.value.metrics).toEqual([]); @@ -112,7 +112,7 @@ describe('useTestDefinitionForm', () => { const createSpy = vi.spyOn(useTestDefinitionStore(), 'create').mockResolvedValue(TEST_DEF_NEW); state.value.name.value = TEST_DEF_NEW.name; - state.value.description = TEST_DEF_NEW.description ?? ''; + state.value.description.value = TEST_DEF_NEW.description ?? ''; const newTest = await createTest('123'); expect(createSpy).toBeCalledWith({ @@ -143,7 +143,7 @@ describe('useTestDefinitionForm', () => { const updateSpy = vi.spyOn(useTestDefinitionStore(), 'update').mockResolvedValue(updatedBTest); state.value.name.value = TEST_DEF_B.name; - state.value.description = TEST_DEF_B.description ?? ''; + state.value.description.value = TEST_DEF_B.description ?? ''; const updatedTest = await updateTest(TEST_DEF_A.id); expect(updateSpy).toBeCalledWith({ @@ -166,7 +166,7 @@ describe('useTestDefinitionForm', () => { .mockRejectedValue(new Error('Update Failed')); state.value.name.value = 'Test'; - state.value.description = 'Some description'; + state.value.description.value = 'Some description'; await expect(updateTest(TEST_DEF_A.id)).rejects.toThrow('Update Failed'); expect(updateSpy).toBeCalled(); diff --git a/packages/editor-ui/src/components/TestDefinition/types.ts b/packages/editor-ui/src/components/TestDefinition/types.ts index f668fec106..087784f709 100644 --- a/packages/editor-ui/src/components/TestDefinition/types.ts +++ b/packages/editor-ui/src/components/TestDefinition/types.ts @@ -10,10 +10,10 @@ export interface EditableField { export interface EditableFormState { name: EditableField; tags: EditableField; + description: EditableField; } export interface EvaluationFormState extends EditableFormState { - description: string; evaluationWorkflow: INodeParameterResourceLocator; metrics: TestMetricRecord[]; mockedNodes: Array<{ name: string; id: string }>; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 913d753fd7..a458322d27 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -2775,7 +2775,9 @@ "communityPlusModal.button.confirm": "Send me a free license key", "executeWorkflowTrigger.createNewSubworkflow": "Create a sub-workflow in {projectName}", "executeWorkflowTrigger.createNewSubworkflow.noProject": "Create a new sub-workflow", - "testDefinition.edit.descriptionPlaceholder": "", + "testDefinition.edit.descriptionPlaceholder": "Enter test description", + "testDefinition.edit.showConfig": "Show config", + "testDefinition.edit.hideConfig": "Hide config", "testDefinition.edit.backButtonTitle": "Back to Workflow Evaluation", "testDefinition.edit.namePlaceholder": "Enter test name", "testDefinition.edit.metricsTitle": "Metrics", @@ -2790,14 +2792,15 @@ "testDefinition.edit.workflowSelectorTitle": "Workflow to make comparisons", "testDefinition.edit.workflowSelectorHelpText": "This workflow will be called once for each test case.", "testDefinition.edit.updateTest": "Update test", - "testDefinition.edit.saveTest": "Run test", + "testDefinition.edit.saveTest": "Save test", + "testDefinition.edit.runTest": "Run test", "testDefinition.edit.testSaved": "Test saved", "testDefinition.edit.testSaveFailed": "Failed to save test", "testDefinition.edit.description": "Description", "testDefinition.edit.description.description": "Add details about what this test evaluates and what success looks like", "testDefinition.edit.tagName": "Tag name", "testDefinition.edit.step.intro": "When running a test", - "testDefinition.edit.step.executions": "1. Fetch N past executions tagge | Fetch {count} past execution tagged | Fetch {count} past executions tagged", + "testDefinition.edit.step.executions": "1. Fetch N past executions tagged | 1. Fetch {count} past execution tagged | 1. Fetch {count} past executions tagged", "testDefinition.edit.step.executions.description": "Use a tag to select past executions for use as test cases in evaluation. The trigger data from each of these past executions will be used as input to run your workflow. The outputs of past executions are used as benchmark and compared against to check whether performance has changed based on logic and metrics that you define below.", "testDefinition.edit.step.mockedNodes": "2. Mock N nodes |2. Mock {count} node |2. Mock {count} nodes", "testDefinition.edit.step.nodes.description": "Mocked nodes have their data replayed rather than being re-executed. Do this to avoid calling external services, save time executing, and isolate what you are evaluating. If a node is mocked, the tagged past execution's output data for that node is used in the evaluation instead.", @@ -2814,11 +2817,17 @@ "testDefinition.edit.pastRuns": "Past runs", "testDefinition.edit.pastRuns.total": "No runs | {count} run | {count} runs", "testDefinition.edit.nodesPinning.pinButtonTooltip": "Pin execution data of this node during test run", + "testDefinition.edit.saving": "Saving...", + "testDefinition.edit.saved": "Changes saved", "testDefinition.list.testDeleted": "Test deleted", "testDefinition.list.tests": "Tests", + "testDefinition.list.evaluations": "Evaluations", + "testDefinition.list.unitTests.title": "Unit tests", + "testDefinition.list.unitTests.description": "Test sections of your workflow in isolation", + "testDefinition.list.unitTests.cta": "Register interest", "testDefinition.list.createNew": "Create new test", "testDefinition.list.actionDescription": "Replay past executions to check whether performance has changed", - "testDefinition.list.actionButton": "Create Test", + "testDefinition.list.actionButton": "Create an Evaluation", "testDefinition.list.testRuns": "No test runs | {count} test run | {count} test runs", "testDefinition.list.lastRun": "Ran", "testDefinition.list.running": "Running", diff --git a/packages/editor-ui/src/plugins/icons/index.ts b/packages/editor-ui/src/plugins/icons/index.ts index 187ab6d623..2e53ddabb8 100644 --- a/packages/editor-ui/src/plugins/icons/index.ts +++ b/packages/editor-ui/src/plugins/icons/index.ts @@ -53,6 +53,7 @@ import { faEnvelope, faEquals, faEye, + faEyeSlash, faExclamationTriangle, faExpand, faExpandAlt, @@ -228,6 +229,7 @@ export const FontAwesomePlugin: Plugin = { addIcon(faEnvelope); addIcon(faEquals); addIcon(faEye); + addIcon(faEyeSlash); addIcon(faExclamationTriangle); addIcon(faExclamationCircle); addIcon(faExpand); diff --git a/packages/editor-ui/src/views/TestDefinition/TestDefinitionEditView.vue b/packages/editor-ui/src/views/TestDefinition/TestDefinitionEditView.vue index 4806095566..fd3b9d5d97 100644 --- a/packages/editor-ui/src/views/TestDefinition/TestDefinitionEditView.vue +++ b/packages/editor-ui/src/views/TestDefinition/TestDefinitionEditView.vue @@ -36,6 +36,7 @@ const uiStore = useUIStore(); const { state, fieldsIssues, + isSaving, cancelEditing, loadTestData, createTest, @@ -52,13 +53,15 @@ const allTags = computed(() => tagsStore.allTags); const tagsById = computed(() => tagsStore.tagsById); const testId = computed(() => props.testId ?? (route.params.testId as string)); const currentWorkflowId = computed(() => route.params.name as string); - +const appliedTheme = computed(() => uiStore.appliedTheme); const tagUsageCount = computed( () => tagsStore.tagsById[state.value.tags.value[0]]?.usageCount ?? 0, ); - +const hasRuns = computed(() => runs.value.length > 0); const nodePinningModal = ref(null); const modalContentWidth = ref(0); +const showConfig = ref(true); +const selectedMetric = ref(''); onMounted(async () => { if (!testDefinitionStore.isFeatureEnabled) { @@ -135,14 +138,18 @@ const runs = computed(() => ), ); -async function onDeleteRuns(runs: TestRunRecord[]) { +async function onDeleteRuns(toDelete: TestRunRecord[]) { await Promise.all( - runs.map(async (run) => { + toDelete.map(async (run) => { await testDefinitionStore.deleteTestRun({ testDefinitionId: testId.value, runId: run.id }); }), ); } +function toggleConfig() { + showConfig.value = !showConfig.value; +} + // Debounced watchers for auto-saving watch( () => state.value.metrics, @@ -164,29 +171,82 @@ watch( - - - - - - - - - - - - + + + + + + + + {{ locale.baseText('testDefinition.edit.saving') }} + + {{ locale.baseText('testDefinition.edit.saved') }} + + + + + + + + + + + + + + + + + + + {{ locale.baseText('testDefinition.edit.step.intro') }} @@ -286,37 +346,6 @@ watch( - - - - - - - {{ - locale.baseText('testDefinition.edit.pastRuns') - }} - @@ -338,52 +367,102 @@ watch( diff --git a/packages/editor-ui/src/views/TestDefinition/TestDefinitionListView.vue b/packages/editor-ui/src/views/TestDefinition/TestDefinitionListView.vue index 3140ebe8f6..64cf826111 100644 --- a/packages/editor-ui/src/views/TestDefinition/TestDefinitionListView.vue +++ b/packages/editor-ui/src/views/TestDefinition/TestDefinitionListView.vue @@ -198,10 +198,10 @@ onMounted(() => { diff --git a/packages/editor-ui/src/views/TestDefinition/TestDefinitionRunDetailView.vue b/packages/editor-ui/src/views/TestDefinition/TestDefinitionRunDetailView.vue index 98d79dfd4c..d2858980fd 100644 --- a/packages/editor-ui/src/views/TestDefinition/TestDefinitionRunDetailView.vue +++ b/packages/editor-ui/src/views/TestDefinition/TestDefinitionRunDetailView.vue @@ -5,8 +5,8 @@ import { useRouter } from 'vue-router'; import { convertToDisplayDate } from '@/utils/typesUtils'; import { useI18n } from '@/composables/useI18n'; import { N8nCard, N8nText } from 'n8n-design-system'; -import TestDefinitionTable from '@/components/TestDefinition/shared/TestDefinitionTable.vue'; -import type { TestDefinitionTableColumn } from '@/components/TestDefinition/shared/TestDefinitionTable.vue'; +import TestTableBase from '@/components/TestDefinition/shared/TestTableBase.vue'; +import type { TestTableColumn } from '@/components/TestDefinition/shared/TestTableBase.vue'; import { useExecutionsStore } from '@/stores/executions.store'; import { get } from 'lodash-es'; import type { ExecutionSummaryWithScopes } from '@/Interface'; @@ -36,7 +36,7 @@ const filteredTestCases = computed(() => { }); const columns = computed( - (): Array> => [ + (): Array> => [ { prop: 'id', width: 200, @@ -193,7 +193,7 @@ onMounted(async () => { - { diff --git a/packages/editor-ui/src/views/TestDefinition/tests/TestDefinitionEditView.test.ts b/packages/editor-ui/src/views/TestDefinition/tests/TestDefinitionEditView.test.ts index 2b290701bd..3e85f86425 100644 --- a/packages/editor-ui/src/views/TestDefinition/tests/TestDefinitionEditView.test.ts +++ b/packages/editor-ui/src/views/TestDefinition/tests/TestDefinitionEditView.test.ts @@ -75,7 +75,7 @@ describe('TestDefinitionEditView', () => { vi.mocked(useTestDefinitionForm).mockReturnValue({ state: ref({ name: { value: '', isEditing: false, tempValue: '' }, - description: '', + description: { value: '', isEditing: false, tempValue: '' }, tags: { value: [], tempValue: [], isEditing: false }, evaluationWorkflow: { mode: 'list', value: '', __rl: true }, metrics: [], @@ -273,7 +273,7 @@ describe('TestDefinitionEditView', () => { ...vi.mocked(useTestDefinitionForm)(), state: ref({ name: { value: 'Test', isEditing: false, tempValue: '' }, - description: '', + description: { value: '', isEditing: false, tempValue: '' }, tags: { value: ['tag1'], tempValue: [], isEditing: false }, evaluationWorkflow: { mode: 'list', value: 'workflow1', __rl: true }, metrics: [],