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: {
|
multiple: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
multipleLimit: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
filterMethod: {
|
filterMethod: {
|
||||||
type: Function,
|
type: Function,
|
||||||
},
|
},
|
||||||
|
@ -120,6 +124,7 @@ defineExpose({
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-bind="{ ...$props, ...listeners }"
|
v-bind="{ ...$props, ...listeners }"
|
||||||
ref="innerSelect"
|
ref="innerSelect"
|
||||||
|
:multiple-limit="props.multipleLimit"
|
||||||
:model-value="props.modelValue ?? undefined"
|
:model-value="props.modelValue ?? undefined"
|
||||||
:size="computedSize"
|
:size="computedSize"
|
||||||
:popper-class="props.popperClass"
|
:popper-class="props.popperClass"
|
||||||
|
|
|
@ -19,6 +19,7 @@ interface TagsDropdownProps {
|
||||||
createEnabled?: boolean;
|
createEnabled?: boolean;
|
||||||
manageEnabled?: boolean;
|
manageEnabled?: boolean;
|
||||||
createTag?: (name: string) => Promise<ITag>;
|
createTag?: (name: string) => Promise<ITag>;
|
||||||
|
multipleLimit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
@ -32,6 +33,7 @@ const props = withDefaults(defineProps<TagsDropdownProps>(), {
|
||||||
createEnabled: true,
|
createEnabled: true,
|
||||||
manageEnabled: true,
|
manageEnabled: true,
|
||||||
createTag: undefined,
|
createTag: undefined,
|
||||||
|
multipleLimit: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -222,6 +224,7 @@ onClickOutside(
|
||||||
:filter-method="filterOptions"
|
:filter-method="filterOptions"
|
||||||
filterable
|
filterable
|
||||||
multiple
|
multiple
|
||||||
|
:multiple-limit="props.multipleLimit"
|
||||||
:reserve-keyword="false"
|
:reserve-keyword="false"
|
||||||
loading-text="..."
|
loading-text="..."
|
||||||
:popper-class="dropdownClasses"
|
:popper-class="dropdownClasses"
|
||||||
|
|
|
@ -2,12 +2,14 @@
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { ElCollapseTransition } from 'element-plus';
|
import { ElCollapseTransition } from 'element-plus';
|
||||||
import { ref, nextTick } from 'vue';
|
import { ref, nextTick } from 'vue';
|
||||||
|
import N8nTooltip from 'n8n-design-system/components/N8nTooltip';
|
||||||
|
|
||||||
interface EvaluationStep {
|
interface EvaluationStep {
|
||||||
title: string;
|
title: string;
|
||||||
warning?: boolean;
|
warning?: boolean;
|
||||||
small?: boolean;
|
small?: boolean;
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
|
tooltip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<EvaluationStep>(), {
|
const props = withDefaults(defineProps<EvaluationStep>(), {
|
||||||
|
@ -15,6 +17,7 @@ const props = withDefaults(defineProps<EvaluationStep>(), {
|
||||||
warning: false,
|
warning: false,
|
||||||
small: false,
|
small: false,
|
||||||
expanded: true,
|
expanded: true,
|
||||||
|
tooltip: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
|
@ -35,36 +38,41 @@ const toggleExpand = async () => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="containerRef" :class="[$style.evaluationStep, small && $style.small]">
|
<div ref="containerRef" :class="[$style.evaluationStep, small && $style.small]">
|
||||||
<div :class="$style.content">
|
<N8nTooltip :disabled="!tooltip" placement="right" :offset="25">
|
||||||
<div :class="$style.header">
|
<template #content>
|
||||||
<div :class="[$style.icon, warning && $style.warning]">
|
{{ tooltip }}
|
||||||
<slot name="icon" />
|
</template>
|
||||||
</div>
|
<div :class="$style.content">
|
||||||
<h3 :class="$style.title">{{ title }}</h3>
|
<div :class="$style.header">
|
||||||
<span v-if="warning" :class="$style.warningIcon">⚠</span>
|
<div :class="[$style.icon, warning && $style.warning]">
|
||||||
<button
|
<slot name="icon" />
|
||||||
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" />
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</ElCollapseTransition>
|
<ElCollapseTransition v-if="$slots.cardContent">
|
||||||
</div>
|
<div v-show="isExpanded" :class="$style.cardContentWrapper">
|
||||||
|
<div ref="contentRef" :class="$style.cardContent">
|
||||||
|
<slot name="cardContent" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElCollapseTransition>
|
||||||
|
</div>
|
||||||
|
</N8nTooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -89,6 +89,7 @@ function updateTags(tags: string[]) {
|
||||||
:event-bus="tagsEventBus"
|
:event-bus="tagsEventBus"
|
||||||
:create-tag="createTag"
|
:create-tag="createTag"
|
||||||
:manage-enabled="false"
|
:manage-enabled="false"
|
||||||
|
:multiple-limit="1"
|
||||||
@update:model-value="updateTags"
|
@update:model-value="updateTags"
|
||||||
@esc="cancelEditing('tags')"
|
@esc="cancelEditing('tags')"
|
||||||
@blur="saveChanges('tags')"
|
@blur="saveChanges('tags')"
|
||||||
|
|
|
@ -2759,14 +2759,20 @@
|
||||||
"testDefinition.edit.testSaved": "Test saved",
|
"testDefinition.edit.testSaved": "Test saved",
|
||||||
"testDefinition.edit.testSaveFailed": "Failed to save test",
|
"testDefinition.edit.testSaveFailed": "Failed to save test",
|
||||||
"testDefinition.edit.description": "Description",
|
"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.tagName": "Tag name",
|
||||||
"testDefinition.edit.step.intro": "When running a test",
|
"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": "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.nodes": "Mock nodes",
|
||||||
"testDefinition.edit.step.mockedNodes": "No nodes mocked | {count} node mocked | {count} nodes mocked",
|
"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": "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": "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": "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.collapse": "Collapse",
|
||||||
"testDefinition.edit.step.expand": "Expand",
|
"testDefinition.edit.step.expand": "Expand",
|
||||||
"testDefinition.list.testDeleted": "Test deleted",
|
"testDefinition.list.testDeleted": "Test deleted",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import { useToast } from '@/composables/useToast';
|
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 WorkflowSelector from '@/components/TestDefinition/EditDefinition/WorkflowSelector.vue';
|
||||||
import MetricsInput from '@/components/TestDefinition/EditDefinition/MetricsInput.vue';
|
import MetricsInput from '@/components/TestDefinition/EditDefinition/MetricsInput.vue';
|
||||||
import type { TestMetricRecord } from '@/api/testDefinition.ee';
|
import type { TestMetricRecord } from '@/api/testDefinition.ee';
|
||||||
import { useExecutionsStore } from '@/stores/executions.store';
|
|
||||||
import type { IExecutionsListResponse } from '@/Interface';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
testId?: string;
|
testId?: string;
|
||||||
|
@ -27,23 +25,7 @@ const route = useRoute();
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
const { debounce } = useDebounce();
|
const { debounce } = useDebounce();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { fetchExecutions } = useExecutionsStore();
|
|
||||||
const tagsStore = useAnnotationTagsStore();
|
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 {
|
const {
|
||||||
state,
|
state,
|
||||||
fieldsIssues,
|
fieldsIssues,
|
||||||
|
@ -59,14 +41,26 @@ const {
|
||||||
updateMetrics,
|
updateMetrics,
|
||||||
} = useTestDefinitionForm();
|
} = 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 () => {
|
onMounted(async () => {
|
||||||
await tagsStore.fetchAll();
|
void tagsStore.fetchAll({ withUsageCount: true });
|
||||||
if (testId.value) {
|
if (testId.value) {
|
||||||
await loadTestData(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 {
|
} else {
|
||||||
await onSaveTest();
|
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) {
|
async function handleCreateTag(tagName: string) {
|
||||||
try {
|
try {
|
||||||
const newTag = await tagsStore.create(tagName);
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -152,6 +143,7 @@ watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: tr
|
||||||
:class="$style.step"
|
:class="$style.step"
|
||||||
:title="locale.baseText('testDefinition.edit.description')"
|
:title="locale.baseText('testDefinition.edit.description')"
|
||||||
:expanded="false"
|
:expanded="false"
|
||||||
|
:tooltip="locale.baseText('testDefinition.edit.description.tooltip')"
|
||||||
>
|
>
|
||||||
<template #icon><font-awesome-icon icon="thumbtack" size="lg" /></template>
|
<template #icon><font-awesome-icon icon="thumbtack" size="lg" /></template>
|
||||||
<template #cardContent>
|
<template #cardContent>
|
||||||
|
@ -166,9 +158,10 @@ watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: tr
|
||||||
:class="$style.step"
|
:class="$style.step"
|
||||||
:title="
|
:title="
|
||||||
locale.baseText('testDefinition.edit.step.executions', {
|
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 #icon><font-awesome-icon icon="history" size="lg" /></template>
|
||||||
<template #cardContent>
|
<template #cardContent>
|
||||||
|
@ -194,6 +187,7 @@ watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: tr
|
||||||
:title="locale.baseText('testDefinition.edit.step.nodes')"
|
:title="locale.baseText('testDefinition.edit.step.nodes')"
|
||||||
:small="true"
|
:small="true"
|
||||||
:expanded="false"
|
:expanded="false"
|
||||||
|
:tooltip="locale.baseText('testDefinition.edit.step.nodes.tooltip')"
|
||||||
>
|
>
|
||||||
<template #icon><font-awesome-icon icon="thumbtack" size="lg" /></template>
|
<template #icon><font-awesome-icon icon="thumbtack" size="lg" /></template>
|
||||||
<template #cardContent>{{
|
<template #cardContent>{{
|
||||||
|
@ -205,6 +199,7 @@ watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: tr
|
||||||
:class="$style.step"
|
:class="$style.step"
|
||||||
:title="locale.baseText('testDefinition.edit.step.reRunExecutions')"
|
:title="locale.baseText('testDefinition.edit.step.reRunExecutions')"
|
||||||
:small="true"
|
:small="true"
|
||||||
|
:tooltip="locale.baseText('testDefinition.edit.step.reRunExecutions.tooltip')"
|
||||||
>
|
>
|
||||||
<template #icon><font-awesome-icon icon="redo" size="lg" /></template>
|
<template #icon><font-awesome-icon icon="redo" size="lg" /></template>
|
||||||
</EvaluationStep>
|
</EvaluationStep>
|
||||||
|
@ -212,6 +207,7 @@ watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: tr
|
||||||
<EvaluationStep
|
<EvaluationStep
|
||||||
:class="$style.step"
|
:class="$style.step"
|
||||||
:title="locale.baseText('testDefinition.edit.step.compareExecutions')"
|
: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 #icon><font-awesome-icon icon="equals" size="lg" /></template>
|
||||||
<template #cardContent>
|
<template #cardContent>
|
||||||
|
@ -225,6 +221,7 @@ watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: tr
|
||||||
<EvaluationStep
|
<EvaluationStep
|
||||||
:class="$style.step"
|
:class="$style.step"
|
||||||
:title="locale.baseText('testDefinition.edit.step.metrics')"
|
: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 #icon><font-awesome-icon icon="chart-bar" size="lg" /></template>
|
||||||
<template #cardContent>
|
<template #cardContent>
|
||||||
|
|
Loading…
Reference in a new issue