mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
fix(editor): Ai 672 minor UI fixes on evaluation creation (#13461)
This commit is contained in:
committed by
GitHub
parent
5ad950f603
commit
b791677ffa
@@ -25,6 +25,7 @@ interface InfoTipProps {
|
|||||||
type?: (typeof TYPE)[number];
|
type?: (typeof TYPE)[number];
|
||||||
bold?: boolean;
|
bold?: boolean;
|
||||||
tooltipPlacement?: Placement;
|
tooltipPlacement?: Placement;
|
||||||
|
enterable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineOptions({ name: 'N8nInfoTip' });
|
defineOptions({ name: 'N8nInfoTip' });
|
||||||
@@ -33,6 +34,7 @@ const props = withDefaults(defineProps<InfoTipProps>(), {
|
|||||||
type: 'note',
|
type: 'note',
|
||||||
bold: true,
|
bold: true,
|
||||||
tooltipPlacement: 'top',
|
tooltipPlacement: 'top',
|
||||||
|
enterable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const iconData = computed<{ icon: IconMap[keyof IconMap]; color: IconColor }>(() => {
|
const iconData = computed<{ icon: IconMap[keyof IconMap]; color: IconColor }>(() => {
|
||||||
@@ -60,6 +62,7 @@ const iconData = computed<{ icon: IconMap[keyof IconMap]; color: IconColor }>(()
|
|||||||
:placement="tooltipPlacement"
|
:placement="tooltipPlacement"
|
||||||
:popper-class="$style.tooltipPopper"
|
:popper-class="$style.tooltipPopper"
|
||||||
:disabled="type !== 'tooltip'"
|
:disabled="type !== 'tooltip'"
|
||||||
|
:enterable
|
||||||
>
|
>
|
||||||
<span :class="$style.iconText">
|
<span :class="$style.iconText">
|
||||||
<N8nIcon :icon="iconData.icon" :color="iconData.color" />
|
<N8nIcon :icon="iconData.icon" :color="iconData.color" />
|
||||||
|
|||||||
@@ -1,125 +1,32 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts"></script>
|
||||||
import { computed, useCssModule } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
state?: 'default' | 'error' | 'success';
|
|
||||||
hoverable?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const css = useCssModule();
|
|
||||||
|
|
||||||
const classes = computed(() => ({
|
|
||||||
[css.arrowConnector]: true,
|
|
||||||
[css.hoverable]: props.hoverable,
|
|
||||||
[css.error]: props.state === 'error',
|
|
||||||
[css.success]: props.state === 'success',
|
|
||||||
}));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="classes">
|
<div :class="$style.blockArrow">
|
||||||
<div :class="$style.stalk"></div>
|
<div :class="$style.stalk"></div>
|
||||||
<div :class="$style.arrowHead"></div>
|
<div :class="$style.arrowHead"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.arrowConnector {
|
.blockArrow {
|
||||||
position: relative;
|
|
||||||
height: var(--arrow-height, 3rem);
|
|
||||||
margin: 0.1rem 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stalk {
|
.stalk {
|
||||||
position: relative;
|
min-height: 14px;
|
||||||
width: var(--stalk-width, 0.125rem);
|
width: 2px;
|
||||||
height: calc(100% - var(--arrow-tip-height, 0.5rem));
|
background-color: var(--color-foreground-xdark);
|
||||||
background-color: var(--arrow-color, var(--color-text-dark));
|
flex: 1;
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 1rem;
|
|
||||||
height: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrowHead {
|
.arrowHead {
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
border-left: calc(var(--arrow-tip-width, 0.75rem) / 2) solid transparent;
|
border-left: 5px solid transparent;
|
||||||
border-right: calc(var(--arrow-tip-width, 0.75rem) / 2) solid transparent;
|
border-right: 5px solid transparent;
|
||||||
border-top: var(--arrow-tip-height, 0.5rem) solid var(--arrow-color, var(--color-text-dark));
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&::after {
|
border-top: 10px solid var(--color-foreground-xdark);
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hoverable {
|
|
||||||
--hover-scale: var(--arrow-hover-scale, 1.8);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.stalk {
|
|
||||||
width: calc(var(--stalk-width, 0.125rem) * var(--hover-scale));
|
|
||||||
background-color: var(--arrow-hover-color, var(--arrow-color, var(--color-text-dark)));
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrowHead {
|
|
||||||
border-left-width: calc(var(--arrow-tip-width, 0.75rem) / 2 * var(--hover-scale));
|
|
||||||
border-right-width: calc(var(--arrow-tip-width, 0.75rem) / 2 * var(--hover-scale));
|
|
||||||
border-top-width: calc(var(--arrow-tip-height, 0.5rem) * var(--hover-scale));
|
|
||||||
border-top-color: var(--arrow-hover-color, var(--arrow-color, var(--color-text-dark)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
--stalk-width: 0.1875rem;
|
|
||||||
--arrow-color: var(--color-danger);
|
|
||||||
--arrow-tip-width: 1rem;
|
|
||||||
--arrow-tip-height: 0.625rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success {
|
|
||||||
--stalk-width: 0.1875rem;
|
|
||||||
--arrow-color: var(--color-success);
|
|
||||||
--arrow-tip-width: 1rem;
|
|
||||||
--arrow-tip-height: 0.625rem;
|
|
||||||
|
|
||||||
.stalk {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '✓';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--arrow-color, var(--color-success));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { ElCollapseTransition } from 'element-plus';
|
import { type Modifier, detectOverflow } from '@popperjs/core';
|
||||||
import { computed, nextTick, ref, useCssModule } from 'vue';
|
import { N8nInfoTip, N8nText, N8nTooltip } from 'n8n-design-system';
|
||||||
|
import { computed, ref, useCssModule } from 'vue';
|
||||||
|
|
||||||
interface EvaluationStep {
|
interface EvaluationStep {
|
||||||
title?: string;
|
title?: string;
|
||||||
warning?: boolean;
|
warning?: boolean;
|
||||||
small?: boolean;
|
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
issues?: Array<{ field: string; message: string }>;
|
issues?: Array<{ field: string; message: string }>;
|
||||||
showIssues?: boolean;
|
showIssues?: boolean;
|
||||||
tooltip?: string;
|
tooltip: string;
|
||||||
|
externalTooltip?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<EvaluationStep>(), {
|
const props = withDefaults(defineProps<EvaluationStep>(), {
|
||||||
description: '',
|
description: '',
|
||||||
warning: false,
|
warning: false,
|
||||||
small: false,
|
|
||||||
expanded: false,
|
expanded: false,
|
||||||
issues: () => [],
|
issues: () => [],
|
||||||
showIssues: true,
|
showIssues: true,
|
||||||
@@ -26,110 +26,115 @@ const props = withDefaults(defineProps<EvaluationStep>(), {
|
|||||||
|
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
const isExpanded = ref(props.expanded);
|
const isExpanded = ref(props.expanded);
|
||||||
const contentRef = ref<HTMLElement | null>(null);
|
|
||||||
const containerRef = ref<HTMLElement | null>(null);
|
|
||||||
const showTooltip = ref(false);
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
|
const hasIssues = computed(() => props.issues.length > 0);
|
||||||
|
|
||||||
const containerClass = computed(() => {
|
const containerClass = computed(() => {
|
||||||
return {
|
return {
|
||||||
[$style.wrap]: true,
|
[$style.evaluationStep]: true,
|
||||||
[$style.expanded]: isExpanded.value,
|
[$style['has-issues']]: true,
|
||||||
[$style.hasIssues]: props.issues.length > 0,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleExpand = async () => {
|
const toggleExpand = () => (isExpanded.value = !isExpanded.value);
|
||||||
isExpanded.value = !isExpanded.value;
|
|
||||||
if (isExpanded.value) {
|
|
||||||
await nextTick();
|
|
||||||
if (containerRef.value) {
|
|
||||||
containerRef.value.style.height = 'auto';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
|
||||||
if (!props.tooltip) return;
|
|
||||||
showTooltip.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
showTooltip.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderIssues = computed(() => props.showIssues && props.issues.length);
|
const renderIssues = computed(() => props.showIssues && props.issues.length);
|
||||||
const issuesList = computed(() => props.issues.map((issue) => issue.message).join(', '));
|
const issuesList = computed(() => props.issues.map((issue) => issue.message).join(', '));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://popper.js.org/docs/v2/modifiers/#custom-modifiers
|
||||||
|
*/
|
||||||
|
const resizeModifier: Modifier<'resize', {}> = {
|
||||||
|
name: 'resize',
|
||||||
|
enabled: true,
|
||||||
|
phase: 'beforeWrite',
|
||||||
|
requires: ['preventOverflow'],
|
||||||
|
fn({ state }) {
|
||||||
|
const overflow = detectOverflow(state);
|
||||||
|
const MARGIN_RIGHT = 15;
|
||||||
|
|
||||||
|
const maxWidth = state.rects.popper.width - overflow.right - MARGIN_RIGHT;
|
||||||
|
|
||||||
|
state.styles.popper.width = `${maxWidth}px`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const popperModifiers = [
|
||||||
|
resizeModifier,
|
||||||
|
{ name: 'preventOverflow', options: { boundary: 'document' } },
|
||||||
|
{ name: 'flip', enabled: false }, // prevent the tooltip from flipping
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="containerClass">
|
<div :class="containerClass" data-test-id="evaluation-step">
|
||||||
<slot name="containerPrefix" />
|
<div :class="$style.content">
|
||||||
<div
|
<N8nTooltip
|
||||||
ref="containerRef"
|
placement="right"
|
||||||
:class="[$style.evaluationStep, small && $style.small]"
|
:disabled="!externalTooltip"
|
||||||
data-test-id="evaluation-step"
|
:show-arrow="false"
|
||||||
@mouseenter="handleMouseEnter"
|
:popper-class="$style.evaluationTooltip"
|
||||||
@mouseleave="handleMouseLeave"
|
:popper-options="{ modifiers: popperModifiers }"
|
||||||
>
|
:content="tooltip"
|
||||||
<div :class="$style.content">
|
>
|
||||||
<div :class="$style.header" @click="toggleExpand">
|
<div :class="$style.header" @click="toggleExpand">
|
||||||
<h3 :class="$style.title">
|
<div :class="$style.label">
|
||||||
<span :class="$style.label">
|
<N8nText bold>
|
||||||
<slot v-if="$slots.title" name="title" />
|
<slot v-if="$slots.title" name="title" />
|
||||||
<span v-else>{{ title }}</span>
|
<template v-else>{{ title }}</template>
|
||||||
<N8nInfoTip
|
</N8nText>
|
||||||
v-if="tooltip"
|
<N8nInfoTip
|
||||||
:class="$style.infoTip"
|
v-if="!externalTooltip"
|
||||||
:bold="true"
|
:class="$style.infoTip"
|
||||||
type="tooltip"
|
:bold="true"
|
||||||
theme="info"
|
type="tooltip"
|
||||||
tooltip-placement="top"
|
theme="info"
|
||||||
>
|
tooltip-placement="top"
|
||||||
{{ tooltip }}
|
:enterable="false"
|
||||||
</N8nInfoTip>
|
>
|
||||||
</span>
|
{{ tooltip }}
|
||||||
</h3>
|
</N8nInfoTip>
|
||||||
<span v-if="renderIssues" :class="$style.warningIcon">
|
</div>
|
||||||
<N8nInfoTip :bold="true" type="tooltip" theme="warning" tooltip-placement="right">
|
<div :class="$style.actions">
|
||||||
|
<N8nInfoTip
|
||||||
|
v-if="renderIssues"
|
||||||
|
:bold="true"
|
||||||
|
type="tooltip"
|
||||||
|
theme="warning"
|
||||||
|
tooltip-placement="top"
|
||||||
|
:enterable="false"
|
||||||
|
>
|
||||||
{{ issuesList }}
|
{{ issuesList }}
|
||||||
</N8nInfoTip>
|
</N8nInfoTip>
|
||||||
</span>
|
<N8nText
|
||||||
<button
|
v-if="$slots.cardContent"
|
||||||
v-if="$slots.cardContent"
|
data-test-id="evaluation-step-collapse-button"
|
||||||
:class="$style.collapseButton"
|
size="xsmall"
|
||||||
:aria-expanded="isExpanded"
|
:color="hasIssues ? 'primary' : 'text-base'"
|
||||||
data-test-id="evaluation-step-collapse-button"
|
bold
|
||||||
>
|
|
||||||
{{
|
|
||||||
isExpanded
|
|
||||||
? locale.baseText('testDefinition.edit.step.collapse')
|
|
||||||
: locale.baseText('testDefinition.edit.step.configure')
|
|
||||||
}}
|
|
||||||
<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"
|
|
||||||
data-test-id="evaluation-step-content"
|
|
||||||
>
|
>
|
||||||
<div v-if="description" :class="$style.description">{{ description }}</div>
|
{{
|
||||||
<slot name="cardContent" />
|
isExpanded
|
||||||
</div>
|
? locale.baseText('testDefinition.edit.step.collapse')
|
||||||
|
: locale.baseText('testDefinition.edit.step.configure')
|
||||||
|
}}
|
||||||
|
<font-awesome-icon :icon="isExpanded ? 'angle-up' : 'angle-down'" size="lg" />
|
||||||
|
</N8nText>
|
||||||
</div>
|
</div>
|
||||||
</ElCollapseTransition>
|
</div>
|
||||||
|
</N8nTooltip>
|
||||||
|
<div v-if="$slots.cardContent && isExpanded" :class="$style.cardContentWrapper">
|
||||||
|
<div :class="$style.cardContent" data-test-id="evaluation-step-content">
|
||||||
|
<N8nText v-if="description" size="small" color="text-light">{{ description }}</N8nText>
|
||||||
|
<slot name="cardContent" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.wrap {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.evaluationStep {
|
.evaluationStep {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -140,11 +145,18 @@ const issuesList = computed(() => props.issues.map((issue) => issue.message).joi
|
|||||||
color: var(--color-text-dark);
|
color: var(--color-text-dark);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
&.small {
|
}
|
||||||
width: 80%;
|
|
||||||
margin-left: auto;
|
.evaluationTooltip {
|
||||||
|
&:global(.el-popper) {
|
||||||
|
background-color: transparent;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--color-text-light);
|
||||||
|
line-height: 1rem;
|
||||||
|
max-width: 25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -171,24 +183,23 @@ const issuesList = computed(() => props.issues.map((issue) => issue.message).joi
|
|||||||
padding: var(--spacing-s);
|
padding: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
line-height: 1.125rem;
|
|
||||||
}
|
|
||||||
.label {
|
.label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-4xs);
|
gap: var(--spacing-4xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoTip {
|
.infoTip {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
.evaluationStep:hover .infoTip {
|
.evaluationStep:hover .infoTip {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.warningIcon {
|
|
||||||
color: var(--color-warning);
|
.actions {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-2xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardContent {
|
.cardContent {
|
||||||
@@ -196,43 +207,15 @@ const issuesList = computed(() => props.issues.map((issue) => issue.message).joi
|
|||||||
padding: 0 var(--spacing-s);
|
padding: 0 var(--spacing-s);
|
||||||
margin: var(--spacing-s) 0;
|
margin: var(--spacing-s) 0;
|
||||||
}
|
}
|
||||||
.collapseButton {
|
|
||||||
pointer-events: none;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
font-size: var(--font-size-3xs);
|
|
||||||
color: var(--color-text-base);
|
|
||||||
margin-left: auto;
|
|
||||||
text-wrap: none;
|
|
||||||
overflow: hidden;
|
|
||||||
min-width: fit-content;
|
|
||||||
|
|
||||||
.hasIssues & {
|
|
||||||
color: var(--color-danger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.cardContentWrapper {
|
.cardContentWrapper {
|
||||||
height: max-content;
|
border-top: var(--border-base);
|
||||||
.expanded & {
|
|
||||||
border-top: var(--border-base);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.has-issues {
|
||||||
font-size: var(--font-size-2xs);
|
/**
|
||||||
color: var(--color-text-light);
|
* This comment is needed or the css module
|
||||||
line-height: 1rem;
|
* will interpret as undefined
|
||||||
}
|
*/
|
||||||
|
|
||||||
.customTooltip {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
background: var(--color-background-dark);
|
|
||||||
color: var(--color-text-light);
|
|
||||||
padding: var(--spacing-3xs) var(--spacing-2xs);
|
|
||||||
border-radius: var(--border-radius-base);
|
|
||||||
font-size: var(--font-size-2xs);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useTemplateRef, nextTick } from 'vue';
|
import { useTemplateRef, nextTick } from 'vue';
|
||||||
import type { TestMetricRecord } from '@/api/testDefinition.ee';
|
import type { TestMetricRecord } from '@/api/testDefinition.ee';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { N8nInput } from 'n8n-design-system';
|
import { N8nInput, N8nButton, N8nIconButton } from 'n8n-design-system';
|
||||||
|
|
||||||
export interface MetricsInputProps {
|
export interface MetricsInputProps {
|
||||||
modelValue: Array<Partial<TestMetricRecord>>;
|
modelValue: Array<Partial<TestMetricRecord>>;
|
||||||
@@ -38,62 +38,33 @@ function onDeleteMetric(metric: Partial<TestMetricRecord>, index: number) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.metrics]">
|
<div>
|
||||||
<n8n-input-label
|
<div
|
||||||
:label="locale.baseText('testDefinition.edit.metricsFields')"
|
v-for="(metric, index) in modelValue"
|
||||||
:bold="false"
|
:key="index"
|
||||||
:class="$style.metricField"
|
:class="$style.metricItem"
|
||||||
|
class="mb-xs"
|
||||||
>
|
>
|
||||||
<div :class="$style.metricsContainer">
|
<N8nInput
|
||||||
<div v-for="(metric, index) in modelValue" :key="index" :class="$style.metricItem">
|
ref="metric"
|
||||||
<N8nInput
|
data-test-id="evaluation-metric-item"
|
||||||
ref="metric"
|
:model-value="metric.name"
|
||||||
data-test-id="evaluation-metric-item"
|
:placeholder="locale.baseText('testDefinition.edit.metricsPlaceholder')"
|
||||||
:model-value="metric.name"
|
@update:model-value="(value: string) => updateMetric(index, value)"
|
||||||
:placeholder="locale.baseText('testDefinition.edit.metricsPlaceholder')"
|
/>
|
||||||
@update:model-value="(value: string) => updateMetric(index, value)"
|
<N8nIconButton icon="trash" type="secondary" text @click="onDeleteMetric(metric, index)" />
|
||||||
/>
|
</div>
|
||||||
<n8n-icon-button icon="trash" type="text" @click="onDeleteMetric(metric, index)" />
|
<N8nButton
|
||||||
</div>
|
type="secondary"
|
||||||
<n8n-button
|
:label="locale.baseText('testDefinition.edit.metricsNew')"
|
||||||
type="tertiary"
|
@click="addNewMetric"
|
||||||
:label="locale.baseText('testDefinition.edit.metricsNew')"
|
/>
|
||||||
:class="$style.newMetricButton"
|
|
||||||
@click="addNewMetric"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</n8n-input-label>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.metricsContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metricItem {
|
.metricItem {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metricField {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metricsDivider {
|
|
||||||
margin-top: var(--spacing-4xs);
|
|
||||||
margin-bottom: var(--spacing-3xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.newMetricButton {
|
|
||||||
align-self: flex-start;
|
|
||||||
margin-top: var(--spacing-2xs);
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--color-sticky-code-background);
|
|
||||||
border-color: var(--color-button-secondary-focus-outline);
|
|
||||||
color: var(--color-button-secondary-font);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import { useI18n } from '@/composables/useI18n';
|
|||||||
import { useMessage } from '@/composables/useMessage';
|
import { useMessage } from '@/composables/useMessage';
|
||||||
import { NODE_PINNING_MODAL_KEY } from '@/constants';
|
import { NODE_PINNING_MODAL_KEY } from '@/constants';
|
||||||
import type { ITag, ModalState } from '@/Interface';
|
import type { ITag, ModalState } from '@/Interface';
|
||||||
|
import { N8nButton, N8nTag, N8nText } from 'n8n-design-system';
|
||||||
import type { IPinData } from 'n8n-workflow';
|
import type { IPinData } from 'n8n-workflow';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
showConfig: boolean;
|
|
||||||
tagsById: Record<string, ITag>;
|
tagsById: Record<string, ITag>;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
examplePinnedData?: IPinData;
|
examplePinnedData?: IPinData;
|
||||||
@@ -33,14 +33,6 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
const activeTooltip = ref<string | null>(null);
|
|
||||||
const tooltipPosition = ref<{
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
right: number;
|
|
||||||
} | null>(null);
|
|
||||||
const tags = defineModel<EvaluationFormState['tags']>('tags', { required: true });
|
const tags = defineModel<EvaluationFormState['tags']>('tags', { required: true });
|
||||||
|
|
||||||
const renameTag = async () => {
|
const renameTag = async () => {
|
||||||
@@ -78,55 +70,29 @@ const mockedNodes = defineModel<EvaluationFormState['mockedNodes']>('mockedNodes
|
|||||||
|
|
||||||
const nodePinningModal = ref<ModalState | null>(null);
|
const nodePinningModal = ref<ModalState | null>(null);
|
||||||
|
|
||||||
const selectedTag = computed(() => {
|
const selectedTag = computed(() => props.tagsById[tags.value.value[0]] ?? {});
|
||||||
return props.tagsById[tags.value.value[0]] ?? {};
|
|
||||||
});
|
|
||||||
|
|
||||||
function openExecutionsView() {
|
function openExecutionsView() {
|
||||||
emit('openExecutionsViewForTag');
|
emit('openExecutionsViewForTag');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showTooltip(event: MouseEvent, tooltip: string) {
|
|
||||||
const container = event.target as HTMLElement;
|
|
||||||
const containerRect = container.getBoundingClientRect();
|
|
||||||
|
|
||||||
activeTooltip.value = tooltip;
|
|
||||||
tooltipPosition.value = {
|
|
||||||
x: containerRect.right,
|
|
||||||
y: containerRect.top,
|
|
||||||
width: containerRect.width,
|
|
||||||
height: containerRect.height,
|
|
||||||
right: window.innerWidth,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideTooltip() {
|
|
||||||
activeTooltip.value = null;
|
|
||||||
tooltipPosition.value = null;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.container, { [$style.hidden]: !showConfig }]">
|
<div>
|
||||||
<div :class="$style.editForm">
|
<div :class="$style.editForm">
|
||||||
<div :class="$style.panelIntro">
|
<template v-if="!hasRuns">
|
||||||
{{ locale.baseText('testDefinition.edit.step.intro') }}
|
<N8nText tag="div" color="text-dark" size="large" class="text-center">
|
||||||
</div>
|
{{ locale.baseText('testDefinition.edit.step.intro') }}
|
||||||
|
</N8nText>
|
||||||
|
<BlockArrow class="mt-5xs mb-5xs" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Select Executions -->
|
<!-- Select Executions -->
|
||||||
<EvaluationStep
|
<EvaluationStep
|
||||||
:class="[$style.step, $style.reducedSpacing]"
|
|
||||||
:issues="getFieldIssues('tags')"
|
:issues="getFieldIssues('tags')"
|
||||||
:tooltip="
|
:tooltip="locale.baseText('testDefinition.edit.step.executions.tooltip')"
|
||||||
hasRuns ? locale.baseText('testDefinition.edit.step.executions.tooltip') : undefined
|
:external-tooltip="!hasRuns"
|
||||||
"
|
|
||||||
@mouseenter="
|
|
||||||
showTooltip($event, locale.baseText('testDefinition.edit.step.executions.tooltip'))
|
|
||||||
"
|
|
||||||
@mouseleave="hideTooltip"
|
|
||||||
>
|
>
|
||||||
<template #containerPrefix>
|
|
||||||
<BlockArrow :class="[$style.middle, $style.diagramArrow, $style.sm]" />
|
|
||||||
</template>
|
|
||||||
<template #title>
|
<template #title>
|
||||||
{{
|
{{
|
||||||
locale.baseText('testDefinition.edit.step.executions', {
|
locale.baseText('testDefinition.edit.step.executions', {
|
||||||
@@ -135,95 +101,73 @@ function hideTooltip() {
|
|||||||
}}
|
}}
|
||||||
</template>
|
</template>
|
||||||
<template #cardContent>
|
<template #cardContent>
|
||||||
<div :class="$style.tagInputContainer">
|
<div :class="$style.tagInputTag">
|
||||||
<div :class="$style.tagInputTag">
|
<i18n-t keypath="testDefinition.edit.step.tag">
|
||||||
<i18n-t keypath="testDefinition.edit.step.tag">
|
<template #tag>
|
||||||
<template #tag>
|
<N8nTag :text="selectedTag.name" :clickable="true" @click="renameTag">
|
||||||
<N8nTag :text="selectedTag.name" :clickable="true" @click="renameTag">
|
<template #tag>
|
||||||
<template #tag>
|
{{ selectedTag.name }} <font-awesome-icon icon="pen" size="sm" />
|
||||||
{{ selectedTag.name }} <font-awesome-icon icon="pen" size="sm" />
|
</template>
|
||||||
</template>
|
</N8nTag>
|
||||||
</N8nTag>
|
</template>
|
||||||
</template>
|
</i18n-t>
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
<div :class="$style.tagInputControls">
|
|
||||||
<n8n-button
|
|
||||||
label="Select executions"
|
|
||||||
type="tertiary"
|
|
||||||
size="small"
|
|
||||||
@click="openExecutionsView"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<N8nButton
|
||||||
</EvaluationStep>
|
label="Select executions"
|
||||||
<!-- Mocked Nodes -->
|
|
||||||
<EvaluationStep
|
|
||||||
:class="$style.step"
|
|
||||||
:title="
|
|
||||||
locale.baseText('testDefinition.edit.step.mockedNodes', {
|
|
||||||
adjustToNumber: mockedNodes?.length ?? 0,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
:small="true"
|
|
||||||
:issues="getFieldIssues('mockedNodes')"
|
|
||||||
:tooltip="hasRuns ? locale.baseText('testDefinition.edit.step.nodes.tooltip') : undefined"
|
|
||||||
@mouseenter="showTooltip($event, locale.baseText('testDefinition.edit.step.nodes.tooltip'))"
|
|
||||||
@mouseleave="hideTooltip"
|
|
||||||
>
|
|
||||||
<template #containerPrefix>
|
|
||||||
<BlockArrow :class="[$style.diagramArrow, $style.right]" />
|
|
||||||
</template>
|
|
||||||
<template #cardContent>
|
|
||||||
<n8n-button
|
|
||||||
size="small"
|
|
||||||
data-test-id="select-nodes-button"
|
|
||||||
:label="locale.baseText('testDefinition.edit.selectNodes')"
|
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
@click="$emit('openPinningModal')"
|
size="small"
|
||||||
|
@click="openExecutionsView"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</EvaluationStep>
|
</EvaluationStep>
|
||||||
|
<div :class="$style.nestedSteps">
|
||||||
|
<BlockArrow class="mt-5xs mb-5xs" />
|
||||||
|
<div style="display: flex; flex-direction: column">
|
||||||
|
<BlockArrow class="mt-5xs mb-5xs ml-auto mr-2xl" />
|
||||||
|
<!-- Mocked Nodes -->
|
||||||
|
<EvaluationStep
|
||||||
|
:issues="getFieldIssues('mockedNodes')"
|
||||||
|
:tooltip="locale.baseText('testDefinition.edit.step.nodes.tooltip')"
|
||||||
|
:external-tooltip="!hasRuns"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
{{
|
||||||
|
locale.baseText('testDefinition.edit.step.mockedNodes', {
|
||||||
|
adjustToNumber: mockedNodes?.length ?? 0,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
<N8nText>({{ locale.baseText('generic.optional') }})</N8nText>
|
||||||
|
</template>
|
||||||
|
<template #cardContent>
|
||||||
|
<N8nButton
|
||||||
|
size="small"
|
||||||
|
data-test-id="select-nodes-button"
|
||||||
|
:label="locale.baseText('testDefinition.edit.selectNodes')"
|
||||||
|
type="tertiary"
|
||||||
|
@click="$emit('openPinningModal')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</EvaluationStep>
|
||||||
|
|
||||||
<!-- Re-run Executions -->
|
<BlockArrow class="mt-5xs mb-5xs ml-auto mr-2xl" />
|
||||||
<EvaluationStep
|
<!-- Re-run Executions -->
|
||||||
:class="$style.step"
|
<EvaluationStep
|
||||||
:title="locale.baseText('testDefinition.edit.step.reRunExecutions')"
|
:title="locale.baseText('testDefinition.edit.step.reRunExecutions')"
|
||||||
:small="true"
|
:tooltip="locale.baseText('testDefinition.edit.step.reRunExecutions.tooltip')"
|
||||||
:tooltip="
|
:external-tooltip="!hasRuns"
|
||||||
hasRuns ? locale.baseText('testDefinition.edit.step.reRunExecutions.tooltip') : undefined
|
/>
|
||||||
"
|
<BlockArrow class="mt-5xs mb-5xs ml-auto mr-2xl" />
|
||||||
@mouseenter="
|
</div>
|
||||||
showTooltip($event, locale.baseText('testDefinition.edit.step.reRunExecutions.tooltip'))
|
</div>
|
||||||
"
|
|
||||||
@mouseleave="hideTooltip"
|
|
||||||
>
|
|
||||||
<template #containerPrefix>
|
|
||||||
<BlockArrow :class="[$style.right, $style.diagramArrow]" />
|
|
||||||
</template>
|
|
||||||
</EvaluationStep>
|
|
||||||
|
|
||||||
<!-- Compare Executions -->
|
<!-- Compare Executions -->
|
||||||
<EvaluationStep
|
<EvaluationStep
|
||||||
:class="$style.step"
|
|
||||||
:title="locale.baseText('testDefinition.edit.step.compareExecutions')"
|
:title="locale.baseText('testDefinition.edit.step.compareExecutions')"
|
||||||
:description="locale.baseText('testDefinition.edit.workflowSelectorLabel')"
|
:description="locale.baseText('testDefinition.edit.workflowSelectorLabel')"
|
||||||
:issues="getFieldIssues('evaluationWorkflow')"
|
:issues="getFieldIssues('evaluationWorkflow')"
|
||||||
:tooltip="
|
:tooltip="locale.baseText('testDefinition.edit.step.compareExecutions.tooltip')"
|
||||||
hasRuns
|
:external-tooltip="!hasRuns"
|
||||||
? locale.baseText('testDefinition.edit.step.compareExecutions.tooltip')
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
@mouseenter="
|
|
||||||
showTooltip($event, locale.baseText('testDefinition.edit.step.compareExecutions.tooltip'))
|
|
||||||
"
|
|
||||||
@mouseleave="hideTooltip"
|
|
||||||
>
|
>
|
||||||
<template #containerPrefix>
|
|
||||||
<BlockArrow :class="[$style.right, $style.diagramArrow]" />
|
|
||||||
<BlockArrow :class="[$style.left, $style.diagramArrow, $style.lg]" />
|
|
||||||
</template>
|
|
||||||
<template #cardContent>
|
<template #cardContent>
|
||||||
<WorkflowSelector
|
<WorkflowSelector
|
||||||
v-model="evaluationWorkflow"
|
v-model="evaluationWorkflow"
|
||||||
@@ -235,25 +179,20 @@ function hideTooltip() {
|
|||||||
</template>
|
</template>
|
||||||
</EvaluationStep>
|
</EvaluationStep>
|
||||||
|
|
||||||
|
<BlockArrow class="mt-5xs mb-5xs" />
|
||||||
<!-- Metrics -->
|
<!-- Metrics -->
|
||||||
<EvaluationStep
|
<EvaluationStep
|
||||||
:class="$style.step"
|
|
||||||
:title="locale.baseText('testDefinition.edit.step.metrics')"
|
:title="locale.baseText('testDefinition.edit.step.metrics')"
|
||||||
:issues="getFieldIssues('metrics')"
|
:issues="getFieldIssues('metrics')"
|
||||||
:description="locale.baseText('testDefinition.edit.step.metrics.description')"
|
:description="locale.baseText('testDefinition.edit.step.metrics.description')"
|
||||||
:tooltip="hasRuns ? locale.baseText('testDefinition.edit.step.metrics.tooltip') : undefined"
|
:tooltip="locale.baseText('testDefinition.edit.step.metrics.tooltip')"
|
||||||
@mouseenter="
|
:external-tooltip="!hasRuns"
|
||||||
showTooltip($event, locale.baseText('testDefinition.edit.step.metrics.tooltip'))
|
|
||||||
"
|
|
||||||
@mouseleave="hideTooltip"
|
|
||||||
>
|
>
|
||||||
<template #containerPrefix>
|
|
||||||
<BlockArrow :class="[$style.middle, $style.diagramArrow]" />
|
|
||||||
</template>
|
|
||||||
<template #cardContent>
|
<template #cardContent>
|
||||||
<MetricsInput
|
<MetricsInput
|
||||||
v-model="metrics"
|
v-model="metrics"
|
||||||
:class="{ 'has-issues': getFieldIssues('metrics').length > 0 }"
|
:class="{ 'has-issues': getFieldIssues('metrics').length > 0 }"
|
||||||
|
class="mt-xs"
|
||||||
@delete-metric="(metric) => emit('deleteMetric', metric)"
|
@delete-metric="(metric) => emit('deleteMetric', metric)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -261,115 +200,25 @@ function hideTooltip() {
|
|||||||
</div>
|
</div>
|
||||||
<Modal ref="nodePinningModal" width="80vw" height="85vh" :name="NODE_PINNING_MODAL_KEY">
|
<Modal ref="nodePinningModal" width="80vw" height="85vh" :name="NODE_PINNING_MODAL_KEY">
|
||||||
<template #header>
|
<template #header>
|
||||||
<N8nHeading size="large" :bold="true">{{
|
<N8nHeading size="large" :bold="true">
|
||||||
locale.baseText('testDefinition.edit.selectNodes')
|
{{ locale.baseText('testDefinition.edit.selectNodes') }}
|
||||||
}}</N8nHeading>
|
</N8nHeading>
|
||||||
<br />
|
<br />
|
||||||
<N8nText :class="$style.modalDescription">{{
|
<N8nText>
|
||||||
locale.baseText('testDefinition.edit.modal.description')
|
{{ locale.baseText('testDefinition.edit.modal.description') }}
|
||||||
}}</N8nText>
|
</N8nText>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<NodesPinning v-model="mockedNodes" data-test-id="nodes-pinning-modal" />
|
<NodesPinning v-model="mockedNodes" data-test-id="nodes-pinning-modal" />
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="tooltipPosition && !hasRuns"
|
|
||||||
:class="$style.customTooltip"
|
|
||||||
:style="{
|
|
||||||
left: `${tooltipPosition.x}px`,
|
|
||||||
top: `${tooltipPosition.y}px`,
|
|
||||||
width: `${tooltipPosition.right - tooltipPosition.x}px`,
|
|
||||||
height: `${tooltipPosition.height}px`,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ activeTooltip }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.container {
|
.nestedSteps {
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: visible;
|
|
||||||
width: auto;
|
|
||||||
margin-left: var(--spacing-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.editForm {
|
|
||||||
width: var(--evaluation-edit-panel-width);
|
|
||||||
display: grid;
|
display: grid;
|
||||||
height: fit-content;
|
grid-template-columns: 20% 1fr;
|
||||||
flex-shrink: 0;
|
|
||||||
padding-bottom: var(--spacing-l);
|
|
||||||
transition: width 0.2s ease;
|
|
||||||
position: relative;
|
|
||||||
gap: var(--spacing-l);
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
margin-left: 0;
|
|
||||||
width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.noRuns & {
|
|
||||||
overflow-y: initial;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.customTooltip {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1000;
|
|
||||||
padding: var(--spacing-xs);
|
|
||||||
max-width: 25rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
color: var(--color-text-light);
|
|
||||||
line-height: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panelIntro {
|
|
||||||
font-size: var(--font-size-m);
|
|
||||||
color: var(--color-text-dark);
|
|
||||||
|
|
||||||
justify-self: center;
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagramArrow {
|
|
||||||
--arrow-height: 4rem;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 100%;
|
|
||||||
left: var(--spacing-2xl);
|
|
||||||
z-index: 0;
|
|
||||||
// increase hover radius of the arrow
|
|
||||||
&.right {
|
|
||||||
left: unset;
|
|
||||||
right: var(--spacing-2xl);
|
|
||||||
}
|
|
||||||
&.middle {
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sm {
|
|
||||||
--arrow-height: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.lg {
|
|
||||||
--arrow-height: 14rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tagInputContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-xs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tagInputTag {
|
.tagInputTag {
|
||||||
@@ -377,9 +226,6 @@ function hideTooltip() {
|
|||||||
gap: var(--spacing-3xs);
|
gap: var(--spacing-3xs);
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
}
|
margin-bottom: var(--spacing-xs);
|
||||||
.tagInputControls {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-2xs);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ const statusRender = computed<IconDefinition & { label: string }>(() => {
|
|||||||
{{ key }}
|
{{ key }}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
<N8nText color="text-base" size="small" bold>
|
<N8nText color="text-base" size="small" bold>
|
||||||
{{ value }}
|
{{ Math.round((value + Number.EPSILON) * 100) / 100 }}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ describe('MetricsInput', () => {
|
|||||||
|
|
||||||
it('should render correctly with initial metrics', () => {
|
it('should render correctly with initial metrics', () => {
|
||||||
const { getAllByPlaceholderText } = renderComponent({ props });
|
const { getAllByPlaceholderText } = renderComponent({ props });
|
||||||
const inputs = getAllByPlaceholderText('Enter metric name');
|
const inputs = getAllByPlaceholderText('e.g. latency');
|
||||||
expect(inputs).toHaveLength(2);
|
expect(inputs).toHaveLength(2);
|
||||||
expect(inputs[0]).toHaveValue('Metric 1');
|
expect(inputs[0]).toHaveValue('Metric 1');
|
||||||
expect(inputs[1]).toHaveValue('Metric 2');
|
expect(inputs[1]).toHaveValue('Metric 2');
|
||||||
@@ -31,7 +31,7 @@ describe('MetricsInput', () => {
|
|||||||
modelValue: [{ name: '' }],
|
modelValue: [{ name: '' }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const inputs = getAllByPlaceholderText('Enter metric name');
|
const inputs = getAllByPlaceholderText('e.g. latency');
|
||||||
await userEvent.type(inputs[0], 'Updated Metric 1');
|
await userEvent.type(inputs[0], 'Updated Metric 1');
|
||||||
|
|
||||||
// Every character typed triggers an update event. Let's check the last emission.
|
// Every character typed triggers an update event. Let's check the last emission.
|
||||||
@@ -88,7 +88,7 @@ describe('MetricsInput', () => {
|
|||||||
modelValue: [{ name: '' }],
|
modelValue: [{ name: '' }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const inputs = getAllByPlaceholderText('Enter metric name');
|
const inputs = getAllByPlaceholderText('e.g. latency');
|
||||||
await userEvent.type(inputs[0], 'ABC');
|
await userEvent.type(inputs[0], 'ABC');
|
||||||
|
|
||||||
const allEmits = emitted('update:modelValue');
|
const allEmits = emitted('update:modelValue');
|
||||||
@@ -117,7 +117,7 @@ describe('MetricsInput', () => {
|
|||||||
const { getAllByPlaceholderText } = renderComponent({
|
const { getAllByPlaceholderText } = renderComponent({
|
||||||
props: { modelValue: [{ name: '' }] },
|
props: { modelValue: [{ name: '' }] },
|
||||||
});
|
});
|
||||||
const updatedInputs = getAllByPlaceholderText('Enter metric name');
|
const updatedInputs = getAllByPlaceholderText('e.g. latency');
|
||||||
expect(updatedInputs).toHaveLength(1);
|
expect(updatedInputs).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ describe('MetricsInput', () => {
|
|||||||
const { getAllByPlaceholderText, getAllByRole, rerender, emitted } = renderComponent({
|
const { getAllByPlaceholderText, getAllByRole, rerender, emitted } = renderComponent({
|
||||||
props,
|
props,
|
||||||
});
|
});
|
||||||
const inputs = getAllByPlaceholderText('Enter metric name');
|
const inputs = getAllByPlaceholderText('e.g. latency');
|
||||||
expect(inputs).toHaveLength(2);
|
expect(inputs).toHaveLength(2);
|
||||||
|
|
||||||
const deleteButtons = getAllByRole('button', { name: '' });
|
const deleteButtons = getAllByRole('button', { name: '' });
|
||||||
@@ -135,7 +135,7 @@ describe('MetricsInput', () => {
|
|||||||
expect(emitted('deleteMetric')[0]).toEqual([props.modelValue[0]]);
|
expect(emitted('deleteMetric')[0]).toEqual([props.modelValue[0]]);
|
||||||
|
|
||||||
await rerender({ modelValue: [{ name: 'Metric 2' }] });
|
await rerender({ modelValue: [{ name: 'Metric 2' }] });
|
||||||
const updatedInputs = getAllByPlaceholderText('Enter metric name');
|
const updatedInputs = getAllByPlaceholderText('e.g. latency');
|
||||||
expect(updatedInputs).toHaveLength(1);
|
expect(updatedInputs).toHaveLength(1);
|
||||||
expect(updatedInputs[0]).toHaveValue('Metric 2');
|
expect(updatedInputs[0]).toHaveValue('Metric 2');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"generic.tag_plural": "Tags",
|
"generic.tag_plural": "Tags",
|
||||||
"generic.tag": "Tag | {count} Tags",
|
"generic.tag": "Tag | {count} Tags",
|
||||||
"generic.tests": "Tests",
|
"generic.tests": "Tests",
|
||||||
|
"generic.optional": "optional",
|
||||||
"generic.or": "or",
|
"generic.or": "or",
|
||||||
"generic.clickToCopy": "Click to copy",
|
"generic.clickToCopy": "Click to copy",
|
||||||
"generic.copiedToClipboard": "Copied to clipboard",
|
"generic.copiedToClipboard": "Copied to clipboard",
|
||||||
@@ -2819,7 +2820,7 @@
|
|||||||
"testDefinition.edit.metricsTitle": "Metrics",
|
"testDefinition.edit.metricsTitle": "Metrics",
|
||||||
"testDefinition.edit.metricsHelpText": "The output field of the last node in the evaluation workflow. Metrics will be averaged across all test cases.",
|
"testDefinition.edit.metricsHelpText": "The output field of the last node in the evaluation workflow. Metrics will be averaged across all test cases.",
|
||||||
"testDefinition.edit.metricsFields": "Output fields to use as metrics",
|
"testDefinition.edit.metricsFields": "Output fields to use as metrics",
|
||||||
"testDefinition.edit.metricsPlaceholder": "Enter metric name",
|
"testDefinition.edit.metricsPlaceholder": "e.g. latency",
|
||||||
"testDefinition.edit.metricsNew": "New metric",
|
"testDefinition.edit.metricsNew": "New metric",
|
||||||
"testDefinition.edit.selectTag": "Select tag...",
|
"testDefinition.edit.selectTag": "Select tag...",
|
||||||
"testDefinition.edit.tagsHelpText": "Executions with this tag will be added as test cases to this test.",
|
"testDefinition.edit.tagsHelpText": "Executions with this tag will be added as test cases to this test.",
|
||||||
@@ -2862,7 +2863,7 @@
|
|||||||
"testDefinition.edit.pastRuns.total": "No runs | Past run ({count}) | Past runs ({count})",
|
"testDefinition.edit.pastRuns.total": "No runs | Past run ({count}) | Past runs ({count})",
|
||||||
"testDefinition.edit.nodesPinning.pinButtonTooltip": "Use benchmark data for this node during evaluation execution",
|
"testDefinition.edit.nodesPinning.pinButtonTooltip": "Use benchmark data for this node during evaluation execution",
|
||||||
"testDefinition.edit.saving": "Saving...",
|
"testDefinition.edit.saving": "Saving...",
|
||||||
"testDefinition.edit.saved": "Changes saved",
|
"testDefinition.edit.saved": "Test saved",
|
||||||
"testDefinition.list.testDeleted": "Test deleted",
|
"testDefinition.list.testDeleted": "Test deleted",
|
||||||
"testDefinition.list.tests": "Tests",
|
"testDefinition.list.tests": "Tests",
|
||||||
"testDefinition.list.evaluations": "Evaluation",
|
"testDefinition.list.evaluations": "Evaluation",
|
||||||
|
|||||||
@@ -173,13 +173,13 @@ function onEvaluationWorkflowCreated(workflowId: string) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!isLoading" :class="[$style.container, { [$style.noRuns]: !hasRuns }]">
|
<div v-if="!isLoading" :class="[$style.container]">
|
||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<div style="display: flex; align-items: center">
|
<div style="display: flex; align-items: center">
|
||||||
<N8nIconButton
|
<N8nIconButton
|
||||||
icon="arrow-left"
|
icon="arrow-left"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
:class="$style.arrowBack"
|
text
|
||||||
@click="router.push({ name: VIEWS.TEST_DEFINITION, params: { testId } })"
|
@click="router.push({ name: VIEWS.TEST_DEFINITION, params: { testId } })"
|
||||||
></N8nIconButton>
|
></N8nIconButton>
|
||||||
<InlineNameEdit
|
<InlineNameEdit
|
||||||
@@ -232,17 +232,17 @@ function onEvaluationWorkflowCreated(workflowId: string) {
|
|||||||
:maxlength="260"
|
:maxlength="260"
|
||||||
max-height="none"
|
max-height="none"
|
||||||
type="Test description"
|
type="Test description"
|
||||||
:class="$style.editDescription"
|
|
||||||
@update:model-value="updateDescription"
|
@update:model-value="updateDescription"
|
||||||
>
|
>
|
||||||
<N8nText size="small" color="text-base">{{ state.description.value }}</N8nText>
|
<N8nText size="medium" color="text-base">{{ state.description.value }}</N8nText>
|
||||||
</InlineNameEdit>
|
</InlineNameEdit>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="$style.content">
|
<div :class="{ [$style.content]: true, [$style.contentWithRuns]: hasRuns }">
|
||||||
<RunsSection
|
<RunsSection
|
||||||
v-if="runs.length > 0"
|
v-if="hasRuns"
|
||||||
v-model:selectedMetric="selectedMetric"
|
v-model:selectedMetric="selectedMetric"
|
||||||
|
:class="$style.runs"
|
||||||
:runs="runs"
|
:runs="runs"
|
||||||
:test-id="testId"
|
:test-id="testId"
|
||||||
:applied-theme="appliedTheme"
|
:applied-theme="appliedTheme"
|
||||||
@@ -250,12 +250,13 @@ function onEvaluationWorkflowCreated(workflowId: string) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfigSection
|
<ConfigSection
|
||||||
|
v-if="showConfig"
|
||||||
v-model:tags="state.tags"
|
v-model:tags="state.tags"
|
||||||
v-model:evaluationWorkflow="state.evaluationWorkflow"
|
v-model:evaluationWorkflow="state.evaluationWorkflow"
|
||||||
v-model:metrics="state.metrics"
|
v-model:metrics="state.metrics"
|
||||||
v-model:mockedNodes="state.mockedNodes"
|
v-model:mockedNodes="state.mockedNodes"
|
||||||
|
:class="$style.config"
|
||||||
:cancel-editing="cancelEditing"
|
:cancel-editing="cancelEditing"
|
||||||
:show-config="showConfig"
|
|
||||||
:tags-by-id="tagsById"
|
:tags-by-id="tagsById"
|
||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
:get-field-issues="getFieldIssues"
|
:get-field-issues="getFieldIssues"
|
||||||
@@ -282,8 +283,16 @@ function onEvaluationWorkflowCreated(workflowId: string) {
|
|||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
|
padding-bottom: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config {
|
||||||
|
width: 480px;
|
||||||
|
|
||||||
|
.contentWithRuns & {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
|||||||
@@ -29,19 +29,24 @@ const toast = useToast();
|
|||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
const { confirm } = useMessage();
|
const { confirm } = useMessage();
|
||||||
|
|
||||||
const { state: tests, isLoading } = useAsyncState(
|
const { isLoading } = useAsyncState(
|
||||||
async () => {
|
async () => {
|
||||||
await testDefinitionStore.fetchAll({ workflowId: props.name });
|
await testDefinitionStore.fetchAll({ workflowId: props.name });
|
||||||
|
|
||||||
const response = testDefinitionStore.allTestDefinitionsByWorkflowId[props.name] ?? [];
|
const response = testDefinitionStore.allTestDefinitionsByWorkflowId[props.name] ?? [];
|
||||||
response.forEach((test) => testDefinitionStore.updateRunFieldIssues(test.id));
|
response.forEach((test) => testDefinitionStore.updateRunFieldIssues(test.id));
|
||||||
|
|
||||||
return response;
|
return [];
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
{ onError: (error) => toast.showError(error, locale.baseText('testDefinition.list.loadError')) },
|
{
|
||||||
|
onError: (error) => toast.showError(error, locale.baseText('testDefinition.list.loadError')),
|
||||||
|
shallow: false,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tests = computed(() => testDefinitionStore.allTestDefinitionsByWorkflowId[props.name]);
|
||||||
|
|
||||||
const listItems = computed(() =>
|
const listItems = computed(() =>
|
||||||
orderBy(tests.value, [(test) => new Date(test.updatedAt ?? test.createdAt)], ['desc']).map(
|
orderBy(tests.value, [(test) => new Date(test.updatedAt ?? test.createdAt)], ['desc']).map(
|
||||||
(test) => ({
|
(test) => ({
|
||||||
|
|||||||
@@ -134,11 +134,10 @@ describe('TestDefinitionListView', () => {
|
|||||||
it('should delete test and show success message', async () => {
|
it('should delete test and show success message', async () => {
|
||||||
const testDefinitionStore = mockedStore(useTestDefinitionStore);
|
const testDefinitionStore = mockedStore(useTestDefinitionStore);
|
||||||
testDefinitionStore.allTestDefinitionsByWorkflowId[workflowId] = mockTestDefinitions;
|
testDefinitionStore.allTestDefinitionsByWorkflowId[workflowId] = mockTestDefinitions;
|
||||||
testDefinitionStore.startTestRun.mockRejectedValueOnce(new Error('Run failed'));
|
|
||||||
|
|
||||||
message.confirm.mockResolvedValueOnce(MODAL_CONFIRM);
|
message.confirm.mockResolvedValueOnce(MODAL_CONFIRM);
|
||||||
|
|
||||||
const { getByTestId } = renderComponent({
|
const { getByTestId, queryByTestId } = renderComponent({
|
||||||
props: { name: workflowId },
|
props: { name: workflowId },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,6 +156,17 @@ describe('TestDefinitionListView', () => {
|
|||||||
|
|
||||||
expect(testDefinitionStore.deleteById).toHaveBeenCalledWith(testToDelete);
|
expect(testDefinitionStore.deleteById).toHaveBeenCalledWith(testToDelete);
|
||||||
expect(toast.showMessage).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' }));
|
expect(toast.showMessage).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' }));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* since the actions are mocked by default,
|
||||||
|
* double check the UI updates correctly
|
||||||
|
*/
|
||||||
|
testDefinitionStore.allTestDefinitionsByWorkflowId = {
|
||||||
|
[workflowId]: [mockTestDefinitions[1], mockTestDefinitions[2]],
|
||||||
|
};
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(queryByTestId(`test-actions-${testToDelete}`)).not.toBeInTheDocument(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sort tests by updated date in descending order', async () => {
|
it('should sort tests by updated date in descending order', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user