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">
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import type { EditableField } from '../types';
|
||||
|
||||
interface Props {
|
||||
modelValue: string;
|
||||
modelValue: EditableField<string>;
|
||||
startEditing: (field: 'description') => void;
|
||||
saveChanges: (field: 'description') => void;
|
||||
handleKeydown: (e: KeyboardEvent, field: 'description') => void;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
});
|
||||
|
||||
defineEmits<{ 'update:modelValue': [value: string] }>();
|
||||
defineProps<Props>();
|
||||
defineEmits<{ 'update:modelValue': [value: EditableField<string>] }>();
|
||||
|
||||
const locale = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[$style.description]">
|
||||
<n8n-input-label
|
||||
:label="locale.baseText('testDefinition.edit.description')"
|
||||
:bold="false"
|
||||
size="small"
|
||||
:class="$style.field"
|
||||
>
|
||||
<N8nInput
|
||||
:model-value="modelValue"
|
||||
type="textarea"
|
||||
:placeholder="locale.baseText('testDefinition.edit.descriptionPlaceholder')"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
<div :class="$style.description">
|
||||
<template v-if="!modelValue.isEditing">
|
||||
<span :class="$style.descriptionText" @click="startEditing('description')">
|
||||
<n8n-icon
|
||||
v-if="modelValue.value.length === 0"
|
||||
:class="$style.icon"
|
||||
icon="plus"
|
||||
color="text-light"
|
||||
size="medium"
|
||||
/>
|
||||
<N8nText size="medium">
|
||||
{{ 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>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.field {
|
||||
width: 100%;
|
||||
margin-top: var(--spacing-xs);
|
||||
.description {
|
||||
display: flex;
|
||||
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>
|
||||
|
|
|
@ -18,8 +18,8 @@ const locale = useI18n();
|
|||
<template>
|
||||
<div :class="$style.header">
|
||||
<n8n-icon-button
|
||||
icon="arrow-left"
|
||||
:class="$style.backButton"
|
||||
icon="arrow-left"
|
||||
type="tertiary"
|
||||
:title="locale.baseText('testDefinition.edit.backButtonTitle')"
|
||||
@click="$router.back()"
|
||||
|
@ -55,8 +55,6 @@ const locale = useI18n();
|
|||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2xs);
|
||||
margin-bottom: var(--spacing-l);
|
||||
|
||||
&:hover {
|
||||
.editInputButton {
|
||||
|
@ -86,12 +84,13 @@ const locale = useI18n();
|
|||
|
||||
.editInputButton {
|
||||
--button-font-color: var(--prim-gray-490);
|
||||
opacity: 0;
|
||||
opacity: 0.2;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.backButton {
|
||||
--button-font-color: var(--color-text-light);
|
||||
border: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -90,6 +90,7 @@ const toggleExpand = async () => {
|
|||
|
||||
&.small {
|
||||
width: 80%;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
|
|
|
@ -11,19 +11,28 @@ const locale = useI18n();
|
|||
<div :class="$style.header">
|
||||
<h1>{{ locale.baseText('testDefinition.list.tests') }}</h1>
|
||||
</div>
|
||||
<n8n-action-box
|
||||
:description="locale.baseText('testDefinition.list.actionDescription')"
|
||||
:button-text="locale.baseText('testDefinition.list.actionButton')"
|
||||
@click:button="$emit('create-test')"
|
||||
/>
|
||||
<div :class="$style.content">
|
||||
<n8n-action-box
|
||||
:class="$style.actionBox"
|
||||
: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>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.container {
|
||||
max-width: 44rem;
|
||||
margin: var(--spacing-4xl) auto 0;
|
||||
gap: var(--spacing-l);
|
||||
max-width: 75rem;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
|
@ -37,4 +46,12 @@ const locale = useI18n();
|
|||
margin: 0;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.actionBox {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -45,11 +45,11 @@ watchEffect(() => {
|
|||
<template>
|
||||
<div v-if="availableMetrics.length > 0" :class="$style.metricsChartContainer">
|
||||
<div :class="$style.chartHeader">
|
||||
<N8nText>{{ locale.baseText('testDefinition.listRuns.metricsOverTime') }}</N8nText>
|
||||
<N8nSelect
|
||||
:model-value="selectedMetric"
|
||||
:class="$style.metricSelect"
|
||||
placeholder="Select metric"
|
||||
size="small"
|
||||
@update:model-value="emit('update:selectedMetric', $event)"
|
||||
>
|
||||
<N8nOption
|
||||
|
@ -59,6 +59,7 @@ watchEffect(() => {
|
|||
:value="metric"
|
||||
/>
|
||||
</N8nSelect>
|
||||
<N8nText>{{ locale.baseText('testDefinition.listRuns.metricsOverTime') }}</N8nText>
|
||||
</div>
|
||||
<div :class="$style.chartWrapper">
|
||||
<Line
|
||||
|
@ -74,17 +75,17 @@ watchEffect(() => {
|
|||
|
||||
<style lang="scss" module>
|
||||
.metricsChartContainer {
|
||||
margin: var(--spacing-m) 0;
|
||||
background: var(--color-background-xlight);
|
||||
border-radius: var(--border-radius-large);
|
||||
box-shadow: var(--box-shadow-base);
|
||||
|
||||
.chartHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
margin-bottom: var(--spacing-m);
|
||||
padding: var(--spacing-s);
|
||||
padding: var(--spacing-xs) var(--spacing-s);
|
||||
border-bottom: 1px solid var(--color-foreground-base);
|
||||
}
|
||||
|
||||
|
@ -100,7 +101,7 @@ watchEffect(() => {
|
|||
|
||||
.chartWrapper {
|
||||
position: relative;
|
||||
height: 400px;
|
||||
height: var(--metrics-chart-height, 400px);
|
||||
width: 100%;
|
||||
padding: var(--spacing-s);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import type { TestRunRecord } from '@/api/testDefinition.ee';
|
||||
import { computed, ref } from 'vue';
|
||||
import type { TestDefinitionTableColumn } from '../shared/TestDefinitionTable.vue';
|
||||
import TestDefinitionTable from '../shared/TestDefinitionTable.vue';
|
||||
import type { TestTableColumn } from '../shared/TestTableBase.vue';
|
||||
import TestTableBase from '../shared/TestTableBase.vue';
|
||||
import { convertToDisplayDate } from '@/utils/typesUtils';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
@ -29,7 +29,7 @@ const metrics = computed(() => {
|
|||
}, [] as string[]);
|
||||
});
|
||||
|
||||
const columns = computed((): Array<TestDefinitionTableColumn<TestRunRecord>> => {
|
||||
const columns = computed((): Array<TestTableColumn<TestRunRecord>> => {
|
||||
return [
|
||||
{
|
||||
prop: 'runNumber',
|
||||
|
@ -58,6 +58,8 @@ const columns = computed((): Array<TestDefinitionTableColumn<TestRunRecord>> =>
|
|||
label: locale.baseText('testDefinition.listRuns.runDate'),
|
||||
sortable: true,
|
||||
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) => ({
|
||||
|
@ -81,13 +83,16 @@ function deleteRuns() {
|
|||
|
||||
<template>
|
||||
<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
|
||||
v-show="selectedRows.length > 0"
|
||||
type="danger"
|
||||
:class="$style.activator"
|
||||
:size="'medium'"
|
||||
:icon="'trash'"
|
||||
size="medium"
|
||||
icon="trash"
|
||||
data-test-id="delete-runs-button"
|
||||
@click="deleteRuns"
|
||||
>
|
||||
|
@ -98,7 +103,7 @@ function deleteRuns() {
|
|||
}}
|
||||
</n8n-button>
|
||||
</div>
|
||||
<TestDefinitionTable
|
||||
<TestTableBase
|
||||
:data="runs"
|
||||
:columns="columns"
|
||||
selectable
|
||||
|
@ -116,5 +121,6 @@ function deleteRuns() {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -79,7 +79,7 @@ export function useMetricsChart(mode: AppliedThemeOption = 'light') {
|
|||
color: colors.text.primary,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
display: false,
|
||||
text: params.metric,
|
||||
padding: 16,
|
||||
color: colors.text.primary,
|
||||
|
@ -90,14 +90,11 @@ export function useMetricsChart(mode: AppliedThemeOption = 'light') {
|
|||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
maxRotation: 45,
|
||||
minRotation: 45,
|
||||
color: colors.text.primary,
|
||||
display: false,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: params.xTitle,
|
||||
padding: 16,
|
||||
padding: 1,
|
||||
color: colors.text.primary,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -26,7 +26,11 @@ export function useTestDefinitionForm() {
|
|||
tempValue: [],
|
||||
isEditing: false,
|
||||
},
|
||||
description: '',
|
||||
description: {
|
||||
value: '',
|
||||
tempValue: '',
|
||||
isEditing: false,
|
||||
},
|
||||
evaluationWorkflow: {
|
||||
mode: 'list',
|
||||
value: '',
|
||||
|
@ -45,9 +49,11 @@ export function useTestDefinitionForm() {
|
|||
const editableFields: ComputedRef<{
|
||||
name: EditableField<string>;
|
||||
tags: EditableField<string[]>;
|
||||
description: EditableField<string>;
|
||||
}> = computed(() => ({
|
||||
name: state.value.name,
|
||||
tags: state.value.tags,
|
||||
description: state.value.description,
|
||||
}));
|
||||
|
||||
/**
|
||||
|
@ -61,7 +67,11 @@ export function useTestDefinitionForm() {
|
|||
if (testDefinition) {
|
||||
const metrics = await evaluationsStore.fetchMetrics(testId);
|
||||
|
||||
state.value.description = testDefinition.description ?? '';
|
||||
state.value.description = {
|
||||
value: testDefinition.description ?? '',
|
||||
isEditing: false,
|
||||
tempValue: '',
|
||||
};
|
||||
state.value.name = {
|
||||
value: testDefinition.name ?? '',
|
||||
isEditing: false,
|
||||
|
@ -95,7 +105,7 @@ export function useTestDefinitionForm() {
|
|||
const params = {
|
||||
name: state.value.name.value,
|
||||
workflowId,
|
||||
description: state.value.description,
|
||||
description: state.value.description.value,
|
||||
};
|
||||
return await evaluationsStore.create(params);
|
||||
} finally {
|
||||
|
@ -125,8 +135,9 @@ export function useTestDefinitionForm() {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
isSaving.value = true;
|
||||
await Promise.all(promises);
|
||||
isSaving.value = false;
|
||||
};
|
||||
|
||||
const updateTest = async (testId: string) => {
|
||||
|
@ -142,7 +153,7 @@ export function useTestDefinitionForm() {
|
|||
|
||||
const params: UpdateTestDefinitionParams = {
|
||||
name: state.value.name.value,
|
||||
description: state.value.description,
|
||||
description: state.value.description.value,
|
||||
};
|
||||
|
||||
if (state.value.evaluationWorkflow.value) {
|
||||
|
@ -157,7 +168,8 @@ export function useTestDefinitionForm() {
|
|||
params.mockedNodes = state.value.mockedNodes;
|
||||
}
|
||||
|
||||
return await evaluationsStore.update({ ...params, id: testId });
|
||||
const response = await evaluationsStore.update({ ...params, id: testId });
|
||||
return response;
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script setup lang="ts" generic="T">
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import type { TestDefinitionTableColumn } from './TestDefinitionTable.vue';
|
||||
import type { TestTableColumn } from './TestTableBase.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
defineProps<{
|
||||
column: TestDefinitionTableColumn<T>;
|
||||
column: TestTableColumn<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;
|
||||
}
|
||||
|
||||
const getCellContent = (column: TestDefinitionTableColumn<T>, row: T) => {
|
||||
const getCellContent = (column: TestTableColumn<T>, row: T) => {
|
||||
if (column.formatter) {
|
||||
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', () => {
|
||||
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.tags.value).toEqual([]);
|
||||
expect(state.value.metrics).toEqual([]);
|
||||
|
@ -70,7 +70,7 @@ describe('useTestDefinitionForm', () => {
|
|||
expect(fetchSpy).toBeCalled();
|
||||
expect(fetchMetricsSpy).toBeCalledWith(TEST_DEF_A.id);
|
||||
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.evaluationWorkflow.value).toEqual(TEST_DEF_A.evaluationWorkflowId);
|
||||
expect(state.value.metrics).toEqual([
|
||||
|
@ -88,7 +88,7 @@ describe('useTestDefinitionForm', () => {
|
|||
await loadTestData('unknown-id');
|
||||
expect(fetchSpy).toBeCalled();
|
||||
// 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.tags.value).toEqual([]);
|
||||
expect(state.value.metrics).toEqual([]);
|
||||
|
@ -112,7 +112,7 @@ describe('useTestDefinitionForm', () => {
|
|||
const createSpy = vi.spyOn(useTestDefinitionStore(), 'create').mockResolvedValue(TEST_DEF_NEW);
|
||||
|
||||
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');
|
||||
expect(createSpy).toBeCalledWith({
|
||||
|
@ -143,7 +143,7 @@ describe('useTestDefinitionForm', () => {
|
|||
const updateSpy = vi.spyOn(useTestDefinitionStore(), 'update').mockResolvedValue(updatedBTest);
|
||||
|
||||
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);
|
||||
expect(updateSpy).toBeCalledWith({
|
||||
|
@ -166,7 +166,7 @@ describe('useTestDefinitionForm', () => {
|
|||
.mockRejectedValue(new Error('Update Failed'));
|
||||
|
||||
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');
|
||||
expect(updateSpy).toBeCalled();
|
||||
|
|
|
@ -10,10 +10,10 @@ export interface EditableField<T = string> {
|
|||
export interface EditableFormState {
|
||||
name: EditableField<string>;
|
||||
tags: EditableField<string[]>;
|
||||
description: EditableField<string>;
|
||||
}
|
||||
|
||||
export interface EvaluationFormState extends EditableFormState {
|
||||
description: string;
|
||||
evaluationWorkflow: INodeParameterResourceLocator;
|
||||
metrics: TestMetricRecord[];
|
||||
mockedNodes: Array<{ name: string; id: string }>;
|
||||
|
|
|
@ -2775,7 +2775,9 @@
|
|||
"communityPlusModal.button.confirm": "Send me a free license key",
|
||||
"executeWorkflowTrigger.createNewSubworkflow": "Create a sub-workflow in {projectName}",
|
||||
"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.namePlaceholder": "Enter test name",
|
||||
"testDefinition.edit.metricsTitle": "Metrics",
|
||||
|
@ -2790,14 +2792,15 @@
|
|||
"testDefinition.edit.workflowSelectorTitle": "Workflow to make comparisons",
|
||||
"testDefinition.edit.workflowSelectorHelpText": "This workflow will be called once for each test case.",
|
||||
"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.testSaveFailed": "Failed to save test",
|
||||
"testDefinition.edit.description": "Description",
|
||||
"testDefinition.edit.description.description": "Add details about what this test evaluates and what success looks like",
|
||||
"testDefinition.edit.tagName": "Tag name",
|
||||
"testDefinition.edit.step.intro": "When running a test",
|
||||
"testDefinition.edit.step.executions": "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.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.",
|
||||
|
@ -2814,11 +2817,17 @@
|
|||
"testDefinition.edit.pastRuns": "Past 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.saving": "Saving...",
|
||||
"testDefinition.edit.saved": "Changes saved",
|
||||
"testDefinition.list.testDeleted": "Test deleted",
|
||||
"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.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.lastRun": "Ran",
|
||||
"testDefinition.list.running": "Running",
|
||||
|
|
|
@ -53,6 +53,7 @@ import {
|
|||
faEnvelope,
|
||||
faEquals,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faExclamationTriangle,
|
||||
faExpand,
|
||||
faExpandAlt,
|
||||
|
@ -228,6 +229,7 @@ export const FontAwesomePlugin: Plugin = {
|
|||
addIcon(faEnvelope);
|
||||
addIcon(faEquals);
|
||||
addIcon(faEye);
|
||||
addIcon(faEyeSlash);
|
||||
addIcon(faExclamationTriangle);
|
||||
addIcon(faExclamationCircle);
|
||||
addIcon(faExpand);
|
||||
|
|
|
@ -36,6 +36,7 @@ const uiStore = useUIStore();
|
|||
const {
|
||||
state,
|
||||
fieldsIssues,
|
||||
isSaving,
|
||||
cancelEditing,
|
||||
loadTestData,
|
||||
createTest,
|
||||
|
@ -52,13 +53,15 @@ const allTags = computed(() => tagsStore.allTags);
|
|||
const tagsById = computed(() => tagsStore.tagsById);
|
||||
const testId = computed(() => props.testId ?? (route.params.testId as string));
|
||||
const currentWorkflowId = computed(() => route.params.name as string);
|
||||
|
||||
const appliedTheme = computed(() => uiStore.appliedTheme);
|
||||
const tagUsageCount = computed(
|
||||
() => tagsStore.tagsById[state.value.tags.value[0]]?.usageCount ?? 0,
|
||||
);
|
||||
|
||||
const hasRuns = computed(() => runs.value.length > 0);
|
||||
const nodePinningModal = ref<ModalState | null>(null);
|
||||
const modalContentWidth = ref(0);
|
||||
const showConfig = ref(true);
|
||||
const selectedMetric = ref<string>('');
|
||||
|
||||
onMounted(async () => {
|
||||
if (!testDefinitionStore.isFeatureEnabled) {
|
||||
|
@ -135,14 +138,18 @@ const runs = computed(() =>
|
|||
),
|
||||
);
|
||||
|
||||
async function onDeleteRuns(runs: TestRunRecord[]) {
|
||||
async function onDeleteRuns(toDelete: TestRunRecord[]) {
|
||||
await Promise.all(
|
||||
runs.map(async (run) => {
|
||||
toDelete.map(async (run) => {
|
||||
await testDefinitionStore.deleteTestRun({ testDefinitionId: testId.value, runId: run.id });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function toggleConfig() {
|
||||
showConfig.value = !showConfig.value;
|
||||
}
|
||||
|
||||
// Debounced watchers for auto-saving
|
||||
watch(
|
||||
() => state.value.metrics,
|
||||
|
@ -164,29 +171,82 @@ watch(
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<div :class="$style.formContent">
|
||||
<!-- Name -->
|
||||
<EvaluationHeader
|
||||
v-model="state.name"
|
||||
:class="{ 'has-issues': hasIssues('name') }"
|
||||
:start-editing="startEditing"
|
||||
:save-changes="saveChanges"
|
||||
:handle-keydown="handleKeydown"
|
||||
/>
|
||||
<div :class="$style.panelBlock">
|
||||
<!-- Description -->
|
||||
<EvaluationStep
|
||||
:class="$style.step"
|
||||
:title="locale.baseText('testDefinition.edit.description')"
|
||||
:expanded="false"
|
||||
>
|
||||
<template #icon><font-awesome-icon icon="thumbtack" size="lg" /></template>
|
||||
<template #cardContent>
|
||||
<DescriptionInput v-model="state.description" />
|
||||
</template>
|
||||
</EvaluationStep>
|
||||
<div :class="[$style.container, { [$style.noRuns]: !hasRuns }]">
|
||||
<div :class="$style.headerSection">
|
||||
<div :class="$style.headerMeta">
|
||||
<div :class="$style.name">
|
||||
<EvaluationHeader
|
||||
v-model="state.name"
|
||||
:class="{ 'has-issues': hasIssues('name') }"
|
||||
:start-editing="startEditing"
|
||||
:save-changes="saveChanges"
|
||||
:handle-keydown="handleKeydown"
|
||||
/>
|
||||
<div :class="$style.lastSaved">
|
||||
<template v-if="isSaving">
|
||||
{{ locale.baseText('testDefinition.edit.saving') }}
|
||||
</template>
|
||||
<template v-else> {{ locale.baseText('testDefinition.edit.saved') }} </template>
|
||||
</div>
|
||||
</div>
|
||||
<DescriptionInput
|
||||
v-model="state.description"
|
||||
:start-editing="startEditing"
|
||||
: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">
|
||||
{{ locale.baseText('testDefinition.edit.step.intro') }}
|
||||
</div>
|
||||
|
@ -286,37 +346,6 @@ watch(
|
|||
</template>
|
||||
</EvaluationStep>
|
||||
</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>
|
||||
|
||||
<Modal ref="nodePinningModal" width="80vw" height="85vh" :name="NODE_PINNING_MODAL_KEY">
|
||||
|
@ -338,52 +367,102 @@ watch(
|
|||
|
||||
<style module lang="scss">
|
||||
.container {
|
||||
--evaluation-edit-panel-width: 35rem;
|
||||
width: 100%;
|
||||
--evaluation-edit-panel-width: 24rem;
|
||||
--metrics-chart-height: 10rem;
|
||||
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;
|
||||
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;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
|
||||
.noRuns & {
|
||||
justify-content: center;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
.runsTable {
|
||||
flex-shrink: 1;
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
|
||||
.headerSection {
|
||||
display: flex;
|
||||
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;
|
||||
|
||||
@media (min-height: 56rem) {
|
||||
margin-top: var(--spacing-2xl);
|
||||
}
|
||||
}
|
||||
.runsTableHeading {
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.panelBlock {
|
||||
max-width: var(--evaluation-edit-panel-width, 35rem);
|
||||
width: var(--evaluation-edit-panel-width);
|
||||
display: grid;
|
||||
height: 100%;
|
||||
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 {
|
||||
font-size: var(--font-size-m);
|
||||
color: var(--color-text-dark);
|
||||
margin-top: var(--spacing-s);
|
||||
|
||||
justify-self: center;
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.step {
|
||||
position: relative;
|
||||
|
||||
|
@ -391,10 +470,13 @@ watch(
|
|||
margin-top: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
.introArrow {
|
||||
--arrow-height: 1.5rem;
|
||||
margin-bottom: -1rem;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.evaluationArrows {
|
||||
--arrow-height: 22rem;
|
||||
display: flex;
|
||||
|
@ -405,38 +487,9 @@ watch(
|
|||
margin-bottom: -100%;
|
||||
z-index: 0;
|
||||
}
|
||||
.footer {
|
||||
margin-top: var(--spacing-xl);
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.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);
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -198,10 +198,10 @@ onMounted(() => {
|
|||
</template>
|
||||
<style module lang="scss">
|
||||
.container {
|
||||
padding: var(--spacing-xl) var(--spacing-l);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: var(--content-container-width);
|
||||
margin: auto;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
|
|
|
@ -21,5 +21,16 @@ onMounted(initWorkflow);
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<router-view />
|
||||
<div :class="$style.evaluationsView">
|
||||
<router-view />
|
||||
</div>
|
||||
</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 { useI18n } from '@/composables/useI18n';
|
||||
import { N8nCard, N8nText } from 'n8n-design-system';
|
||||
import TestDefinitionTable from '@/components/TestDefinition/shared/TestDefinitionTable.vue';
|
||||
import type { TestDefinitionTableColumn } from '@/components/TestDefinition/shared/TestDefinitionTable.vue';
|
||||
import TestTableBase from '@/components/TestDefinition/shared/TestTableBase.vue';
|
||||
import type { TestTableColumn } from '@/components/TestDefinition/shared/TestTableBase.vue';
|
||||
import { useExecutionsStore } from '@/stores/executions.store';
|
||||
import { get } from 'lodash-es';
|
||||
import type { ExecutionSummaryWithScopes } from '@/Interface';
|
||||
|
@ -36,7 +36,7 @@ const filteredTestCases = computed(() => {
|
|||
});
|
||||
|
||||
const columns = computed(
|
||||
(): Array<TestDefinitionTableColumn<TestCase>> => [
|
||||
(): Array<TestTableColumn<TestCase>> => [
|
||||
{
|
||||
prop: 'id',
|
||||
width: 200,
|
||||
|
@ -193,7 +193,7 @@ onMounted(async () => {
|
|||
<div v-if="isLoading" :class="$style.loading">
|
||||
<n8n-loading :loading="true" :rows="5" />
|
||||
</div>
|
||||
<TestDefinitionTable
|
||||
<TestTableBase
|
||||
v-else
|
||||
:data="filteredTestCases"
|
||||
:columns="columns"
|
||||
|
@ -205,10 +205,10 @@ onMounted(async () => {
|
|||
|
||||
<style module lang="scss">
|
||||
.container {
|
||||
padding: var(--spacing-xl) var(--spacing-l);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: var(--content-container-width);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.backButton {
|
||||
|
|
|
@ -98,10 +98,10 @@ onMounted(async () => {
|
|||
<N8nLoading :rows="5" />
|
||||
<N8nLoading :rows="10" />
|
||||
</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" />
|
||||
<TestRunsTable :runs="runs" @get-run-detail="getRunDetail" @delete-runs="onDeleteRuns" />
|
||||
</template>
|
||||
</div>
|
||||
<template v-else>
|
||||
<N8nActionBox
|
||||
:heading="locale.baseText('testDefinition.listRuns.noRuns')"
|
||||
|
@ -114,16 +114,24 @@ onMounted(async () => {
|
|||
</template>
|
||||
<style module lang="scss">
|
||||
.container {
|
||||
padding: var(--spacing-xl) var(--spacing-l);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: var(--content-container-width);
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.backButton {
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
.description {
|
||||
margin-top: var(--spacing-xs);
|
||||
margin: var(--spacing-s) 0;
|
||||
display: block;
|
||||
}
|
||||
.details {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -75,7 +75,7 @@ describe('TestDefinitionEditView', () => {
|
|||
vi.mocked(useTestDefinitionForm).mockReturnValue({
|
||||
state: ref({
|
||||
name: { value: '', isEditing: false, tempValue: '' },
|
||||
description: '',
|
||||
description: { value: '', isEditing: false, tempValue: '' },
|
||||
tags: { value: [], tempValue: [], isEditing: false },
|
||||
evaluationWorkflow: { mode: 'list', value: '', __rl: true },
|
||||
metrics: [],
|
||||
|
@ -273,7 +273,7 @@ describe('TestDefinitionEditView', () => {
|
|||
...vi.mocked(useTestDefinitionForm)(),
|
||||
state: ref({
|
||||
name: { value: 'Test', isEditing: false, tempValue: '' },
|
||||
description: '',
|
||||
description: { value: '', isEditing: false, tempValue: '' },
|
||||
tags: { value: ['tag1'], tempValue: [], isEditing: false },
|
||||
evaluationWorkflow: { mode: 'list', value: 'workflow1', __rl: true },
|
||||
metrics: [],
|
||||
|
|
Loading…
Reference in a new issue