mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-21 02:56:40 -08:00
feat(editor): Enhance workflow evaluations edit view (no-changelog) (#12586)
This commit is contained in:
parent
bdf266cf55
commit
ee68afe561
|
@ -1,40 +1,84 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import type { EditableField } from '../types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: string;
|
modelValue: EditableField<string>;
|
||||||
|
startEditing: (field: 'description') => void;
|
||||||
|
saveChanges: (field: 'description') => void;
|
||||||
|
handleKeydown: (e: KeyboardEvent, field: 'description') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
defineProps<Props>();
|
||||||
modelValue: '',
|
defineEmits<{ 'update:modelValue': [value: EditableField<string>] }>();
|
||||||
});
|
|
||||||
|
|
||||||
defineEmits<{ 'update:modelValue': [value: string] }>();
|
|
||||||
|
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.description]">
|
<div :class="$style.description">
|
||||||
<n8n-input-label
|
<template v-if="!modelValue.isEditing">
|
||||||
:label="locale.baseText('testDefinition.edit.description')"
|
<span :class="$style.descriptionText" @click="startEditing('description')">
|
||||||
:bold="false"
|
<n8n-icon
|
||||||
size="small"
|
v-if="modelValue.value.length === 0"
|
||||||
:class="$style.field"
|
:class="$style.icon"
|
||||||
>
|
icon="plus"
|
||||||
<N8nInput
|
color="text-light"
|
||||||
:model-value="modelValue"
|
size="medium"
|
||||||
type="textarea"
|
/>
|
||||||
:placeholder="locale.baseText('testDefinition.edit.descriptionPlaceholder')"
|
<N8nText size="medium">
|
||||||
@update:model-value="$emit('update:modelValue', $event)"
|
{{ modelValue.value.length > 0 ? modelValue.value : 'Add a description' }}
|
||||||
|
</N8nText>
|
||||||
|
</span>
|
||||||
|
<n8n-icon-button
|
||||||
|
:class="$style.editInputButton"
|
||||||
|
icon="pen"
|
||||||
|
type="tertiary"
|
||||||
|
@click="startEditing('description')"
|
||||||
/>
|
/>
|
||||||
</n8n-input-label>
|
</template>
|
||||||
|
<N8nInput
|
||||||
|
v-else
|
||||||
|
ref="descriptionInput"
|
||||||
|
data-test-id="evaluation-description-input"
|
||||||
|
:model-value="modelValue.tempValue"
|
||||||
|
type="textarea"
|
||||||
|
:placeholder="locale.baseText('testDefinition.edit.descriptionPlaceholder')"
|
||||||
|
@update:model-value="$emit('update:modelValue', { ...modelValue, tempValue: $event })"
|
||||||
|
@blur="() => saveChanges('description')"
|
||||||
|
@keydown="(e: KeyboardEvent) => handleKeydown(e, 'description')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.field {
|
.description {
|
||||||
width: 100%;
|
display: flex;
|
||||||
margin-top: var(--spacing-xs);
|
align-items: center;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.editInputButton {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionText {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editInputButton {
|
||||||
|
--button-font-color: var(--prim-gray-490);
|
||||||
|
opacity: 0;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,8 +18,8 @@ const locale = useI18n();
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<n8n-icon-button
|
<n8n-icon-button
|
||||||
icon="arrow-left"
|
|
||||||
:class="$style.backButton"
|
:class="$style.backButton"
|
||||||
|
icon="arrow-left"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
:title="locale.baseText('testDefinition.edit.backButtonTitle')"
|
:title="locale.baseText('testDefinition.edit.backButtonTitle')"
|
||||||
@click="$router.back()"
|
@click="$router.back()"
|
||||||
|
@ -55,8 +55,6 @@ const locale = useI18n();
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-2xs);
|
|
||||||
margin-bottom: var(--spacing-l);
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.editInputButton {
|
.editInputButton {
|
||||||
|
@ -86,12 +84,13 @@ const locale = useI18n();
|
||||||
|
|
||||||
.editInputButton {
|
.editInputButton {
|
||||||
--button-font-color: var(--prim-gray-490);
|
--button-font-color: var(--prim-gray-490);
|
||||||
opacity: 0;
|
opacity: 0.2;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backButton {
|
.backButton {
|
||||||
--button-font-color: var(--color-text-light);
|
--button-font-color: var(--color-text-light);
|
||||||
border: none;
|
border: none;
|
||||||
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -90,6 +90,7 @@ const toggleExpand = async () => {
|
||||||
|
|
||||||
&.small {
|
&.small {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.icon {
|
.icon {
|
||||||
|
|
|
@ -11,19 +11,28 @@ const locale = useI18n();
|
||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<h1>{{ locale.baseText('testDefinition.list.tests') }}</h1>
|
<h1>{{ locale.baseText('testDefinition.list.tests') }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<n8n-action-box
|
<div :class="$style.content">
|
||||||
:description="locale.baseText('testDefinition.list.actionDescription')"
|
<n8n-action-box
|
||||||
:button-text="locale.baseText('testDefinition.list.actionButton')"
|
:class="$style.actionBox"
|
||||||
@click:button="$emit('create-test')"
|
:heading="locale.baseText('testDefinition.list.evaluations')"
|
||||||
/>
|
:description="locale.baseText('testDefinition.list.actionDescription')"
|
||||||
|
:button-text="locale.baseText('testDefinition.list.actionButton')"
|
||||||
|
@click:button="$emit('create-test')"
|
||||||
|
/>
|
||||||
|
<n8n-action-box
|
||||||
|
:class="$style.actionBox"
|
||||||
|
:heading="locale.baseText('testDefinition.list.unitTests.title')"
|
||||||
|
:description="locale.baseText('testDefinition.list.unitTests.description')"
|
||||||
|
:button-text="locale.baseText('testDefinition.list.unitTests.cta')"
|
||||||
|
button-type="tertiary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.container {
|
.container {
|
||||||
max-width: 44rem;
|
max-width: 75rem;
|
||||||
margin: var(--spacing-4xl) auto 0;
|
|
||||||
gap: var(--spacing-l);
|
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -37,4 +46,12 @@ const locale = useI18n();
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.actionBox {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -45,11 +45,11 @@ watchEffect(() => {
|
||||||
<template>
|
<template>
|
||||||
<div v-if="availableMetrics.length > 0" :class="$style.metricsChartContainer">
|
<div v-if="availableMetrics.length > 0" :class="$style.metricsChartContainer">
|
||||||
<div :class="$style.chartHeader">
|
<div :class="$style.chartHeader">
|
||||||
<N8nText>{{ locale.baseText('testDefinition.listRuns.metricsOverTime') }}</N8nText>
|
|
||||||
<N8nSelect
|
<N8nSelect
|
||||||
:model-value="selectedMetric"
|
:model-value="selectedMetric"
|
||||||
:class="$style.metricSelect"
|
:class="$style.metricSelect"
|
||||||
placeholder="Select metric"
|
placeholder="Select metric"
|
||||||
|
size="small"
|
||||||
@update:model-value="emit('update:selectedMetric', $event)"
|
@update:model-value="emit('update:selectedMetric', $event)"
|
||||||
>
|
>
|
||||||
<N8nOption
|
<N8nOption
|
||||||
|
@ -59,6 +59,7 @@ watchEffect(() => {
|
||||||
:value="metric"
|
:value="metric"
|
||||||
/>
|
/>
|
||||||
</N8nSelect>
|
</N8nSelect>
|
||||||
|
<N8nText>{{ locale.baseText('testDefinition.listRuns.metricsOverTime') }}</N8nText>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.chartWrapper">
|
<div :class="$style.chartWrapper">
|
||||||
<Line
|
<Line
|
||||||
|
@ -74,17 +75,17 @@ watchEffect(() => {
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.metricsChartContainer {
|
.metricsChartContainer {
|
||||||
margin: var(--spacing-m) 0;
|
|
||||||
background: var(--color-background-xlight);
|
background: var(--color-background-xlight);
|
||||||
border-radius: var(--border-radius-large);
|
border-radius: var(--border-radius-large);
|
||||||
box-shadow: var(--box-shadow-base);
|
box-shadow: var(--box-shadow-base);
|
||||||
|
|
||||||
.chartHeader {
|
.chartHeader {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
margin-bottom: var(--spacing-m);
|
margin-bottom: var(--spacing-m);
|
||||||
padding: var(--spacing-s);
|
padding: var(--spacing-xs) var(--spacing-s);
|
||||||
border-bottom: 1px solid var(--color-foreground-base);
|
border-bottom: 1px solid var(--color-foreground-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +101,7 @@ watchEffect(() => {
|
||||||
|
|
||||||
.chartWrapper {
|
.chartWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 400px;
|
height: var(--metrics-chart-height, 400px);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--spacing-s);
|
padding: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { TestRunRecord } from '@/api/testDefinition.ee';
|
import type { TestRunRecord } from '@/api/testDefinition.ee';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import type { TestDefinitionTableColumn } from '../shared/TestDefinitionTable.vue';
|
import type { TestTableColumn } from '../shared/TestTableBase.vue';
|
||||||
import TestDefinitionTable from '../shared/TestDefinitionTable.vue';
|
import TestTableBase from '../shared/TestTableBase.vue';
|
||||||
import { convertToDisplayDate } from '@/utils/typesUtils';
|
import { convertToDisplayDate } from '@/utils/typesUtils';
|
||||||
import { VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
@ -29,7 +29,7 @@ const metrics = computed(() => {
|
||||||
}, [] as string[]);
|
}, [] as string[]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const columns = computed((): Array<TestDefinitionTableColumn<TestRunRecord>> => {
|
const columns = computed((): Array<TestTableColumn<TestRunRecord>> => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
prop: 'runNumber',
|
prop: 'runNumber',
|
||||||
|
@ -58,6 +58,8 @@ const columns = computed((): Array<TestDefinitionTableColumn<TestRunRecord>> =>
|
||||||
label: locale.baseText('testDefinition.listRuns.runDate'),
|
label: locale.baseText('testDefinition.listRuns.runDate'),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: (row: TestRunRecord) => convertToDisplayDate(new Date(row.runAt).getTime()),
|
formatter: (row: TestRunRecord) => convertToDisplayDate(new Date(row.runAt).getTime()),
|
||||||
|
sortMethod: (a: TestRunRecord, b: TestRunRecord) =>
|
||||||
|
new Date(a.runAt).getTime() - new Date(b.runAt).getTime(),
|
||||||
},
|
},
|
||||||
|
|
||||||
...metrics.value.map((metric) => ({
|
...metrics.value.map((metric) => ({
|
||||||
|
@ -81,13 +83,16 @@ function deleteRuns() {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.container">
|
<div :class="$style.container">
|
||||||
<div :class="$style.footer">
|
<N8nHeading size="large" :bold="true" :class="$style.runsTableHeading">{{
|
||||||
|
locale.baseText('testDefinition.edit.pastRuns')
|
||||||
|
}}</N8nHeading>
|
||||||
|
<div :class="$style.header">
|
||||||
<n8n-button
|
<n8n-button
|
||||||
v-show="selectedRows.length > 0"
|
v-show="selectedRows.length > 0"
|
||||||
type="danger"
|
type="danger"
|
||||||
:class="$style.activator"
|
:class="$style.activator"
|
||||||
:size="'medium'"
|
size="medium"
|
||||||
:icon="'trash'"
|
icon="trash"
|
||||||
data-test-id="delete-runs-button"
|
data-test-id="delete-runs-button"
|
||||||
@click="deleteRuns"
|
@click="deleteRuns"
|
||||||
>
|
>
|
||||||
|
@ -98,7 +103,7 @@ function deleteRuns() {
|
||||||
}}
|
}}
|
||||||
</n8n-button>
|
</n8n-button>
|
||||||
</div>
|
</div>
|
||||||
<TestDefinitionTable
|
<TestTableBase
|
||||||
:data="runs"
|
:data="runs"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
selectable
|
selectable
|
||||||
|
@ -116,5 +121,6 @@ function deleteRuns() {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -79,7 +79,7 @@ export function useMetricsChart(mode: AppliedThemeOption = 'light') {
|
||||||
color: colors.text.primary,
|
color: colors.text.primary,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: false,
|
||||||
text: params.metric,
|
text: params.metric,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
color: colors.text.primary,
|
color: colors.text.primary,
|
||||||
|
@ -90,14 +90,11 @@ export function useMetricsChart(mode: AppliedThemeOption = 'light') {
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
maxRotation: 45,
|
display: false,
|
||||||
minRotation: 45,
|
|
||||||
color: colors.text.primary,
|
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
|
||||||
text: params.xTitle,
|
text: params.xTitle,
|
||||||
padding: 16,
|
padding: 1,
|
||||||
color: colors.text.primary,
|
color: colors.text.primary,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,7 +26,11 @@ export function useTestDefinitionForm() {
|
||||||
tempValue: [],
|
tempValue: [],
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
},
|
},
|
||||||
description: '',
|
description: {
|
||||||
|
value: '',
|
||||||
|
tempValue: '',
|
||||||
|
isEditing: false,
|
||||||
|
},
|
||||||
evaluationWorkflow: {
|
evaluationWorkflow: {
|
||||||
mode: 'list',
|
mode: 'list',
|
||||||
value: '',
|
value: '',
|
||||||
|
@ -45,9 +49,11 @@ export function useTestDefinitionForm() {
|
||||||
const editableFields: ComputedRef<{
|
const editableFields: ComputedRef<{
|
||||||
name: EditableField<string>;
|
name: EditableField<string>;
|
||||||
tags: EditableField<string[]>;
|
tags: EditableField<string[]>;
|
||||||
|
description: EditableField<string>;
|
||||||
}> = computed(() => ({
|
}> = computed(() => ({
|
||||||
name: state.value.name,
|
name: state.value.name,
|
||||||
tags: state.value.tags,
|
tags: state.value.tags,
|
||||||
|
description: state.value.description,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,7 +67,11 @@ export function useTestDefinitionForm() {
|
||||||
if (testDefinition) {
|
if (testDefinition) {
|
||||||
const metrics = await evaluationsStore.fetchMetrics(testId);
|
const metrics = await evaluationsStore.fetchMetrics(testId);
|
||||||
|
|
||||||
state.value.description = testDefinition.description ?? '';
|
state.value.description = {
|
||||||
|
value: testDefinition.description ?? '',
|
||||||
|
isEditing: false,
|
||||||
|
tempValue: '',
|
||||||
|
};
|
||||||
state.value.name = {
|
state.value.name = {
|
||||||
value: testDefinition.name ?? '',
|
value: testDefinition.name ?? '',
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
|
@ -95,7 +105,7 @@ export function useTestDefinitionForm() {
|
||||||
const params = {
|
const params = {
|
||||||
name: state.value.name.value,
|
name: state.value.name.value,
|
||||||
workflowId,
|
workflowId,
|
||||||
description: state.value.description,
|
description: state.value.description.value,
|
||||||
};
|
};
|
||||||
return await evaluationsStore.create(params);
|
return await evaluationsStore.create(params);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -125,8 +135,9 @@ export function useTestDefinitionForm() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
isSaving.value = true;
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
isSaving.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateTest = async (testId: string) => {
|
const updateTest = async (testId: string) => {
|
||||||
|
@ -142,7 +153,7 @@ export function useTestDefinitionForm() {
|
||||||
|
|
||||||
const params: UpdateTestDefinitionParams = {
|
const params: UpdateTestDefinitionParams = {
|
||||||
name: state.value.name.value,
|
name: state.value.name.value,
|
||||||
description: state.value.description,
|
description: state.value.description.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (state.value.evaluationWorkflow.value) {
|
if (state.value.evaluationWorkflow.value) {
|
||||||
|
@ -157,7 +168,8 @@ export function useTestDefinitionForm() {
|
||||||
params.mockedNodes = state.value.mockedNodes;
|
params.mockedNodes = state.value.mockedNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await evaluationsStore.update({ ...params, id: testId });
|
const response = await evaluationsStore.update({ ...params, id: testId });
|
||||||
|
return response;
|
||||||
} finally {
|
} finally {
|
||||||
isSaving.value = false;
|
isSaving.value = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<script setup lang="ts" generic="T">
|
<script setup lang="ts" generic="T">
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import type { TestDefinitionTableColumn } from './TestDefinitionTable.vue';
|
import type { TestTableColumn } from './TestTableBase.vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
column: TestDefinitionTableColumn<T>;
|
column: TestTableColumn<T>;
|
||||||
row: T;
|
row: T;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ function hasProperty(row: unknown, prop: string): row is Record<string, unknown>
|
||||||
return typeof row === 'object' && row !== null && prop in row;
|
return typeof row === 'object' && row !== null && prop in row;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCellContent = (column: TestDefinitionTableColumn<T>, row: T) => {
|
const getCellContent = (column: TestTableColumn<T>, row: T) => {
|
||||||
if (column.formatter) {
|
if (column.formatter) {
|
||||||
return column.formatter(row);
|
return column.formatter(row);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
<script setup lang="ts" generic="T">
|
|
||||||
import type { RouteLocationRaw } from 'vue-router';
|
|
||||||
import TableCell from './TableCell.vue';
|
|
||||||
import { ElTable, ElTableColumn } from 'element-plus';
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import type { TableInstance } from 'element-plus';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A reusable table component for displaying test definition data
|
|
||||||
* @template T - The type of data being displayed in the table rows
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration for a table column
|
|
||||||
* @template TRow - The type of data in each table row
|
|
||||||
*/
|
|
||||||
export type TestDefinitionTableColumn<TRow> = {
|
|
||||||
prop: string;
|
|
||||||
label: string;
|
|
||||||
width?: number;
|
|
||||||
sortable?: boolean;
|
|
||||||
filters?: Array<{ text: string; value: string }>;
|
|
||||||
filterMethod?: (value: string, row: TRow) => boolean;
|
|
||||||
route?: (row: TRow) => RouteLocationRaw;
|
|
||||||
openInNewTab?: boolean;
|
|
||||||
formatter?: (row: TRow) => string;
|
|
||||||
};
|
|
||||||
|
|
||||||
withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
data: T[];
|
|
||||||
columns: Array<TestDefinitionTableColumn<T>>;
|
|
||||||
showControls?: boolean;
|
|
||||||
defaultSort?: { prop: string; order: 'ascending' | 'descending' };
|
|
||||||
selectable?: boolean;
|
|
||||||
selectableFilter?: (row: T) => boolean;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
defaultSort: () => ({ prop: 'date', order: 'ascending' }),
|
|
||||||
selectable: false,
|
|
||||||
selectableFilter: () => true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const tableRef = ref<TableInstance>();
|
|
||||||
const selectedRows = ref<T[]>([]);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
rowClick: [row: T];
|
|
||||||
selectionChange: [rows: T[]];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const handleSelectionChange = (rows: T[]) => {
|
|
||||||
selectedRows.value = rows;
|
|
||||||
emit('selectionChange', rows);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ElTable
|
|
||||||
ref="tableRef"
|
|
||||||
:default-sort="defaultSort"
|
|
||||||
:data="data"
|
|
||||||
style="width: 100%"
|
|
||||||
:border="true"
|
|
||||||
max-height="800"
|
|
||||||
resizable
|
|
||||||
@selection-change="handleSelectionChange"
|
|
||||||
>
|
|
||||||
<ElTableColumn
|
|
||||||
v-if="selectable"
|
|
||||||
type="selection"
|
|
||||||
:selectable="selectableFilter"
|
|
||||||
width="55"
|
|
||||||
data-test-id="table-column-select"
|
|
||||||
/>
|
|
||||||
<ElTableColumn
|
|
||||||
v-for="column in columns"
|
|
||||||
:key="column.prop"
|
|
||||||
v-bind="column"
|
|
||||||
style="width: 100%"
|
|
||||||
:resizable="true"
|
|
||||||
data-test-id="table-column"
|
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
|
||||||
<TableCell
|
|
||||||
:column="column"
|
|
||||||
:row="row"
|
|
||||||
@click="$emit('rowClick', row)"
|
|
||||||
data-test-id="table-cell"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
</ElTable>
|
|
||||||
</template>
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
<script setup lang="ts" generic="T extends object">
|
||||||
|
import type { RouteLocationRaw } from 'vue-router';
|
||||||
|
import TableCell from './TableCell.vue';
|
||||||
|
import { ElTable, ElTableColumn } from 'element-plus';
|
||||||
|
import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue';
|
||||||
|
import type { TableInstance } from 'element-plus';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
/**
|
||||||
|
* A reusable table component for displaying evaluation results data
|
||||||
|
* @template T - The type of data being displayed in the table rows
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for a table column
|
||||||
|
* @template TRow - The type of data in each table row
|
||||||
|
*/
|
||||||
|
export type TestTableColumn<TRow> = {
|
||||||
|
prop: string;
|
||||||
|
label: string;
|
||||||
|
width?: number;
|
||||||
|
sortable?: boolean;
|
||||||
|
filters?: Array<{ text: string; value: string }>;
|
||||||
|
filterMethod?: (value: string, row: TRow) => boolean;
|
||||||
|
route?: (row: TRow) => RouteLocationRaw;
|
||||||
|
sortMethod?: (a: TRow, b: TRow) => number;
|
||||||
|
openInNewTab?: boolean;
|
||||||
|
formatter?: (row: TRow) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TableRow = T & { id: string };
|
||||||
|
|
||||||
|
const MIN_TABLE_HEIGHT = 350;
|
||||||
|
const MAX_TABLE_HEIGHT = 1400;
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
data: TableRow[];
|
||||||
|
columns: Array<TestTableColumn<TableRow>>;
|
||||||
|
showControls?: boolean;
|
||||||
|
defaultSort?: { prop: string; order: 'ascending' | 'descending' };
|
||||||
|
selectable?: boolean;
|
||||||
|
selectableFilter?: (row: TableRow) => boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
defaultSort: () => ({ prop: 'date', order: 'descending' }),
|
||||||
|
selectable: false,
|
||||||
|
selectableFilter: () => true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableRef = ref<TableInstance>();
|
||||||
|
const selectedRows = ref<TableRow[]>([]);
|
||||||
|
const localData = ref<TableRow[]>([]);
|
||||||
|
const tableHeight = ref<string>('100%');
|
||||||
|
const emit = defineEmits<{
|
||||||
|
rowClick: [row: TableRow];
|
||||||
|
selectionChange: [rows: TableRow[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// Watch for changes to the data prop and update the local data state
|
||||||
|
// This preserves selected rows when the data changes by:
|
||||||
|
// 1. Storing current selection IDs
|
||||||
|
// 2. Updating local data with new data
|
||||||
|
// 3. Re-applying default sort
|
||||||
|
// 4. Re-selecting previously selected rows that still exist in new data
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
async (newData) => {
|
||||||
|
if (!isEqual(localData.value, newData)) {
|
||||||
|
const currentSelectionIds = selectedRows.value.map((row) => row.id);
|
||||||
|
|
||||||
|
localData.value = newData;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
tableRef.value?.sort(props.defaultSort.prop, props.defaultSort.order);
|
||||||
|
currentSelectionIds.forEach((id) => {
|
||||||
|
const row = localData.value.find((r) => r.id === id);
|
||||||
|
if (row) {
|
||||||
|
tableRef.value?.toggleRowSelection(row, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectionChange = (rows: TableRow[]) => {
|
||||||
|
selectedRows.value = rows;
|
||||||
|
emit('selectionChange', rows);
|
||||||
|
};
|
||||||
|
|
||||||
|
const computeTableHeight = () => {
|
||||||
|
const containerHeight = tableRef.value?.$el?.parentElement?.clientHeight ?? 600;
|
||||||
|
const height = Math.min(Math.max(containerHeight, MIN_TABLE_HEIGHT), MAX_TABLE_HEIGHT);
|
||||||
|
tableHeight.value = `${height - 100}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
computeTableHeight();
|
||||||
|
|
||||||
|
window.addEventListener('resize', computeTableHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', computeTableHeight);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElTable
|
||||||
|
ref="tableRef"
|
||||||
|
:class="$style.table"
|
||||||
|
:default-sort="defaultSort"
|
||||||
|
:data="localData"
|
||||||
|
:border="true"
|
||||||
|
:max-height="tableHeight"
|
||||||
|
resizable
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@vue:mounted="computeTableHeight"
|
||||||
|
>
|
||||||
|
<ElTableColumn
|
||||||
|
v-if="selectable"
|
||||||
|
type="selection"
|
||||||
|
:selectable="selectableFilter"
|
||||||
|
data-test-id="table-column-select"
|
||||||
|
/>
|
||||||
|
<ElTableColumn
|
||||||
|
v-for="column in columns"
|
||||||
|
:key="column.prop"
|
||||||
|
v-bind="column"
|
||||||
|
:resizable="true"
|
||||||
|
data-test-id="table-column"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<TableCell
|
||||||
|
:key="row.status"
|
||||||
|
:column="column"
|
||||||
|
:row="row"
|
||||||
|
data-test-id="table-cell"
|
||||||
|
@click="$emit('rowClick', row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.table {
|
||||||
|
:global(.el-table__cell) {
|
||||||
|
padding: var(--spacing-3xs) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -42,7 +42,7 @@ describe('useTestDefinitionForm', () => {
|
||||||
it('should initialize with default props', () => {
|
it('should initialize with default props', () => {
|
||||||
const { state } = useTestDefinitionForm();
|
const { state } = useTestDefinitionForm();
|
||||||
|
|
||||||
expect(state.value.description).toBe('');
|
expect(state.value.description.value).toBe('');
|
||||||
expect(state.value.name.value).toContain('My Test');
|
expect(state.value.name.value).toContain('My Test');
|
||||||
expect(state.value.tags.value).toEqual([]);
|
expect(state.value.tags.value).toEqual([]);
|
||||||
expect(state.value.metrics).toEqual([]);
|
expect(state.value.metrics).toEqual([]);
|
||||||
|
@ -70,7 +70,7 @@ describe('useTestDefinitionForm', () => {
|
||||||
expect(fetchSpy).toBeCalled();
|
expect(fetchSpy).toBeCalled();
|
||||||
expect(fetchMetricsSpy).toBeCalledWith(TEST_DEF_A.id);
|
expect(fetchMetricsSpy).toBeCalledWith(TEST_DEF_A.id);
|
||||||
expect(state.value.name.value).toEqual(TEST_DEF_A.name);
|
expect(state.value.name.value).toEqual(TEST_DEF_A.name);
|
||||||
expect(state.value.description).toEqual(TEST_DEF_A.description);
|
expect(state.value.description.value).toEqual(TEST_DEF_A.description);
|
||||||
expect(state.value.tags.value).toEqual([TEST_DEF_A.annotationTagId]);
|
expect(state.value.tags.value).toEqual([TEST_DEF_A.annotationTagId]);
|
||||||
expect(state.value.evaluationWorkflow.value).toEqual(TEST_DEF_A.evaluationWorkflowId);
|
expect(state.value.evaluationWorkflow.value).toEqual(TEST_DEF_A.evaluationWorkflowId);
|
||||||
expect(state.value.metrics).toEqual([
|
expect(state.value.metrics).toEqual([
|
||||||
|
@ -88,7 +88,7 @@ describe('useTestDefinitionForm', () => {
|
||||||
await loadTestData('unknown-id');
|
await loadTestData('unknown-id');
|
||||||
expect(fetchSpy).toBeCalled();
|
expect(fetchSpy).toBeCalled();
|
||||||
// Should remain unchanged since no definition found
|
// Should remain unchanged since no definition found
|
||||||
expect(state.value.description).toBe('');
|
expect(state.value.description.value).toBe('');
|
||||||
expect(state.value.name.value).toContain('My Test');
|
expect(state.value.name.value).toContain('My Test');
|
||||||
expect(state.value.tags.value).toEqual([]);
|
expect(state.value.tags.value).toEqual([]);
|
||||||
expect(state.value.metrics).toEqual([]);
|
expect(state.value.metrics).toEqual([]);
|
||||||
|
@ -112,7 +112,7 @@ describe('useTestDefinitionForm', () => {
|
||||||
const createSpy = vi.spyOn(useTestDefinitionStore(), 'create').mockResolvedValue(TEST_DEF_NEW);
|
const createSpy = vi.spyOn(useTestDefinitionStore(), 'create').mockResolvedValue(TEST_DEF_NEW);
|
||||||
|
|
||||||
state.value.name.value = TEST_DEF_NEW.name;
|
state.value.name.value = TEST_DEF_NEW.name;
|
||||||
state.value.description = TEST_DEF_NEW.description ?? '';
|
state.value.description.value = TEST_DEF_NEW.description ?? '';
|
||||||
|
|
||||||
const newTest = await createTest('123');
|
const newTest = await createTest('123');
|
||||||
expect(createSpy).toBeCalledWith({
|
expect(createSpy).toBeCalledWith({
|
||||||
|
@ -143,7 +143,7 @@ describe('useTestDefinitionForm', () => {
|
||||||
const updateSpy = vi.spyOn(useTestDefinitionStore(), 'update').mockResolvedValue(updatedBTest);
|
const updateSpy = vi.spyOn(useTestDefinitionStore(), 'update').mockResolvedValue(updatedBTest);
|
||||||
|
|
||||||
state.value.name.value = TEST_DEF_B.name;
|
state.value.name.value = TEST_DEF_B.name;
|
||||||
state.value.description = TEST_DEF_B.description ?? '';
|
state.value.description.value = TEST_DEF_B.description ?? '';
|
||||||
|
|
||||||
const updatedTest = await updateTest(TEST_DEF_A.id);
|
const updatedTest = await updateTest(TEST_DEF_A.id);
|
||||||
expect(updateSpy).toBeCalledWith({
|
expect(updateSpy).toBeCalledWith({
|
||||||
|
@ -166,7 +166,7 @@ describe('useTestDefinitionForm', () => {
|
||||||
.mockRejectedValue(new Error('Update Failed'));
|
.mockRejectedValue(new Error('Update Failed'));
|
||||||
|
|
||||||
state.value.name.value = 'Test';
|
state.value.name.value = 'Test';
|
||||||
state.value.description = 'Some description';
|
state.value.description.value = 'Some description';
|
||||||
|
|
||||||
await expect(updateTest(TEST_DEF_A.id)).rejects.toThrow('Update Failed');
|
await expect(updateTest(TEST_DEF_A.id)).rejects.toThrow('Update Failed');
|
||||||
expect(updateSpy).toBeCalled();
|
expect(updateSpy).toBeCalled();
|
||||||
|
|
|
@ -10,10 +10,10 @@ export interface EditableField<T = string> {
|
||||||
export interface EditableFormState {
|
export interface EditableFormState {
|
||||||
name: EditableField<string>;
|
name: EditableField<string>;
|
||||||
tags: EditableField<string[]>;
|
tags: EditableField<string[]>;
|
||||||
|
description: EditableField<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EvaluationFormState extends EditableFormState {
|
export interface EvaluationFormState extends EditableFormState {
|
||||||
description: string;
|
|
||||||
evaluationWorkflow: INodeParameterResourceLocator;
|
evaluationWorkflow: INodeParameterResourceLocator;
|
||||||
metrics: TestMetricRecord[];
|
metrics: TestMetricRecord[];
|
||||||
mockedNodes: Array<{ name: string; id: string }>;
|
mockedNodes: Array<{ name: string; id: string }>;
|
||||||
|
|
|
@ -2775,7 +2775,9 @@
|
||||||
"communityPlusModal.button.confirm": "Send me a free license key",
|
"communityPlusModal.button.confirm": "Send me a free license key",
|
||||||
"executeWorkflowTrigger.createNewSubworkflow": "Create a sub-workflow in {projectName}",
|
"executeWorkflowTrigger.createNewSubworkflow": "Create a sub-workflow in {projectName}",
|
||||||
"executeWorkflowTrigger.createNewSubworkflow.noProject": "Create a new sub-workflow",
|
"executeWorkflowTrigger.createNewSubworkflow.noProject": "Create a new sub-workflow",
|
||||||
"testDefinition.edit.descriptionPlaceholder": "",
|
"testDefinition.edit.descriptionPlaceholder": "Enter test description",
|
||||||
|
"testDefinition.edit.showConfig": "Show config",
|
||||||
|
"testDefinition.edit.hideConfig": "Hide config",
|
||||||
"testDefinition.edit.backButtonTitle": "Back to Workflow Evaluation",
|
"testDefinition.edit.backButtonTitle": "Back to Workflow Evaluation",
|
||||||
"testDefinition.edit.namePlaceholder": "Enter test name",
|
"testDefinition.edit.namePlaceholder": "Enter test name",
|
||||||
"testDefinition.edit.metricsTitle": "Metrics",
|
"testDefinition.edit.metricsTitle": "Metrics",
|
||||||
|
@ -2790,14 +2792,15 @@
|
||||||
"testDefinition.edit.workflowSelectorTitle": "Workflow to make comparisons",
|
"testDefinition.edit.workflowSelectorTitle": "Workflow to make comparisons",
|
||||||
"testDefinition.edit.workflowSelectorHelpText": "This workflow will be called once for each test case.",
|
"testDefinition.edit.workflowSelectorHelpText": "This workflow will be called once for each test case.",
|
||||||
"testDefinition.edit.updateTest": "Update test",
|
"testDefinition.edit.updateTest": "Update test",
|
||||||
"testDefinition.edit.saveTest": "Run test",
|
"testDefinition.edit.saveTest": "Save test",
|
||||||
|
"testDefinition.edit.runTest": "Run test",
|
||||||
"testDefinition.edit.testSaved": "Test saved",
|
"testDefinition.edit.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.description": "Add details about what this test evaluates and what success looks like",
|
"testDefinition.edit.description.description": "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": "1. Fetch N past executions tagge | Fetch {count} past execution tagged | Fetch {count} past executions tagged",
|
"testDefinition.edit.step.executions": "1. Fetch N past executions tagged | 1. Fetch {count} past execution tagged | 1. Fetch {count} past executions tagged",
|
||||||
"testDefinition.edit.step.executions.description": "Use a tag to select past executions for use as test cases in evaluation. The trigger data from each of these past executions will be used as input to run your workflow. The outputs of past executions are used as benchmark and compared against to check whether performance has changed based on logic and metrics that you define below.",
|
"testDefinition.edit.step.executions.description": "Use a tag to select past executions for use as test cases in evaluation. The trigger data from each of these past executions will be used as input to run your workflow. The outputs of past executions are used as benchmark and compared against to check whether performance has changed based on logic and metrics that you define below.",
|
||||||
"testDefinition.edit.step.mockedNodes": "2. Mock N nodes |2. Mock {count} node |2. Mock {count} nodes",
|
"testDefinition.edit.step.mockedNodes": "2. Mock N nodes |2. Mock {count} node |2. Mock {count} nodes",
|
||||||
"testDefinition.edit.step.nodes.description": "Mocked nodes have their data replayed rather than being re-executed. Do this to avoid calling external services, save time executing, and isolate what you are evaluating. If a node is mocked, the tagged past execution's output data for that node is used in the evaluation instead.",
|
"testDefinition.edit.step.nodes.description": "Mocked nodes have their data replayed rather than being re-executed. Do this to avoid calling external services, save time executing, and isolate what you are evaluating. If a node is mocked, the tagged past execution's output data for that node is used in the evaluation instead.",
|
||||||
|
@ -2814,11 +2817,17 @@
|
||||||
"testDefinition.edit.pastRuns": "Past runs",
|
"testDefinition.edit.pastRuns": "Past runs",
|
||||||
"testDefinition.edit.pastRuns.total": "No runs | {count} run | {count} runs",
|
"testDefinition.edit.pastRuns.total": "No runs | {count} run | {count} runs",
|
||||||
"testDefinition.edit.nodesPinning.pinButtonTooltip": "Pin execution data of this node during test run",
|
"testDefinition.edit.nodesPinning.pinButtonTooltip": "Pin execution data of this node during test run",
|
||||||
|
"testDefinition.edit.saving": "Saving...",
|
||||||
|
"testDefinition.edit.saved": "Changes saved",
|
||||||
"testDefinition.list.testDeleted": "Test deleted",
|
"testDefinition.list.testDeleted": "Test deleted",
|
||||||
"testDefinition.list.tests": "Tests",
|
"testDefinition.list.tests": "Tests",
|
||||||
|
"testDefinition.list.evaluations": "Evaluations",
|
||||||
|
"testDefinition.list.unitTests.title": "Unit tests",
|
||||||
|
"testDefinition.list.unitTests.description": "Test sections of your workflow in isolation",
|
||||||
|
"testDefinition.list.unitTests.cta": "Register interest",
|
||||||
"testDefinition.list.createNew": "Create new test",
|
"testDefinition.list.createNew": "Create new test",
|
||||||
"testDefinition.list.actionDescription": "Replay past executions to check whether performance has changed",
|
"testDefinition.list.actionDescription": "Replay past executions to check whether performance has changed",
|
||||||
"testDefinition.list.actionButton": "Create Test",
|
"testDefinition.list.actionButton": "Create an Evaluation",
|
||||||
"testDefinition.list.testRuns": "No test runs | {count} test run | {count} test runs",
|
"testDefinition.list.testRuns": "No test runs | {count} test run | {count} test runs",
|
||||||
"testDefinition.list.lastRun": "Ran",
|
"testDefinition.list.lastRun": "Ran",
|
||||||
"testDefinition.list.running": "Running",
|
"testDefinition.list.running": "Running",
|
||||||
|
|
|
@ -53,6 +53,7 @@ import {
|
||||||
faEnvelope,
|
faEnvelope,
|
||||||
faEquals,
|
faEquals,
|
||||||
faEye,
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
faExclamationTriangle,
|
faExclamationTriangle,
|
||||||
faExpand,
|
faExpand,
|
||||||
faExpandAlt,
|
faExpandAlt,
|
||||||
|
@ -228,6 +229,7 @@ export const FontAwesomePlugin: Plugin = {
|
||||||
addIcon(faEnvelope);
|
addIcon(faEnvelope);
|
||||||
addIcon(faEquals);
|
addIcon(faEquals);
|
||||||
addIcon(faEye);
|
addIcon(faEye);
|
||||||
|
addIcon(faEyeSlash);
|
||||||
addIcon(faExclamationTriangle);
|
addIcon(faExclamationTriangle);
|
||||||
addIcon(faExclamationCircle);
|
addIcon(faExclamationCircle);
|
||||||
addIcon(faExpand);
|
addIcon(faExpand);
|
||||||
|
|
|
@ -36,6 +36,7 @@ const uiStore = useUIStore();
|
||||||
const {
|
const {
|
||||||
state,
|
state,
|
||||||
fieldsIssues,
|
fieldsIssues,
|
||||||
|
isSaving,
|
||||||
cancelEditing,
|
cancelEditing,
|
||||||
loadTestData,
|
loadTestData,
|
||||||
createTest,
|
createTest,
|
||||||
|
@ -52,13 +53,15 @@ const allTags = computed(() => tagsStore.allTags);
|
||||||
const tagsById = computed(() => tagsStore.tagsById);
|
const tagsById = computed(() => tagsStore.tagsById);
|
||||||
const testId = computed(() => props.testId ?? (route.params.testId as string));
|
const testId = computed(() => props.testId ?? (route.params.testId as string));
|
||||||
const currentWorkflowId = computed(() => route.params.name as string);
|
const currentWorkflowId = computed(() => route.params.name as string);
|
||||||
|
const appliedTheme = computed(() => uiStore.appliedTheme);
|
||||||
const tagUsageCount = computed(
|
const tagUsageCount = computed(
|
||||||
() => tagsStore.tagsById[state.value.tags.value[0]]?.usageCount ?? 0,
|
() => tagsStore.tagsById[state.value.tags.value[0]]?.usageCount ?? 0,
|
||||||
);
|
);
|
||||||
|
const hasRuns = computed(() => runs.value.length > 0);
|
||||||
const nodePinningModal = ref<ModalState | null>(null);
|
const nodePinningModal = ref<ModalState | null>(null);
|
||||||
const modalContentWidth = ref(0);
|
const modalContentWidth = ref(0);
|
||||||
|
const showConfig = ref(true);
|
||||||
|
const selectedMetric = ref<string>('');
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!testDefinitionStore.isFeatureEnabled) {
|
if (!testDefinitionStore.isFeatureEnabled) {
|
||||||
|
@ -135,14 +138,18 @@ const runs = computed(() =>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
async function onDeleteRuns(runs: TestRunRecord[]) {
|
async function onDeleteRuns(toDelete: TestRunRecord[]) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
runs.map(async (run) => {
|
toDelete.map(async (run) => {
|
||||||
await testDefinitionStore.deleteTestRun({ testDefinitionId: testId.value, runId: run.id });
|
await testDefinitionStore.deleteTestRun({ testDefinitionId: testId.value, runId: run.id });
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleConfig() {
|
||||||
|
showConfig.value = !showConfig.value;
|
||||||
|
}
|
||||||
|
|
||||||
// Debounced watchers for auto-saving
|
// Debounced watchers for auto-saving
|
||||||
watch(
|
watch(
|
||||||
() => state.value.metrics,
|
() => state.value.metrics,
|
||||||
|
@ -164,29 +171,82 @@ watch(
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.container">
|
<div :class="[$style.container, { [$style.noRuns]: !hasRuns }]">
|
||||||
<div :class="$style.formContent">
|
<div :class="$style.headerSection">
|
||||||
<!-- Name -->
|
<div :class="$style.headerMeta">
|
||||||
<EvaluationHeader
|
<div :class="$style.name">
|
||||||
v-model="state.name"
|
<EvaluationHeader
|
||||||
:class="{ 'has-issues': hasIssues('name') }"
|
v-model="state.name"
|
||||||
:start-editing="startEditing"
|
:class="{ 'has-issues': hasIssues('name') }"
|
||||||
:save-changes="saveChanges"
|
:start-editing="startEditing"
|
||||||
:handle-keydown="handleKeydown"
|
:save-changes="saveChanges"
|
||||||
/>
|
:handle-keydown="handleKeydown"
|
||||||
<div :class="$style.panelBlock">
|
/>
|
||||||
<!-- Description -->
|
<div :class="$style.lastSaved">
|
||||||
<EvaluationStep
|
<template v-if="isSaving">
|
||||||
:class="$style.step"
|
{{ locale.baseText('testDefinition.edit.saving') }}
|
||||||
:title="locale.baseText('testDefinition.edit.description')"
|
</template>
|
||||||
:expanded="false"
|
<template v-else> {{ locale.baseText('testDefinition.edit.saved') }} </template>
|
||||||
>
|
</div>
|
||||||
<template #icon><font-awesome-icon icon="thumbtack" size="lg" /></template>
|
</div>
|
||||||
<template #cardContent>
|
<DescriptionInput
|
||||||
<DescriptionInput v-model="state.description" />
|
v-model="state.description"
|
||||||
</template>
|
:start-editing="startEditing"
|
||||||
</EvaluationStep>
|
:save-changes="saveChanges"
|
||||||
|
:handle-keydown="handleKeydown"
|
||||||
|
:class="$style.descriptionInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.controls">
|
||||||
|
<n8n-button
|
||||||
|
v-if="runs.length > 0"
|
||||||
|
size="small"
|
||||||
|
:icon="showConfig ? 'eye-slash' : 'eye'"
|
||||||
|
data-test-id="toggle-config-button"
|
||||||
|
:label="
|
||||||
|
showConfig
|
||||||
|
? locale.baseText('testDefinition.edit.hideConfig')
|
||||||
|
: locale.baseText('testDefinition.edit.showConfig')
|
||||||
|
"
|
||||||
|
type="tertiary"
|
||||||
|
@click="toggleConfig"
|
||||||
|
/>
|
||||||
|
<n8n-button
|
||||||
|
v-if="state.evaluationWorkflow.value && state.tags.value.length > 0"
|
||||||
|
:class="$style.runTestButton"
|
||||||
|
size="small"
|
||||||
|
data-test-id="run-test-button"
|
||||||
|
:label="locale.baseText('testDefinition.runTest')"
|
||||||
|
type="primary"
|
||||||
|
@click="runTest"
|
||||||
|
/>
|
||||||
|
<n8n-button
|
||||||
|
v-else
|
||||||
|
:class="$style.runTestButton"
|
||||||
|
size="small"
|
||||||
|
data-test-id="run-test-button"
|
||||||
|
:label="locale.baseText('testDefinition.edit.saveTest')"
|
||||||
|
type="primary"
|
||||||
|
@click="onSaveTest"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="$style.content">
|
||||||
|
<div v-if="runs.length > 0" :class="$style.runs">
|
||||||
|
<!-- Metrics Chart -->
|
||||||
|
<MetricsChart v-model:selectedMetric="selectedMetric" :runs="runs" :theme="appliedTheme" />
|
||||||
|
<!-- Past Runs Table -->
|
||||||
|
<TestRunsTable
|
||||||
|
:class="$style.runsTable"
|
||||||
|
:runs="runs"
|
||||||
|
:selectable="true"
|
||||||
|
data-test-id="past-runs-table"
|
||||||
|
@delete-runs="onDeleteRuns"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="[$style.panelBlock, { [$style.hidden]: !showConfig }]">
|
||||||
<div :class="$style.panelIntro">
|
<div :class="$style.panelIntro">
|
||||||
{{ locale.baseText('testDefinition.edit.step.intro') }}
|
{{ locale.baseText('testDefinition.edit.step.intro') }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -286,37 +346,6 @@ watch(
|
||||||
</template>
|
</template>
|
||||||
</EvaluationStep>
|
</EvaluationStep>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n8n-button
|
|
||||||
v-if="state.evaluationWorkflow.value && state.tags.value.length > 0"
|
|
||||||
:class="$style.runTestButton"
|
|
||||||
size="small"
|
|
||||||
data-test-id="run-test-button"
|
|
||||||
:label="locale.baseText('testDefinition.runTest')"
|
|
||||||
type="primary"
|
|
||||||
@click="runTest"
|
|
||||||
/>
|
|
||||||
<n8n-button
|
|
||||||
v-else
|
|
||||||
:class="$style.runTestButton"
|
|
||||||
size="small"
|
|
||||||
data-test-id="run-test-button"
|
|
||||||
:label="'Save Test'"
|
|
||||||
type="primary"
|
|
||||||
@click="onSaveTest"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- Past Runs Table -->
|
|
||||||
<div v-if="runs.length > 0" :class="$style.runsTable">
|
|
||||||
<N8nHeading size="large" :bold="true" :class="$style.runsTableHeading">{{
|
|
||||||
locale.baseText('testDefinition.edit.pastRuns')
|
|
||||||
}}</N8nHeading>
|
|
||||||
<TestRunsTable
|
|
||||||
:runs="runs"
|
|
||||||
:selectable="true"
|
|
||||||
data-test-id="past-runs-table"
|
|
||||||
@delete-runs="onDeleteRuns"
|
|
||||||
/>
|
|
||||||
</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">
|
||||||
|
@ -338,52 +367,102 @@ watch(
|
||||||
|
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.container {
|
.container {
|
||||||
--evaluation-edit-panel-width: 35rem;
|
--evaluation-edit-panel-width: 24rem;
|
||||||
width: 100%;
|
--metrics-chart-height: 10rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
|
||||||
padding: var(--spacing-s);
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(auto, var(--evaluation-edit-panel-width)) 1fr;
|
|
||||||
gap: var(--spacing-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.formContent {
|
|
||||||
width: 100%;
|
|
||||||
min-width: fit-content;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
@media (min-height: 56rem) {
|
||||||
|
--metrics-chart-height: 16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include mixins.breakpoint('lg-and-up') {
|
||||||
|
--evaluation-edit-panel-width: 30rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.runsTableTotal {
|
|
||||||
display: block;
|
.content {
|
||||||
margin-bottom: var(--spacing-xs);
|
display: flex;
|
||||||
|
overflow-y: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.noRuns & {
|
||||||
|
justify-content: center;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.runsTable {
|
|
||||||
flex-shrink: 1;
|
.headerSection {
|
||||||
max-width: 100%;
|
display: flex;
|
||||||
max-height: 80vh;
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
background-color: var(--color-background-light);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerMeta {
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.lastSaved {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
color: var(--color-text-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionInput {
|
||||||
|
margin-top: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.runs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
flex: 1;
|
||||||
|
padding-top: var(--spacing-3xs);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
|
@media (min-height: 56rem) {
|
||||||
|
margin-top: var(--spacing-2xl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.runsTableHeading {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
.panelBlock {
|
.panelBlock {
|
||||||
max-width: var(--evaluation-edit-panel-width, 35rem);
|
width: var(--evaluation-edit-panel-width);
|
||||||
display: grid;
|
display: grid;
|
||||||
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
min-height: 0;
|
flex-shrink: 0;
|
||||||
|
padding-bottom: var(--spacing-l);
|
||||||
|
margin-left: var(--spacing-2xl);
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
margin-left: 0;
|
||||||
|
width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noRuns & {
|
||||||
|
overflow-y: initial;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.panelIntro {
|
.panelIntro {
|
||||||
font-size: var(--font-size-m);
|
font-size: var(--font-size-m);
|
||||||
color: var(--color-text-dark);
|
color: var(--color-text-dark);
|
||||||
margin-top: var(--spacing-s);
|
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step {
|
.step {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -391,10 +470,13 @@ watch(
|
||||||
margin-top: var(--spacing-m);
|
margin-top: var(--spacing-m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.introArrow {
|
.introArrow {
|
||||||
--arrow-height: 1.5rem;
|
--arrow-height: 1.5rem;
|
||||||
|
margin-bottom: -1rem;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.evaluationArrows {
|
.evaluationArrows {
|
||||||
--arrow-height: 22rem;
|
--arrow-height: 22rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -405,38 +487,9 @@ watch(
|
||||||
margin-bottom: -100%;
|
margin-bottom: -100%;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
.footer {
|
|
||||||
margin-top: var(--spacing-xl);
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
gap: var(--spacing-s);
|
||||||
}
|
|
||||||
|
|
||||||
.workflow {
|
|
||||||
padding: var(--spacing-l);
|
|
||||||
background-color: var(--color-background-light);
|
|
||||||
border-radius: var(--border-radius-large);
|
|
||||||
border: var(--border-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workflowSteps {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--spacing-2xs);
|
|
||||||
max-width: 42rem;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sideBySide {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto 1fr;
|
|
||||||
gap: var(--spacing-2xs);
|
|
||||||
justify-items: end;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
.mockedNodesLabel {
|
|
||||||
min-height: 1.5rem;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.runTestButton {
|
|
||||||
margin-top: var(--spacing-m);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -198,10 +198,10 @@ onMounted(() => {
|
||||||
</template>
|
</template>
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.container {
|
.container {
|
||||||
padding: var(--spacing-xl) var(--spacing-l);
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: var(--content-container-width);
|
max-width: var(--content-container-width);
|
||||||
|
margin: auto;
|
||||||
}
|
}
|
||||||
.loading {
|
.loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -21,5 +21,16 @@ onMounted(initWorkflow);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<router-view />
|
<div :class="$style.evaluationsView">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.evaluationsView {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
padding: var(--spacing-xl) var(--spacing-l);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { useRouter } from 'vue-router';
|
||||||
import { convertToDisplayDate } from '@/utils/typesUtils';
|
import { convertToDisplayDate } from '@/utils/typesUtils';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { N8nCard, N8nText } from 'n8n-design-system';
|
import { N8nCard, N8nText } from 'n8n-design-system';
|
||||||
import TestDefinitionTable from '@/components/TestDefinition/shared/TestDefinitionTable.vue';
|
import TestTableBase from '@/components/TestDefinition/shared/TestTableBase.vue';
|
||||||
import type { TestDefinitionTableColumn } from '@/components/TestDefinition/shared/TestDefinitionTable.vue';
|
import type { TestTableColumn } from '@/components/TestDefinition/shared/TestTableBase.vue';
|
||||||
import { useExecutionsStore } from '@/stores/executions.store';
|
import { useExecutionsStore } from '@/stores/executions.store';
|
||||||
import { get } from 'lodash-es';
|
import { get } from 'lodash-es';
|
||||||
import type { ExecutionSummaryWithScopes } from '@/Interface';
|
import type { ExecutionSummaryWithScopes } from '@/Interface';
|
||||||
|
@ -36,7 +36,7 @@ const filteredTestCases = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const columns = computed(
|
const columns = computed(
|
||||||
(): Array<TestDefinitionTableColumn<TestCase>> => [
|
(): Array<TestTableColumn<TestCase>> => [
|
||||||
{
|
{
|
||||||
prop: 'id',
|
prop: 'id',
|
||||||
width: 200,
|
width: 200,
|
||||||
|
@ -193,7 +193,7 @@ onMounted(async () => {
|
||||||
<div v-if="isLoading" :class="$style.loading">
|
<div v-if="isLoading" :class="$style.loading">
|
||||||
<n8n-loading :loading="true" :rows="5" />
|
<n8n-loading :loading="true" :rows="5" />
|
||||||
</div>
|
</div>
|
||||||
<TestDefinitionTable
|
<TestTableBase
|
||||||
v-else
|
v-else
|
||||||
:data="filteredTestCases"
|
:data="filteredTestCases"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
|
@ -205,10 +205,10 @@ onMounted(async () => {
|
||||||
|
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.container {
|
.container {
|
||||||
padding: var(--spacing-xl) var(--spacing-l);
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: var(--content-container-width);
|
max-width: var(--content-container-width);
|
||||||
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backButton {
|
.backButton {
|
||||||
|
|
|
@ -98,10 +98,10 @@ onMounted(async () => {
|
||||||
<N8nLoading :rows="5" />
|
<N8nLoading :rows="5" />
|
||||||
<N8nLoading :rows="10" />
|
<N8nLoading :rows="10" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="runs.length > 0">
|
<div :class="$style.details" v-else-if="runs.length > 0">
|
||||||
<MetricsChart v-model:selectedMetric="selectedMetric" :runs="runs" :theme="appliedTheme" />
|
<MetricsChart v-model:selectedMetric="selectedMetric" :runs="runs" :theme="appliedTheme" />
|
||||||
<TestRunsTable :runs="runs" @get-run-detail="getRunDetail" @delete-runs="onDeleteRuns" />
|
<TestRunsTable :runs="runs" @get-run-detail="getRunDetail" @delete-runs="onDeleteRuns" />
|
||||||
</template>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<N8nActionBox
|
<N8nActionBox
|
||||||
:heading="locale.baseText('testDefinition.listRuns.noRuns')"
|
:heading="locale.baseText('testDefinition.listRuns.noRuns')"
|
||||||
|
@ -114,16 +114,24 @@ onMounted(async () => {
|
||||||
</template>
|
</template>
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.container {
|
.container {
|
||||||
padding: var(--spacing-xl) var(--spacing-l);
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: var(--content-container-width);
|
max-width: var(--content-container-width);
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.backButton {
|
.backButton {
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
}
|
}
|
||||||
.description {
|
.description {
|
||||||
margin-top: var(--spacing-xs);
|
margin: var(--spacing-s) 0;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.details {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -75,7 +75,7 @@ describe('TestDefinitionEditView', () => {
|
||||||
vi.mocked(useTestDefinitionForm).mockReturnValue({
|
vi.mocked(useTestDefinitionForm).mockReturnValue({
|
||||||
state: ref({
|
state: ref({
|
||||||
name: { value: '', isEditing: false, tempValue: '' },
|
name: { value: '', isEditing: false, tempValue: '' },
|
||||||
description: '',
|
description: { value: '', isEditing: false, tempValue: '' },
|
||||||
tags: { value: [], tempValue: [], isEditing: false },
|
tags: { value: [], tempValue: [], isEditing: false },
|
||||||
evaluationWorkflow: { mode: 'list', value: '', __rl: true },
|
evaluationWorkflow: { mode: 'list', value: '', __rl: true },
|
||||||
metrics: [],
|
metrics: [],
|
||||||
|
@ -273,7 +273,7 @@ describe('TestDefinitionEditView', () => {
|
||||||
...vi.mocked(useTestDefinitionForm)(),
|
...vi.mocked(useTestDefinitionForm)(),
|
||||||
state: ref({
|
state: ref({
|
||||||
name: { value: 'Test', isEditing: false, tempValue: '' },
|
name: { value: 'Test', isEditing: false, tempValue: '' },
|
||||||
description: '',
|
description: { value: '', isEditing: false, tempValue: '' },
|
||||||
tags: { value: ['tag1'], tempValue: [], isEditing: false },
|
tags: { value: ['tag1'], tempValue: [], isEditing: false },
|
||||||
evaluationWorkflow: { mode: 'list', value: 'workflow1', __rl: true },
|
evaluationWorkflow: { mode: 'list', value: 'workflow1', __rl: true },
|
||||||
metrics: [],
|
metrics: [],
|
||||||
|
|
Loading…
Reference in a new issue