Add tooltips and limit tag selection in test definitions

- Add tooltips to test definition steps explaining their purpose
- Limit tag selection to single tag in test definitions
- Add multiple-limit prop to N8nSelect component
- Update TagsDropdown to support multiple-limit
This commit is contained in:
Oleg Ivaniv 2024-12-10 08:41:51 +01:00
parent 42926ba960
commit ee05fd9fef
No known key found for this signature in database
6 changed files with 93 additions and 73 deletions

View file

@ -31,6 +31,10 @@ const props = defineProps({
multiple: {
type: Boolean,
},
multipleLimit: {
type: Number,
default: 0,
},
filterMethod: {
type: Function,
},
@ -120,6 +124,7 @@ defineExpose({
<ElSelect
v-bind="{ ...$props, ...listeners }"
ref="innerSelect"
:multiple-limit="props.multipleLimit"
:model-value="props.modelValue ?? undefined"
:size="computedSize"
:popper-class="props.popperClass"

View file

@ -19,6 +19,7 @@ interface TagsDropdownProps {
createEnabled?: boolean;
manageEnabled?: boolean;
createTag?: (name: string) => Promise<ITag>;
multipleLimit?: number;
}
const i18n = useI18n();
@ -32,6 +33,7 @@ const props = withDefaults(defineProps<TagsDropdownProps>(), {
createEnabled: true,
manageEnabled: true,
createTag: undefined,
multipleLimit: 0,
});
const emit = defineEmits<{
@ -222,6 +224,7 @@ onClickOutside(
:filter-method="filterOptions"
filterable
multiple
:multiple-limit="props.multipleLimit"
:reserve-keyword="false"
loading-text="..."
:popper-class="dropdownClasses"

View file

@ -2,12 +2,14 @@
import { useI18n } from '@/composables/useI18n';
import { ElCollapseTransition } from 'element-plus';
import { ref, nextTick } from 'vue';
import N8nTooltip from 'n8n-design-system/components/N8nTooltip';
interface EvaluationStep {
title: string;
warning?: boolean;
small?: boolean;
expanded?: boolean;
tooltip?: string;
}
const props = withDefaults(defineProps<EvaluationStep>(), {
@ -15,6 +17,7 @@ const props = withDefaults(defineProps<EvaluationStep>(), {
warning: false,
small: false,
expanded: true,
tooltip: '',
});
const locale = useI18n();
@ -35,6 +38,10 @@ const toggleExpand = async () => {
<template>
<div ref="containerRef" :class="[$style.evaluationStep, small && $style.small]">
<N8nTooltip :disabled="!tooltip" placement="right" :offset="25">
<template #content>
{{ tooltip }}
</template>
<div :class="$style.content">
<div :class="$style.header">
<div :class="[$style.icon, warning && $style.warning]">
@ -65,6 +72,7 @@ const toggleExpand = async () => {
</div>
</ElCollapseTransition>
</div>
</N8nTooltip>
</div>
</template>

View file

@ -89,6 +89,7 @@ function updateTags(tags: string[]) {
:event-bus="tagsEventBus"
:create-tag="createTag"
:manage-enabled="false"
:multiple-limit="1"
@update:model-value="updateTags"
@esc="cancelEditing('tags')"
@blur="saveChanges('tags')"

View file

@ -2759,14 +2759,20 @@
"testDefinition.edit.testSaved": "Test saved",
"testDefinition.edit.testSaveFailed": "Failed to save test",
"testDefinition.edit.description": "Description",
"testDefinition.edit.description.tooltip": "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": "Fetch past executions | Fetch {count} past execution | Fetch {count} past executions",
"testDefinition.edit.step.executions.tooltip": "Select which tagged executions to use as test cases. Each execution will be replayed to compare performance",
"testDefinition.edit.step.nodes": "Mock nodes",
"testDefinition.edit.step.mockedNodes": "No nodes mocked | {count} node mocked | {count} nodes mocked",
"testDefinition.edit.step.nodes.tooltip": "Replace specific nodes with test data to isolate what you're testing",
"testDefinition.edit.step.reRunExecutions": "Re-run executions",
"testDefinition.edit.step.reRunExecutions.tooltip": "Each test case will be re-run using the current workflow version",
"testDefinition.edit.step.compareExecutions": "Compare each past and new execution",
"testDefinition.edit.step.compareExecutions.tooltip": "Select which workflow to use for running the comparison tests",
"testDefinition.edit.step.metrics": "Summarise metrics",
"testDefinition.edit.step.metrics.tooltip": "Define which output fields to track and compare between test runs",
"testDefinition.edit.step.collapse": "Collapse",
"testDefinition.edit.step.expand": "Expand",
"testDefinition.list.testDeleted": "Test deleted",

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { computed, onMounted, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { VIEWS } from '@/constants';
import { useToast } from '@/composables/useToast';
@ -15,8 +15,6 @@ import TagsInput from '@/components/TestDefinition/EditDefinition/TagsInput.vue'
import WorkflowSelector from '@/components/TestDefinition/EditDefinition/WorkflowSelector.vue';
import MetricsInput from '@/components/TestDefinition/EditDefinition/MetricsInput.vue';
import type { TestMetricRecord } from '@/api/testDefinition.ee';
import { useExecutionsStore } from '@/stores/executions.store';
import type { IExecutionsListResponse } from '@/Interface';
const props = defineProps<{
testId?: string;
@ -27,23 +25,7 @@ const route = useRoute();
const locale = useI18n();
const { debounce } = useDebounce();
const toast = useToast();
const { fetchExecutions } = useExecutionsStore();
const tagsStore = useAnnotationTagsStore();
const isLoading = computed(() => tagsStore.isLoading);
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 buttonLabel = computed(() =>
testId.value
? locale.baseText('testDefinition.edit.updateTest')
: locale.baseText('testDefinition.edit.saveTest'),
);
const matchedExecutions = ref<IExecutionsListResponse['results']>([]);
const {
state,
fieldsIssues,
@ -59,14 +41,26 @@ const {
updateMetrics,
} = useTestDefinitionForm();
const isLoading = computed(() => tagsStore.isLoading);
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 buttonLabel = computed(() =>
testId.value
? locale.baseText('testDefinition.edit.updateTest')
: locale.baseText('testDefinition.edit.saveTest'),
);
const tagUsageCount = computed(
() => tagsStore.tagsById[state.value.tags.value[0]]?.usageCount ?? 0,
);
onMounted(async () => {
await tagsStore.fetchAll();
void tagsStore.fetchAll({ withUsageCount: true });
if (testId.value) {
await loadTestData(testId.value);
// Now tags are in state.tags.value instead of appliedTagIds
if (state.value.tags.value.length > 0) {
await fetchSelectedExecutions();
}
} else {
await onSaveTest();
}
@ -105,25 +99,6 @@ async function onDeleteMetric(deletedMetric: Partial<TestMetricRecord>) {
}
}
async function fetchSelectedExecutions() {
// Use state.tags.value for the annotationTags
const executionsForTags = await fetchExecutions({
annotationTags: state.value.tags.value,
});
matchedExecutions.value = executionsForTags.results;
}
// Debounced watchers for auto-saving
watch([() => state.value.evaluationWorkflow], debounce(onSaveTest, { debounceTime: 400 }), {
deep: true,
});
watch(
() => state.value.metrics,
debounce(async () => await updateMetrics(testId.value), { debounceTime: 400 }),
{ deep: true },
);
async function handleCreateTag(tagName: string) {
try {
const newTag = await tagsStore.create(tagName);
@ -134,7 +109,23 @@ async function handleCreateTag(tagName: string) {
}
}
watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: true });
// Debounced watchers for auto-saving
watch(
() => state.value.metrics,
debounce(async () => await updateMetrics(testId.value), { debounceTime: 400 }),
{ deep: true },
);
watch(
() => [
state.value.description,
state.value.name,
state.value.tags,
state.value.evaluationWorkflow,
],
debounce(onSaveTest, { debounceTime: 400 }),
{ deep: true },
);
</script>
<template>
@ -152,6 +143,7 @@ watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: tr
:class="$style.step"
:title="locale.baseText('testDefinition.edit.description')"
:expanded="false"
:tooltip="locale.baseText('testDefinition.edit.description.tooltip')"
>
<template #icon><font-awesome-icon icon="thumbtack" size="lg" /></template>
<template #cardContent>
@ -166,9 +158,10 @@ watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: tr
:class="$style.step"
:title="
locale.baseText('testDefinition.edit.step.executions', {
adjustToNumber: matchedExecutions.length,
adjustToNumber: tagUsageCount,
})
"
:tooltip="locale.baseText('testDefinition.edit.step.executions.tooltip')"
>
<template #icon><font-awesome-icon icon="history" size="lg" /></template>
<template #cardContent>
@ -194,6 +187,7 @@ watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: tr
:title="locale.baseText('testDefinition.edit.step.nodes')"
:small="true"
:expanded="false"
:tooltip="locale.baseText('testDefinition.edit.step.nodes.tooltip')"
>
<template #icon><font-awesome-icon icon="thumbtack" size="lg" /></template>
<template #cardContent>{{
@ -205,6 +199,7 @@ watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: tr
:class="$style.step"
:title="locale.baseText('testDefinition.edit.step.reRunExecutions')"
:small="true"
:tooltip="locale.baseText('testDefinition.edit.step.reRunExecutions.tooltip')"
>
<template #icon><font-awesome-icon icon="redo" size="lg" /></template>
</EvaluationStep>
@ -212,6 +207,7 @@ watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: tr
<EvaluationStep
:class="$style.step"
:title="locale.baseText('testDefinition.edit.step.compareExecutions')"
:tooltip="locale.baseText('testDefinition.edit.step.compareExecutions.tooltip')"
>
<template #icon><font-awesome-icon icon="equals" size="lg" /></template>
<template #cardContent>
@ -225,6 +221,7 @@ watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: tr
<EvaluationStep
:class="$style.step"
:title="locale.baseText('testDefinition.edit.step.metrics')"
:tooltip="locale.baseText('testDefinition.edit.step.metrics.tooltip')"
>
<template #icon><font-awesome-icon icon="chart-bar" size="lg" /></template>
<template #cardContent>