mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor-ui): Add test metrics management and simplify tags input
- Add API endpoints for test metrics CRUD operations - Implement metrics input component with add/delete functionality - Simplify tags input component to use single tag selection
This commit is contained in:
parent
5e97cfbcd3
commit
b247103e38
|
@ -35,6 +35,7 @@ export interface UpdateTestResponse {
|
|||
}
|
||||
|
||||
const endpoint = '/evaluation/test-definitions';
|
||||
const getMetricsEndpoint = (testDefinitionId: string) => `${endpoint}/${testDefinitionId}/metrics`;
|
||||
|
||||
export async function getTestDefinitions(context: IRestApiContext) {
|
||||
return await makeRestApiRequest<{ count: number; testDefinitions: TestDefinitionRecord[] }>(
|
||||
|
@ -71,3 +72,84 @@ export async function updateTestDefinition(
|
|||
export async function deleteTestDefinition(context: IRestApiContext, id: string) {
|
||||
return await makeRestApiRequest<{ success: boolean }>(context, 'DELETE', `${endpoint}/${id}`);
|
||||
}
|
||||
|
||||
// Metrics
|
||||
|
||||
export interface TestMetricRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
testDefinitionId: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface CreateTestMetricParams {
|
||||
testDefinitionId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface UpdateTestMetricParams {
|
||||
name: string;
|
||||
id: string;
|
||||
testDefinitionId: string;
|
||||
}
|
||||
|
||||
export interface DeleteTestMetricParams {
|
||||
testDefinitionId: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const getTestMetrics = async (context: IRestApiContext, testDefinitionId: string) => {
|
||||
return await makeRestApiRequest<TestMetricRecord[]>(
|
||||
context,
|
||||
'GET',
|
||||
getMetricsEndpoint(testDefinitionId),
|
||||
);
|
||||
};
|
||||
|
||||
export const getTestMetric = async (
|
||||
context: IRestApiContext,
|
||||
testDefinitionId: string,
|
||||
id: string,
|
||||
) => {
|
||||
return await makeRestApiRequest<TestMetricRecord>(
|
||||
context,
|
||||
'GET',
|
||||
`${getMetricsEndpoint(testDefinitionId)}/${id}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const createTestMetric = async (
|
||||
context: IRestApiContext,
|
||||
params: CreateTestMetricParams,
|
||||
) => {
|
||||
return await makeRestApiRequest<TestMetricRecord>(
|
||||
context,
|
||||
'POST',
|
||||
getMetricsEndpoint(params.testDefinitionId),
|
||||
{ name: params.name },
|
||||
);
|
||||
};
|
||||
|
||||
export const updateTestMetric = async (
|
||||
context: IRestApiContext,
|
||||
params: UpdateTestMetricParams,
|
||||
) => {
|
||||
return await makeRestApiRequest<TestMetricRecord>(
|
||||
context,
|
||||
'PATCH',
|
||||
`${getMetricsEndpoint(params.testDefinitionId)}/${params.id}`,
|
||||
{ name: params.name },
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteTestMetric = async (
|
||||
context: IRestApiContext,
|
||||
params: DeleteTestMetricParams,
|
||||
) => {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'DELETE',
|
||||
`${getMetricsEndpoint(params.testDefinitionId)}/${params.id}`,
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,22 +1,30 @@
|
|||
<script setup lang="ts">
|
||||
import type { TestMetricRecord } from '@/api/testDefinition.ee';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
export interface MetricsInputProps {
|
||||
modelValue: string[];
|
||||
modelValue: Array<Partial<TestMetricRecord>>;
|
||||
}
|
||||
const props = defineProps<MetricsInputProps>();
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: MetricsInputProps['modelValue']] }>();
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: MetricsInputProps['modelValue']];
|
||||
deleteMetric: [metric: Partial<TestMetricRecord>];
|
||||
}>();
|
||||
const locale = useI18n();
|
||||
|
||||
function addNewMetric() {
|
||||
emit('update:modelValue', [...props.modelValue, '']);
|
||||
emit('update:modelValue', [...props.modelValue, { name: '' }]);
|
||||
}
|
||||
|
||||
function updateMetric(index: number, value: string) {
|
||||
function updateMetric(index: number, name: string) {
|
||||
const newMetrics = [...props.modelValue];
|
||||
newMetrics[index] = value;
|
||||
newMetrics[index].name = name;
|
||||
emit('update:modelValue', newMetrics);
|
||||
}
|
||||
|
||||
function onDeleteMetric(metric: Partial<TestMetricRecord>) {
|
||||
emit('deleteMetric', metric);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -27,14 +35,26 @@ function updateMetric(index: number, value: string) {
|
|||
:class="$style.metricField"
|
||||
>
|
||||
<div :class="$style.metricsContainer">
|
||||
<div v-for="(metric, index) in modelValue" :key="index">
|
||||
<div v-for="(metric, index) in modelValue" :key="index" :class="$style.metricItem">
|
||||
<N8nInput
|
||||
:ref="`metric_${index}`"
|
||||
data-test-id="evaluation-metric-item"
|
||||
:model-value="metric"
|
||||
:model-value="metric.name"
|
||||
:placeholder="locale.baseText('testDefinition.edit.metricsPlaceholder')"
|
||||
@update:model-value="(value: string) => updateMetric(index, value)"
|
||||
/>
|
||||
<n8n-icon-button
|
||||
:class="{ [$style.sendButton]: true }"
|
||||
icon="trash"
|
||||
type="text"
|
||||
@click="onDeleteMetric(metric)"
|
||||
/>
|
||||
<!-- <n8n-button
|
||||
type="tertiary"
|
||||
:label="locale.baseText('testDefinition.edit.deleteMetric')"
|
||||
:class="$style.newMetricButton"
|
||||
@click="onDeleteMetric(metric)"
|
||||
/> -->
|
||||
</div>
|
||||
<n8n-button
|
||||
type="tertiary"
|
||||
|
@ -54,6 +74,11 @@ function updateMetric(index: number, value: string) {
|
|||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.metricItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metricField {
|
||||
width: 100%;
|
||||
margin-top: var(--spacing-xs);
|
||||
|
|
|
@ -1,89 +1,32 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import type { ITag } from '@/Interface';
|
||||
import { createEventBus } from 'n8n-design-system';
|
||||
import { computed } from 'vue';
|
||||
|
||||
export interface TagsInputProps {
|
||||
modelValue?: {
|
||||
isEditing: boolean;
|
||||
appliedTagIds: string[];
|
||||
};
|
||||
allTags: ITag[];
|
||||
tagsById: Record<string, ITag>;
|
||||
isLoading: boolean;
|
||||
startEditing: (field: string) => void;
|
||||
saveChanges: (field: string) => void;
|
||||
cancelEditing: (field: string) => void;
|
||||
selectedTags: ITag[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TagsInputProps>(), {
|
||||
modelValue: () => ({
|
||||
isEditing: false,
|
||||
appliedTagIds: [],
|
||||
}),
|
||||
selectedTags: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: TagsInputProps['modelValue']] }>();
|
||||
|
||||
const locale = useI18n();
|
||||
const tagsEventBus = createEventBus();
|
||||
const getTagName = computed(() => (tagId: string) => {
|
||||
return props.tagsById[tagId]?.name ?? '';
|
||||
});
|
||||
|
||||
function updateTags(tags: string[]) {
|
||||
const newTags = tags[0] ? [tags[0]] : [];
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
appliedTagIds: newTags,
|
||||
});
|
||||
}
|
||||
const emit = defineEmits<{
|
||||
addTag: [];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-test-id="workflow-tags-field">
|
||||
<n8n-input-label
|
||||
:label="locale.baseText('testDefinition.edit.tagName')"
|
||||
:bold="false"
|
||||
size="small"
|
||||
>
|
||||
<div v-if="!modelValue.isEditing" :class="$style.tagsRead" @click="startEditing('tags')">
|
||||
<n8n-text v-if="modelValue.appliedTagIds.length === 0" size="small">
|
||||
{{ locale.baseText('testDefinition.edit.selectTag') }}
|
||||
</n8n-text>
|
||||
<n8n-tag
|
||||
v-for="tagId in modelValue.appliedTagIds"
|
||||
:key="tagId"
|
||||
:text="getTagName(tagId)"
|
||||
data-test-id="evaluation-tag-field"
|
||||
/>
|
||||
<n8n-icon-button
|
||||
:class="$style.editInputButton"
|
||||
icon="pen"
|
||||
type="tertiary"
|
||||
size="small"
|
||||
transparent
|
||||
/>
|
||||
<n8n-input-label :bold="false" size="small">
|
||||
<div :class="$style.tagsRead">
|
||||
Use all executions that are tagged with {{ selectedTags[0]?.name }}
|
||||
<br />
|
||||
<br />
|
||||
<n8n-button type="primary" size="small" transparent @click="emit('addTag')"
|
||||
>Select Executions</n8n-button
|
||||
>
|
||||
</div>
|
||||
<TagsDropdown
|
||||
v-else
|
||||
:model-value="modelValue.appliedTagIds"
|
||||
:placeholder="locale.baseText('executionAnnotationView.chooseOrCreateATag')"
|
||||
:create-enabled="false"
|
||||
:all-tags="allTags"
|
||||
:is-loading="isLoading"
|
||||
:tags-by-id="tagsById"
|
||||
data-test-id="workflow-tags-dropdown"
|
||||
:event-bus="tagsEventBus"
|
||||
@update:model-value="updateTags"
|
||||
@esc="cancelEditing('tags')"
|
||||
@blur="saveChanges('tags')"
|
||||
/>
|
||||
</n8n-input-label>
|
||||
<n8n-text size="small" color="text-light">{{
|
||||
locale.baseText('testDefinition.edit.tagsHelpText')
|
||||
}}</n8n-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -4,7 +4,10 @@ import type { INodeParameterResourceLocator } from 'n8n-workflow';
|
|||
import { useTestDefinitionStore } from '@/stores/testDefinition.store.ee';
|
||||
import type AnnotationTagsDropdownEe from '@/components/AnnotationTagsDropdown.ee.vue';
|
||||
import type { N8nInput } from 'n8n-design-system';
|
||||
import type { UpdateTestDefinitionParams } from '@/api/testDefinition.ee';
|
||||
import type { TestMetricRecord, UpdateTestDefinitionParams } from '@/api/testDefinition.ee';
|
||||
import type { ITag } from '@/Interface';
|
||||
import { useAnnotationTagsStore } from '@/stores/tags.store';
|
||||
// import type { TestMetricRecord } from '@/api/testMetric.ee';
|
||||
|
||||
interface EditableField {
|
||||
value: string;
|
||||
|
@ -15,12 +18,9 @@ interface EditableField {
|
|||
export interface IEvaluationFormState {
|
||||
name: EditableField;
|
||||
description: string;
|
||||
tags: {
|
||||
isEditing: boolean;
|
||||
appliedTagIds: string[];
|
||||
};
|
||||
tags: ITag[];
|
||||
evaluationWorkflow: INodeParameterResourceLocator;
|
||||
metrics: string[];
|
||||
metrics: TestMetricRecord[];
|
||||
}
|
||||
|
||||
type FormRefs = {
|
||||
|
@ -31,25 +31,23 @@ type FormRefs = {
|
|||
export function useTestDefinitionForm() {
|
||||
// Stores
|
||||
const evaluationsStore = useTestDefinitionStore();
|
||||
const tagsStore = useAnnotationTagsStore();
|
||||
|
||||
// Form state
|
||||
const state = ref<IEvaluationFormState>({
|
||||
description: '',
|
||||
name: {
|
||||
value: `My Test [${new Date().toLocaleString(undefined, { month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' })}]`,
|
||||
value: `My Test ${evaluationsStore.allTestDefinitions.length + 1}`,
|
||||
isEditing: false,
|
||||
tempValue: '',
|
||||
},
|
||||
tags: {
|
||||
isEditing: false,
|
||||
appliedTagIds: [],
|
||||
},
|
||||
tags: [],
|
||||
evaluationWorkflow: {
|
||||
mode: 'list',
|
||||
value: '',
|
||||
__rl: true,
|
||||
},
|
||||
metrics: [''],
|
||||
metrics: [],
|
||||
});
|
||||
|
||||
// Loading states
|
||||
|
@ -59,6 +57,9 @@ export function useTestDefinitionForm() {
|
|||
// Field refs
|
||||
const fields = ref<FormRefs>({} as FormRefs);
|
||||
|
||||
const tagIdToITag = (tagId: string) => {
|
||||
return tagsStore.tagsById[tagId];
|
||||
};
|
||||
// Methods
|
||||
const loadTestData = async (testId: string) => {
|
||||
try {
|
||||
|
@ -66,6 +67,10 @@ export function useTestDefinitionForm() {
|
|||
const testDefinition = evaluationsStore.testDefinitionsById[testId];
|
||||
|
||||
if (testDefinition) {
|
||||
// Fetch metrics for this test definition
|
||||
const metrics = await evaluationsStore.fetchMetrics(testId);
|
||||
console.log('Loaded metrics:', metrics);
|
||||
|
||||
state.value = {
|
||||
description: testDefinition.description ?? '',
|
||||
name: {
|
||||
|
@ -73,20 +78,16 @@ export function useTestDefinitionForm() {
|
|||
isEditing: false,
|
||||
tempValue: '',
|
||||
},
|
||||
tags: {
|
||||
isEditing: false,
|
||||
appliedTagIds: testDefinition.annotationTagId ? [testDefinition.annotationTagId] : [],
|
||||
},
|
||||
tags: testDefinition.annotationTagId ? [tagIdToITag(testDefinition.annotationTagId)] : [],
|
||||
evaluationWorkflow: {
|
||||
mode: 'list',
|
||||
value: testDefinition.evaluationWorkflowId ?? '',
|
||||
__rl: true,
|
||||
},
|
||||
metrics: [''],
|
||||
metrics, // Use the fetched metrics
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// TODO: Throw better errors
|
||||
console.error('Failed to load test data', error);
|
||||
}
|
||||
};
|
||||
|
@ -114,6 +115,33 @@ export function useTestDefinitionForm() {
|
|||
}
|
||||
};
|
||||
|
||||
const deleteMetric = async (metricId: string, testId: string) => {
|
||||
await evaluationsStore.deleteMetric({ id: metricId, testDefinitionId: testId });
|
||||
state.value.metrics = state.value.metrics.filter((metric) => metric.id !== metricId);
|
||||
};
|
||||
|
||||
const updateMetrics = async (testId: string) => {
|
||||
const updatePromises = state.value.metrics.map(async (metric) => {
|
||||
if (!metric.name) return;
|
||||
|
||||
if (!metric.id) {
|
||||
const createdMetric = await evaluationsStore.createMetric({
|
||||
name: metric.name,
|
||||
testDefinitionId: testId,
|
||||
});
|
||||
metric.id = createdMetric.id;
|
||||
} else {
|
||||
await evaluationsStore.updateMetric({
|
||||
name: metric.name,
|
||||
id: metric.id,
|
||||
testDefinitionId: testId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
};
|
||||
|
||||
const updateTest = async (testId: string) => {
|
||||
if (isSaving.value) return;
|
||||
|
||||
|
@ -135,12 +163,14 @@ export function useTestDefinitionForm() {
|
|||
params.evaluationWorkflowId = state.value.evaluationWorkflow.value.toString();
|
||||
}
|
||||
|
||||
const annotationTagId = state.value.tags.appliedTagIds[0];
|
||||
const annotationTagId = state.value.tags?.[0]?.id;
|
||||
if (annotationTagId) {
|
||||
params.annotationTagId = annotationTagId;
|
||||
}
|
||||
// Update the existing test
|
||||
return await evaluationsStore.update({ ...params, id: testId });
|
||||
const updatedTest = await evaluationsStore.update({ ...params, id: testId });
|
||||
|
||||
return updatedTest;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
|
@ -152,8 +182,6 @@ export function useTestDefinitionForm() {
|
|||
if (field === 'name') {
|
||||
state.value.name.tempValue = state.value.name.value;
|
||||
state.value.name.isEditing = true;
|
||||
} else {
|
||||
state.value.tags.isEditing = true;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -161,8 +189,6 @@ export function useTestDefinitionForm() {
|
|||
if (field === 'name') {
|
||||
state.value.name.value = state.value.name.tempValue;
|
||||
state.value.name.isEditing = false;
|
||||
} else {
|
||||
state.value.tags.isEditing = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -170,8 +196,6 @@ export function useTestDefinitionForm() {
|
|||
if (field === 'name') {
|
||||
state.value.name.isEditing = false;
|
||||
state.value.name.tempValue = '';
|
||||
} else {
|
||||
state.value.tags.isEditing = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -189,6 +213,8 @@ export function useTestDefinitionForm() {
|
|||
fields,
|
||||
isSaving: computed(() => isSaving.value),
|
||||
fieldsIssues: computed(() => fieldsIssues.value),
|
||||
deleteMetric,
|
||||
updateMetrics,
|
||||
loadTestData,
|
||||
createTest,
|
||||
updateTest,
|
||||
|
|
|
@ -2741,7 +2741,7 @@
|
|||
"testDefinition.edit.description": "Description",
|
||||
"testDefinition.edit.tagName": "Tag name",
|
||||
"testDefinition.edit.step.intro": "When running a test",
|
||||
"testDefinition.edit.step.executions": "Fetch 5 past executions",
|
||||
"testDefinition.edit.step.executions": "Fetch past executions | Fetch {count} past execution | Fetch {count} past executions",
|
||||
"testDefinition.edit.step.nodes": "Mock nodes",
|
||||
"testDefinition.edit.step.mockedNodes": "No nodes mocked | {count} node mocked | {count} nodes mocked",
|
||||
"testDefinition.edit.step.reRunExecutions": "Re-run executions",
|
||||
|
|
|
@ -153,6 +153,7 @@ export const useExecutionsStore = defineStore('executions', () => {
|
|||
|
||||
executionsCount.value = data.count;
|
||||
executionsCountEstimated.value = data.estimated;
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
} finally {
|
||||
|
|
|
@ -13,6 +13,7 @@ export const useTestDefinitionStore = defineStore(
|
|||
const testDefinitionsById = ref<Record<string, TestDefinitionRecord>>({});
|
||||
const loading = ref(false);
|
||||
const fetchedAll = ref(false);
|
||||
const metricsById = ref<Record<string, testDefinitionsApi.TestMetricRecord>>({});
|
||||
|
||||
// Store instances
|
||||
const posthogStore = usePostHog();
|
||||
|
@ -34,6 +35,12 @@ export const useTestDefinitionStore = defineStore(
|
|||
|
||||
const hasTestDefinitions = computed(() => Object.keys(testDefinitionsById.value).length > 0);
|
||||
|
||||
const getMetricsByTestId = computed(() => (testId: string) => {
|
||||
return Object.values(metricsById.value).filter(
|
||||
(metric) => metric.testDefinitionId === testId,
|
||||
);
|
||||
});
|
||||
|
||||
// Methods
|
||||
const setAllTestDefinitions = (definitions: TestDefinitionRecord[]) => {
|
||||
testDefinitionsById.value = definitions.reduce(
|
||||
|
@ -147,6 +154,44 @@ export const useTestDefinitionStore = defineStore(
|
|||
return result.success;
|
||||
};
|
||||
|
||||
const fetchMetrics = async (testId: string) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const metrics = await testDefinitionsApi.getTestMetrics(rootStore.restApiContext, testId);
|
||||
metrics.forEach((metric) => {
|
||||
metricsById.value[metric.id] = metric;
|
||||
});
|
||||
return metrics;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const createMetric = async (params: {
|
||||
name: string;
|
||||
testDefinitionId: string;
|
||||
}): Promise<testDefinitionsApi.TestMetricRecord> => {
|
||||
const metric = await testDefinitionsApi.createTestMetric(rootStore.restApiContext, params);
|
||||
metricsById.value[metric.id] = metric;
|
||||
return metric;
|
||||
};
|
||||
|
||||
const updateMetric = async (
|
||||
params: testDefinitionsApi.TestMetricRecord,
|
||||
): Promise<testDefinitionsApi.TestMetricRecord> => {
|
||||
const metric = await testDefinitionsApi.updateTestMetric(rootStore.restApiContext, params);
|
||||
metricsById.value[metric.id] = metric;
|
||||
return metric;
|
||||
};
|
||||
|
||||
const deleteMetric = async (
|
||||
params: testDefinitionsApi.DeleteTestMetricParams,
|
||||
): Promise<void> => {
|
||||
await testDefinitionsApi.deleteTestMetric(rootStore.restApiContext, params);
|
||||
const { [params.id]: deleted, ...rest } = metricsById.value;
|
||||
metricsById.value = rest;
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
fetchedAll,
|
||||
|
@ -157,6 +202,8 @@ export const useTestDefinitionStore = defineStore(
|
|||
isLoading,
|
||||
hasTestDefinitions,
|
||||
isFeatureEnabled,
|
||||
metricsById,
|
||||
getMetricsByTestId,
|
||||
|
||||
// Methods
|
||||
fetchAll,
|
||||
|
@ -165,6 +212,10 @@ export const useTestDefinitionStore = defineStore(
|
|||
deleteById,
|
||||
upsertTestDefinitions,
|
||||
deleteTestDefinition,
|
||||
fetchMetrics,
|
||||
createMetric,
|
||||
updateMetric,
|
||||
deleteMetric,
|
||||
};
|
||||
},
|
||||
{},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, watch } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
|
@ -14,6 +14,10 @@ import EvaluationStep from '@/components/TestDefinition/EditDefinition/Evaluatio
|
|||
import TagsInput from '@/components/TestDefinition/EditDefinition/TagsInput.vue';
|
||||
import WorkflowSelector from '@/components/TestDefinition/EditDefinition/WorkflowSelector.vue';
|
||||
import MetricsInput from '@/components/TestDefinition/EditDefinition/MetricsInput.vue';
|
||||
import type { TestMetricRecord } from '@/api/testDefinition.ee';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import { useExecutionsStore } from '@/stores/executions.store';
|
||||
import { IExecutionsListResponse } from '@/Interface';
|
||||
|
||||
const props = defineProps<{
|
||||
testId?: string;
|
||||
|
@ -24,16 +28,20 @@ const route = useRoute();
|
|||
const locale = useI18n();
|
||||
const { debounce } = useDebounce();
|
||||
const toast = useToast();
|
||||
const { isLoading, allTags, tagsById, fetchAll } = useAnnotationTagsStore();
|
||||
const { isLoading, allTags, tagsById, fetchAll, create: createTag } = useAnnotationTagsStore();
|
||||
const { fetchExecutions } = useExecutionsStore();
|
||||
|
||||
const testId = computed(() => props.testId ?? (route.params.testId as string));
|
||||
const currentWorkflowId = computed(() => route.params.name as string);
|
||||
|
||||
const buttonLabel = computed(() =>
|
||||
testId.value
|
||||
? locale.baseText('testDefinition.edit.updateTest')
|
||||
: locale.baseText('testDefinition.edit.saveTest'),
|
||||
);
|
||||
|
||||
const matchedExecutions = ref<IExecutionsListResponse['results']>([]);
|
||||
|
||||
const {
|
||||
state,
|
||||
fieldsIssues,
|
||||
|
@ -43,14 +51,18 @@ const {
|
|||
updateTest,
|
||||
startEditing,
|
||||
saveChanges,
|
||||
cancelEditing,
|
||||
handleKeydown,
|
||||
deleteMetric,
|
||||
updateMetrics,
|
||||
} = useTestDefinitionForm();
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchAll();
|
||||
if (testId.value) {
|
||||
await loadTestData(testId.value);
|
||||
if (state.value.tags.length > 0) {
|
||||
await fetchSelectedExecutions();
|
||||
}
|
||||
} else {
|
||||
await onSaveTest();
|
||||
}
|
||||
|
@ -64,7 +76,7 @@ async function onSaveTest() {
|
|||
} else {
|
||||
savedTest = await createTest(currentWorkflowId.value);
|
||||
}
|
||||
if (savedTest && route.name === VIEWS.TEST_DEFINITION_EDIT) {
|
||||
if (savedTest && route.name === VIEWS.NEW_TEST_DEFINITION) {
|
||||
await router.replace({
|
||||
name: VIEWS.TEST_DEFINITION_EDIT,
|
||||
params: { testId: savedTest.id },
|
||||
|
@ -83,7 +95,50 @@ function hasIssues(key: string) {
|
|||
return fieldsIssues.value.some((issue) => issue.field === key);
|
||||
}
|
||||
|
||||
watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: true });
|
||||
async function onDeleteMetric(deletedMetric: Partial<TestMetricRecord>) {
|
||||
if (deletedMetric.id) {
|
||||
await deleteMetric(deletedMetric.id, testId.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSelectedExecutions() {
|
||||
const executionsForTags = await fetchExecutions({
|
||||
annotationTags: state.value.tags.map((tag) => tag.id),
|
||||
});
|
||||
matchedExecutions.value = executionsForTags.results;
|
||||
}
|
||||
|
||||
async function onAddTag() {
|
||||
const currentTags = state.value.tags;
|
||||
|
||||
if (currentTags.length === 0) {
|
||||
const { prompt } = useMessage();
|
||||
const tagName = await prompt('Enter tag name');
|
||||
const newTag = await createTag(tagName.value);
|
||||
|
||||
state.value.tags = [newTag];
|
||||
console.log('🚀 ~ onSelectExecutions ~ newWindow:', newTag);
|
||||
}
|
||||
|
||||
const newWindow = window.open(`/workflow/${currentWorkflowId.value}/executions`, '_blank');
|
||||
if (newWindow) {
|
||||
newWindow.onload = () =>
|
||||
(newWindow.onbeforeunload = async () => {
|
||||
await fetchSelectedExecutions();
|
||||
});
|
||||
}
|
||||
}
|
||||
watch(
|
||||
[() => state.value.name, () => state.value.description, () => state.value.evaluationWorkflow],
|
||||
debounce(onSaveTest, { debounceTime: 400 }),
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
[() => state.value.metrics],
|
||||
debounce(async () => await updateMetrics(testId.value), { debounceTime: 400 }),
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -113,20 +168,15 @@ watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: tr
|
|||
<div :class="$style.panelBlock">
|
||||
<EvaluationStep
|
||||
:class="$style.step"
|
||||
:title="locale.baseText('testDefinition.edit.step.executions')"
|
||||
:title="
|
||||
locale.baseText('testDefinition.edit.step.executions', {
|
||||
adjustToNumber: matchedExecutions.length,
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #icon><font-awesome-icon icon="history" size="lg" /></template>
|
||||
<template #cardContent>
|
||||
<TagsInput
|
||||
v-model="state.tags"
|
||||
:class="{ 'has-issues': hasIssues('tags') }"
|
||||
:all-tags="allTags"
|
||||
:tags-by-id="tagsById"
|
||||
:is-loading="isLoading"
|
||||
:start-editing="startEditing"
|
||||
:save-changes="saveChanges"
|
||||
:cancel-editing="cancelEditing"
|
||||
/>
|
||||
<TagsInput v-model="state.tags" :selected-tags="state.tags" @add-tag="onAddTag" />
|
||||
</template>
|
||||
</EvaluationStep>
|
||||
<div :class="$style.evaluationArrows">
|
||||
|
@ -172,7 +222,11 @@ watch(() => state.value, debounce(onSaveTest, { debounceTime: 400 }), { deep: tr
|
|||
>
|
||||
<template #icon><font-awesome-icon icon="chart-bar" size="lg" /></template>
|
||||
<template #cardContent>
|
||||
<MetricsInput v-model="state.metrics" :class="{ 'has-issues': hasIssues('metrics') }" />
|
||||
<MetricsInput
|
||||
v-model="state.metrics"
|
||||
:class="{ 'has-issues': hasIssues('metrics') }"
|
||||
@delete-metric="onDeleteMetric"
|
||||
/>
|
||||
</template>
|
||||
</EvaluationStep>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue