mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -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',
|
WORKFLOWS = 'WorkflowsView',
|
||||||
WORKFLOW_EXECUTIONS = 'WorkflowExecutions',
|
WORKFLOW_EXECUTIONS = 'WorkflowExecutions',
|
||||||
WORKFLOW_EVALUATION = 'WorkflowEvaluation',
|
WORKFLOW_EVALUATION = 'WorkflowEvaluation',
|
||||||
|
WORKFLOW_EVALUATION_EDIT = 'WorkflowEvaluationEdit',
|
||||||
NEW_WORKFLOW_EVALUATION = 'NewWorkflowEvaluation',
|
NEW_WORKFLOW_EVALUATION = 'NewWorkflowEvaluation',
|
||||||
USAGE = 'Usage',
|
USAGE = 'Usage',
|
||||||
LOG_STREAMING_SETTINGS = 'LogStreamingSettingsView',
|
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