mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-15 00:54:06 -08:00
Refactor workflow evaluation list view and add new components
• Split EmptyState into separate component • Create TestItem and TestsList components • Update EvaluationListView with new components • Add WORKFLOW_EVALUATION_EDIT to constants • Improve styles and layout of test items
This commit is contained in:
parent
fd94fe3ce4
commit
7adfbd236c
|
@ -0,0 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
defineEmits(['create-test']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div :class="$style.header">
|
||||
<h1>Tests</h1>
|
||||
<n8n-button type="primary" label="Create new test" @click="$emit('create-test')" />
|
||||
</div>
|
||||
<n8n-action-box
|
||||
heading="Get confidence your workflow is working as expected"
|
||||
description="Tests run your workflow and compare the results to expected ones. Create your first test from a past execution. More info"
|
||||
button-text="Choose Execution(s)"
|
||||
@click:button="$emit('create-test')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,135 @@
|
|||
<script setup lang="ts">
|
||||
import type { TestListItem } from '@/components/WorkflowEvaluation/types';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import n8nIconButton from 'n8n-design-system/components/N8nIconButton';
|
||||
|
||||
export interface TestItemProps {
|
||||
test: TestListItem;
|
||||
}
|
||||
|
||||
const props = defineProps<TestItemProps>();
|
||||
const locale = useI18n();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'run-test': [testId: number];
|
||||
'view-details': [testId: number];
|
||||
'edit-test': [testId: number];
|
||||
'delete-test': [testId: number];
|
||||
}>();
|
||||
|
||||
const actions = [
|
||||
{
|
||||
icon: 'play',
|
||||
event: () => emit('run-test', props.test.id),
|
||||
tooltip: locale.baseText('workflowEvaluation.runTest'),
|
||||
},
|
||||
{
|
||||
icon: 'list',
|
||||
event: () => emit('view-details', props.test.id),
|
||||
tooltip: locale.baseText('workflowEvaluation.viewDetails'),
|
||||
},
|
||||
{
|
||||
icon: 'pen',
|
||||
event: () => emit('edit-test', props.test.id),
|
||||
tooltip: locale.baseText('workflowEvaluation.editTest'),
|
||||
},
|
||||
{
|
||||
icon: 'trash',
|
||||
event: () => emit('delete-test', props.test.id),
|
||||
tooltip: locale.baseText('workflowEvaluation.deleteTest'),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.testItem" @click="$emit('view-details', test.id)">
|
||||
<div :class="$style.testInfo">
|
||||
<div :class="$style.testName">
|
||||
{{ test.name }}
|
||||
<n8n-tag v-if="test.tagName" :text="test.tagName" />
|
||||
</div>
|
||||
<div :class="$style.testCases">
|
||||
{{ test.testCases }} test case(s)
|
||||
<n8n-loading v-if="!test.execution.lastRun" :loading="true" :rows="1" />
|
||||
<span v-else>Ran {{ test.execution.lastRun }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.metrics">
|
||||
<div :class="$style.metric">Error rate: {{ test.execution.errorRate ?? '-' }}</div>
|
||||
<div v-for="(value, key) in test.execution.metrics" :key="key" :class="$style.metric">
|
||||
{{ key }}: {{ value ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.actions">
|
||||
<n8n-tooltip v-for="action in actions" :key="action.icon" placement="top">
|
||||
<template #content>
|
||||
{{ action.tooltip }}
|
||||
</template>
|
||||
<component
|
||||
:is="n8nIconButton"
|
||||
:icon="action.icon"
|
||||
type="tertiary"
|
||||
size="small"
|
||||
@click.stop="action.event"
|
||||
/>
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.testItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
border: 1px solid var(--color-foreground-base);
|
||||
border-radius: var(--border-radius-base);
|
||||
background-color: var(--color-background-light);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-base);
|
||||
}
|
||||
}
|
||||
|
||||
.testInfo {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.testName {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2xs);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--spacing-4xs);
|
||||
}
|
||||
|
||||
.testCases {
|
||||
color: var(--color-text-base);
|
||||
font-size: var(--font-size-2xs);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
margin: 0 var(--spacing-l);
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: var(--font-size-2xs);
|
||||
color: var(--color-text-dark);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
--color-button-secondary-font: var(--color-callout-info-icon);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,35 @@
|
|||
<script setup lang="ts">
|
||||
import TestItem from './TestItem.vue';
|
||||
import type { TestListItem } from '@/components/WorkflowEvaluation/types';
|
||||
|
||||
export interface TestListProps {
|
||||
tests: TestListItem[];
|
||||
}
|
||||
defineProps<TestListProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.testsList">
|
||||
<div :class="$style.testsHeader">
|
||||
<n8n-button
|
||||
size="small"
|
||||
type="tertiary"
|
||||
label="Create new test"
|
||||
@click="$emit('create-test')"
|
||||
/>
|
||||
</div>
|
||||
<TestItem v-for="test in tests" :key="test.id" :test="test" v-bind="$attrs" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.testsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.testsHeader {
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
</style>
|
|
@ -485,6 +485,7 @@ export const enum VIEWS {
|
|||
WORKFLOWS = 'WorkflowsView',
|
||||
WORKFLOW_EXECUTIONS = 'WorkflowExecutions',
|
||||
WORKFLOW_EVALUATION = 'WorkflowEvaluation',
|
||||
WORKFLOW_EVALUATION_EDIT = 'WorkflowEvaluationEdit',
|
||||
NEW_WORKFLOW_EVALUATION = 'NewWorkflowEvaluation',
|
||||
USAGE = 'Usage',
|
||||
LOG_STREAMING_SETTINGS = 'LogStreamingSettingsView',
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { useEvaluationsStore } from '@/stores/evaluations.store.ee';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import EmptyState from '@/components/WorkflowEvaluation/ListEvaluation/EmptyState.vue';
|
||||
import TestsList from '@/components/WorkflowEvaluation/ListEvaluation/TestsList.vue';
|
||||
import type { TestExecution, TestListItem } from '@/components/WorkflowEvaluation/types';
|
||||
|
||||
const router = useRouter();
|
||||
const evaluationsStore = useEvaluationsStore();
|
||||
const isLoading = ref(false);
|
||||
const toast = useToast();
|
||||
const locale = useI18n();
|
||||
|
||||
const tests = computed<TestListItem[]>(() => {
|
||||
return evaluationsStore.allTestDefinitions.map((test) => ({
|
||||
id: test.id,
|
||||
name: test.name,
|
||||
tagName: test.annotationTagId ? getTagName(test.annotationTagId) : '',
|
||||
testCases: 0, // This should come from the API
|
||||
execution: getTestExecution(test.id),
|
||||
}));
|
||||
});
|
||||
const hasTests = computed(() => tests.value.length > 0);
|
||||
|
||||
// Mock function to get tag name - replace with actual tag lookup
|
||||
function getTagName(tagId: string) {
|
||||
const tags = {
|
||||
tag1: 'marketing',
|
||||
tag2: 'SupportOps',
|
||||
};
|
||||
return tags[tagId] || '';
|
||||
}
|
||||
|
||||
// Mock function to get test execution data - replace with actual API call
|
||||
function getTestExecution(testId: number): TestExecution {
|
||||
console.log('🚀 ~ getTestExecution ~ testId:', testId);
|
||||
// Mock data - replace with actual data from your API
|
||||
const mockExecutions = {
|
||||
12: {
|
||||
lastRun: 'an hour ago',
|
||||
errorRate: 0,
|
||||
metrics: { metric1: 0.12, metric2: 0.99, metric3: 0.87 },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
mockExecutions[12] || {
|
||||
lastRun: null,
|
||||
errorRate: null,
|
||||
metrics: { metric1: null, metric2: null, metric3: null },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Action handlers
|
||||
function onCreateTest() {
|
||||
void router.push({ name: VIEWS.NEW_WORKFLOW_EVALUATION });
|
||||
}
|
||||
|
||||
function onRunTest(testId: number) {
|
||||
console.log('Running test:', testId);
|
||||
// Implement test run logic
|
||||
}
|
||||
|
||||
function onViewDetails(testId: number) {
|
||||
console.log('Viewing details for test:', testId);
|
||||
void router.push({ name: VIEWS.WORKFLOW_EVALUATION_EDIT, params: { testId } });
|
||||
// Implement navigation to test details
|
||||
}
|
||||
|
||||
function onEditTest(testId: number) {
|
||||
console.log('Editing test:', testId);
|
||||
void router.push({ name: VIEWS.WORKFLOW_EVALUATION_EDIT, params: { testId } });
|
||||
// Implement edit navigation
|
||||
}
|
||||
|
||||
async function onDeleteTest(testId: number) {
|
||||
console.log('Deleting test:', testId);
|
||||
// Implement delete logic
|
||||
await evaluationsStore.deleteById(testId);
|
||||
|
||||
toast.showMessage({
|
||||
title: locale.baseText('generic.deleted'),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
async function loadTests() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await evaluationsStore.fetchAll();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Load tests on mount
|
||||
void loadTests();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<div v-if="isLoading" :class="$style.loading">
|
||||
<n8n-loading :loading="true" :rows="3" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<EmptyState v-if="!hasTests" @create-test="onCreateTest" />
|
||||
<TestsList
|
||||
v-else
|
||||
:tests="tests"
|
||||
@create-test="onCreateTest"
|
||||
@run-test="onRunTest"
|
||||
@view-details="onViewDetails"
|
||||
@edit-test="onEditTest"
|
||||
@delete-test="onDeleteTest"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<style module lang="scss">
|
||||
.container {
|
||||
padding: var(--spacing-xl) var(--spacing-l);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: var(--content-container-width);
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
}
|
||||
</style>
|
|
@ -1,271 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { useEvaluationsStore } from '@/stores/evaluations.store.ee';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
interface TestMetrics {
|
||||
metric1: number | null;
|
||||
metric2: number | null;
|
||||
metric3: number | null;
|
||||
}
|
||||
|
||||
interface TestExecution {
|
||||
lastRun: string | null;
|
||||
errorRate: number | null;
|
||||
metrics: TestMetrics;
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
const evaluationsStore = useEvaluationsStore();
|
||||
const isLoading = ref(false);
|
||||
const toast = useToast();
|
||||
const locale = useI18n();
|
||||
|
||||
// Computed properties for test data
|
||||
const tests = computed(() => {
|
||||
return evaluationsStore.allTestDefinitions.map((test) => ({
|
||||
id: test.id,
|
||||
name: test.name,
|
||||
tagName: test.annotationTagId ? getTagName(test.annotationTagId) : '',
|
||||
testCases: 0, // This should come from the API
|
||||
execution: getTestExecution(test.id),
|
||||
}));
|
||||
});
|
||||
|
||||
const hasTests = computed(() => tests.value.length > 0);
|
||||
|
||||
// Mock function to get tag name - replace with actual tag lookup
|
||||
function getTagName(tagId: string) {
|
||||
const tags = {
|
||||
tag1: 'marketing',
|
||||
tag2: 'SupportOps',
|
||||
};
|
||||
return tags[tagId] || '';
|
||||
}
|
||||
|
||||
// Mock function to get test execution data - replace with actual API call
|
||||
function getTestExecution(testId: number): TestExecution {
|
||||
// Mock data - replace with actual data from your API
|
||||
const mockExecutions = {
|
||||
12: {
|
||||
lastRun: 'an hour ago',
|
||||
errorRate: 0,
|
||||
metrics: { metric1: 0.12, metric2: 0.99, metric3: 0.87 },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
mockExecutions[testId] || {
|
||||
lastRun: null,
|
||||
errorRate: null,
|
||||
metrics: { metric1: null, metric2: null, metric3: null },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Action handlers
|
||||
function onCreateTest() {
|
||||
void router.push({ name: VIEWS.NEW_WORKFLOW_EVALUATION });
|
||||
}
|
||||
|
||||
function onRunTest(testId: number) {
|
||||
console.log('Running test:', testId);
|
||||
// Implement test run logic
|
||||
}
|
||||
|
||||
function onViewDetails(testId: number) {
|
||||
console.log('Viewing details for test:', testId);
|
||||
// Implement navigation to test details
|
||||
}
|
||||
|
||||
function onEditTest(testId: number) {
|
||||
console.log('Editing test:', testId);
|
||||
// Implement edit navigation
|
||||
}
|
||||
|
||||
async function onDeleteTest(testId: number) {
|
||||
console.log('Deleting test:', testId);
|
||||
// Implement delete logic
|
||||
await evaluationsStore.deleteById(testId);
|
||||
|
||||
toast.showMessage({
|
||||
title: locale.baseText('generic.deleted'),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
async function loadTests() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await evaluationsStore.fetchAll();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Load tests on mount
|
||||
void loadTests();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<div v-if="isLoading" :class="$style.loading">
|
||||
<n8n-loading :loading="true" :rows="3" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- Empty State -->
|
||||
<template v-if="!hasTests">
|
||||
<div :class="$style.header">
|
||||
<h1>Tests</h1>
|
||||
<n8n-button type="primary" label="Create new test" @click="onCreateTest" />
|
||||
</div>
|
||||
<n8n-action-box
|
||||
v-if="!hasTests"
|
||||
heading="Get confidence your workflow is working as expected"
|
||||
description="Tests run your workflow and compare the results to expected ones. Create your first test from a past execution. More info"
|
||||
button-text="Choose Execution(s)"
|
||||
@click:button="onCreateTest"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Tests List -->
|
||||
<div v-else :class="$style.testsList">
|
||||
<div :class="$style.testsHeader">
|
||||
<n8n-button size="small" type="tertiary" label="Create new test" @click="onCreateTest" />
|
||||
</div>
|
||||
|
||||
<!-- Test Items -->
|
||||
<div v-for="test in tests" :key="test.id" :class="$style.testItem">
|
||||
<div :class="$style.testInfo">
|
||||
<div :class="$style.testName">
|
||||
{{ test.name }}
|
||||
<n8n-tag v-if="test.tagName" :text="test.tagName" />
|
||||
</div>
|
||||
<div :class="$style.testCases">
|
||||
{{ test.testCases }} test case(s)
|
||||
<n8n-loading v-if="!test.execution.lastRun" :loading="true" :rows="1" />
|
||||
<span v-else>Ran {{ test.execution.lastRun }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.metrics">
|
||||
<div :class="$style.metric">Error rate: {{ test.execution.errorRate ?? '-' }}</div>
|
||||
<div :class="$style.metric">Metric 1: {{ test.execution.metrics.metric1 ?? '-' }}</div>
|
||||
<div :class="$style.metric">Metric 2: {{ test.execution.metrics.metric2 ?? '-' }}</div>
|
||||
<div :class="$style.metric">Metric 3: {{ test.execution.metrics.metric3 ?? '-' }}</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.actions">
|
||||
<n8n-icon-button icon="play" type="tertiary" size="small" @click="onRunTest(test.id)" />
|
||||
<n8n-icon-button
|
||||
icon="list"
|
||||
type="tertiary"
|
||||
size="small"
|
||||
@click="onViewDetails(test.id)"
|
||||
/>
|
||||
<n8n-icon-button icon="pen" type="tertiary" size="small" @click="onEditTest(test.id)" />
|
||||
<n8n-icon-button
|
||||
icon="trash"
|
||||
type="tertiary"
|
||||
size="small"
|
||||
@click="onDeleteTest(test.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.container {
|
||||
padding: var(--spacing-xl) var(--spacing-l);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: var(--content-container-width);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.testsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.testsHeader {
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
|
||||
.testItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
border: 1px solid var(--color-foreground-base);
|
||||
border-radius: var(--border-radius-base);
|
||||
background-color: var(--color-background-light);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-base);
|
||||
}
|
||||
}
|
||||
|
||||
.testInfo {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.testName {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2xs);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--spacing-4xs);
|
||||
}
|
||||
|
||||
.testCases {
|
||||
color: var(--color-text-base);
|
||||
font-size: var(--font-size-2xs);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
margin: 0 var(--spacing-l);
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: var(--font-size-2xs);
|
||||
color: var(--color-text-base);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-4xs);
|
||||
}
|
||||
</style>
|
Loading…
Reference in a new issue