mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
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:
parent
42926ba960
commit
ee05fd9fef
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,36 +38,41 @@ const toggleExpand = async () => {
|
|||
|
||||
<template>
|
||||
<div ref="containerRef" :class="[$style.evaluationStep, small && $style.small]">
|
||||
<div :class="$style.content">
|
||||
<div :class="$style.header">
|
||||
<div :class="[$style.icon, warning && $style.warning]">
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
<h3 :class="$style.title">{{ title }}</h3>
|
||||
<span v-if="warning" :class="$style.warningIcon">⚠</span>
|
||||
<button
|
||||
v-if="$slots.cardContent"
|
||||
:class="$style.collapseButton"
|
||||
:aria-expanded="isExpanded"
|
||||
:aria-controls="'content-' + title.replace(/\s+/g, '-')"
|
||||
@click="toggleExpand"
|
||||
>
|
||||
{{
|
||||
isExpanded
|
||||
? locale.baseText('testDefinition.edit.step.collapse')
|
||||
: locale.baseText('testDefinition.edit.step.expand')
|
||||
}}
|
||||
<font-awesome-icon :icon="isExpanded ? 'angle-down' : 'angle-right'" size="lg" />
|
||||
</button>
|
||||
</div>
|
||||
<ElCollapseTransition v-if="$slots.cardContent">
|
||||
<div v-show="isExpanded" :class="$style.cardContentWrapper">
|
||||
<div ref="contentRef" :class="$style.cardContent">
|
||||
<slot name="cardContent" />
|
||||
<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]">
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
<h3 :class="$style.title">{{ title }}</h3>
|
||||
<span v-if="warning" :class="$style.warningIcon">⚠</span>
|
||||
<button
|
||||
v-if="$slots.cardContent"
|
||||
:class="$style.collapseButton"
|
||||
:aria-expanded="isExpanded"
|
||||
:aria-controls="'content-' + title.replace(/\s+/g, '-')"
|
||||
@click="toggleExpand"
|
||||
>
|
||||
{{
|
||||
isExpanded
|
||||
? locale.baseText('testDefinition.edit.step.collapse')
|
||||
: locale.baseText('testDefinition.edit.step.expand')
|
||||
}}
|
||||
<font-awesome-icon :icon="isExpanded ? 'angle-down' : 'angle-right'" size="lg" />
|
||||
</button>
|
||||
</div>
|
||||
</ElCollapseTransition>
|
||||
</div>
|
||||
<ElCollapseTransition v-if="$slots.cardContent">
|
||||
<div v-show="isExpanded" :class="$style.cardContentWrapper">
|
||||
<div ref="contentRef" :class="$style.cardContent">
|
||||
<slot name="cardContent" />
|
||||
</div>
|
||||
</div>
|
||||
</ElCollapseTransition>
|
||||
</div>
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -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')"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue