mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(core): Add telemetry for workflow evaluation feature (no-changelog) (#13367)
This commit is contained in:
parent
72e489910c
commit
c9c0716a69
|
@ -8,6 +8,7 @@ import {
|
||||||
testMetricPatchRequestBodySchema,
|
testMetricPatchRequestBodySchema,
|
||||||
} from '@/evaluation.ee/metric.schema';
|
} from '@/evaluation.ee/metric.schema';
|
||||||
import { getSharedWorkflowIds } from '@/public-api/v1/handlers/workflows/workflows.service';
|
import { getSharedWorkflowIds } from '@/public-api/v1/handlers/workflows/workflows.service';
|
||||||
|
import { Telemetry } from '@/telemetry';
|
||||||
|
|
||||||
import { TestDefinitionService } from './test-definition.service.ee';
|
import { TestDefinitionService } from './test-definition.service.ee';
|
||||||
import { TestMetricsRequest } from './test-definitions.types.ee';
|
import { TestMetricsRequest } from './test-definitions.types.ee';
|
||||||
|
@ -17,6 +18,7 @@ export class TestMetricsController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly testDefinitionService: TestDefinitionService,
|
private readonly testDefinitionService: TestDefinitionService,
|
||||||
private readonly testMetricRepository: TestMetricRepository,
|
private readonly testMetricRepository: TestMetricRepository,
|
||||||
|
private readonly telemetry: Telemetry,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// This method is used in multiple places in the controller to get the test definition
|
// This method is used in multiple places in the controller to get the test definition
|
||||||
|
@ -105,7 +107,16 @@ export class TestMetricsController {
|
||||||
|
|
||||||
if (!metric) throw new NotFoundError('Metric not found');
|
if (!metric) throw new NotFoundError('Metric not found');
|
||||||
|
|
||||||
await this.testMetricRepository.update(metricId, bodyParseResult.data);
|
const updateResult = await this.testMetricRepository.update(metricId, bodyParseResult.data);
|
||||||
|
|
||||||
|
// Send telemetry event if the metric was updated
|
||||||
|
if (updateResult.affected === 1 && metric.name !== bodyParseResult.data.name) {
|
||||||
|
this.telemetry.track('User added metrics to test', {
|
||||||
|
metric_id: metricId,
|
||||||
|
metric_name: bodyParseResult.data.name,
|
||||||
|
test_id: testDefinitionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Respond with the updated metric
|
// Respond with the updated metric
|
||||||
return await this.testMetricRepository.findOneBy({ id: metricId });
|
return await this.testMetricRepository.findOneBy({ id: metricId });
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
import { validateEntity } from '@/generic-helpers';
|
import { validateEntity } from '@/generic-helpers';
|
||||||
import type { ListQuery } from '@/requests';
|
import type { ListQuery } from '@/requests';
|
||||||
|
import { Telemetry } from '@/telemetry';
|
||||||
|
|
||||||
type TestDefinitionLike = Omit<
|
type TestDefinitionLike = Omit<
|
||||||
Partial<TestDefinition>,
|
Partial<TestDefinition>,
|
||||||
|
@ -22,6 +23,7 @@ export class TestDefinitionService {
|
||||||
constructor(
|
constructor(
|
||||||
private testDefinitionRepository: TestDefinitionRepository,
|
private testDefinitionRepository: TestDefinitionRepository,
|
||||||
private annotationTagRepository: AnnotationTagRepository,
|
private annotationTagRepository: AnnotationTagRepository,
|
||||||
|
private telemetry: Telemetry,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private toEntityLike(attrs: {
|
private toEntityLike(attrs: {
|
||||||
|
@ -94,6 +96,13 @@ export class TestDefinitionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: string, attrs: TestDefinitionLike) {
|
async update(id: string, attrs: TestDefinitionLike) {
|
||||||
|
const existingTestDefinition = await this.testDefinitionRepository.findOneOrFail({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
relations: ['workflow'],
|
||||||
|
});
|
||||||
|
|
||||||
if (attrs.name) {
|
if (attrs.name) {
|
||||||
const updatedTest = this.toEntity(attrs);
|
const updatedTest = this.toEntity(attrs);
|
||||||
await validateEntity(updatedTest);
|
await validateEntity(updatedTest);
|
||||||
|
@ -114,13 +123,6 @@ export class TestDefinitionService {
|
||||||
|
|
||||||
// If there are mocked nodes, validate them
|
// If there are mocked nodes, validate them
|
||||||
if (attrs.mockedNodes && attrs.mockedNodes.length > 0) {
|
if (attrs.mockedNodes && attrs.mockedNodes.length > 0) {
|
||||||
const existingTestDefinition = await this.testDefinitionRepository.findOneOrFail({
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
relations: ['workflow'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const existingNodeNames = new Map(
|
const existingNodeNames = new Map(
|
||||||
existingTestDefinition.workflow.nodes.map((n) => [n.name, n]),
|
existingTestDefinition.workflow.nodes.map((n) => [n.name, n]),
|
||||||
);
|
);
|
||||||
|
@ -146,6 +148,24 @@ export class TestDefinitionService {
|
||||||
if (queryResult.affected === 0) {
|
if (queryResult.affected === 0) {
|
||||||
throw new NotFoundError('Test definition not found');
|
throw new NotFoundError('Test definition not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send the telemetry events
|
||||||
|
if (attrs.annotationTagId && attrs.annotationTagId !== existingTestDefinition.annotationTagId) {
|
||||||
|
this.telemetry.track('User added tag to test', {
|
||||||
|
test_id: id,
|
||||||
|
tag_id: attrs.annotationTagId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
attrs.evaluationWorkflowId &&
|
||||||
|
existingTestDefinition.evaluationWorkflowId !== attrs.evaluationWorkflowId
|
||||||
|
) {
|
||||||
|
this.telemetry.track('User added evaluation workflow to test', {
|
||||||
|
test_id: id,
|
||||||
|
subworkflow_id: attrs.evaluationWorkflowId,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string, accessibleWorkflowIds: string[]) {
|
async delete(id: string, accessibleWorkflowIds: string[]) {
|
||||||
|
@ -154,6 +174,8 @@ export class TestDefinitionService {
|
||||||
if (deleteResult.affected === 0) {
|
if (deleteResult.affected === 0) {
|
||||||
throw new NotFoundError('Test definition not found');
|
throw new NotFoundError('Test definition not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.telemetry.track('User deleted a test', { test_id: id });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMany(options: ListQuery.Options, accessibleWorkflowIds: string[] = []) {
|
async getMany(options: ListQuery.Options, accessibleWorkflowIds: string[] = []) {
|
||||||
|
|
|
@ -292,6 +292,8 @@ export class TestRunnerService {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let testRunEndStatusForTelemetry;
|
||||||
|
|
||||||
const abortSignal = abortController.signal;
|
const abortSignal = abortController.signal;
|
||||||
try {
|
try {
|
||||||
// Get the evaluation workflow
|
// Get the evaluation workflow
|
||||||
|
@ -338,7 +340,7 @@ export class TestRunnerService {
|
||||||
// Update test run status
|
// Update test run status
|
||||||
await this.testRunRepository.markAsRunning(testRun.id, pastExecutions.length);
|
await this.testRunRepository.markAsRunning(testRun.id, pastExecutions.length);
|
||||||
|
|
||||||
this.telemetry.track('User runs test', {
|
this.telemetry.track('User ran test', {
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
test_id: test.id,
|
test_id: test.id,
|
||||||
run_id: testRun.id,
|
run_id: testRun.id,
|
||||||
|
@ -504,13 +506,17 @@ export class TestRunnerService {
|
||||||
await Db.transaction(async (trx) => {
|
await Db.transaction(async (trx) => {
|
||||||
await this.testRunRepository.markAsCancelled(testRun.id, trx);
|
await this.testRunRepository.markAsCancelled(testRun.id, trx);
|
||||||
await this.testCaseExecutionRepository.markAllPendingAsCancelled(testRun.id, trx);
|
await this.testCaseExecutionRepository.markAllPendingAsCancelled(testRun.id, trx);
|
||||||
|
|
||||||
|
testRunEndStatusForTelemetry = 'cancelled';
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const aggregatedMetrics = metrics.getAggregatedMetrics();
|
const aggregatedMetrics = metrics.getAggregatedMetrics();
|
||||||
|
|
||||||
await this.testRunRepository.markAsCompleted(testRun.id, aggregatedMetrics);
|
await this.testRunRepository.markAsCompleted(testRun.id, aggregatedMetrics);
|
||||||
|
|
||||||
this.logger.debug('Test run finished', { testId: test.id });
|
this.logger.debug('Test run finished', { testId: test.id, testRunId: testRun.id });
|
||||||
|
|
||||||
|
testRunEndStatusForTelemetry = 'completed';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ExecutionCancelledError) {
|
if (e instanceof ExecutionCancelledError) {
|
||||||
|
@ -523,15 +529,26 @@ export class TestRunnerService {
|
||||||
await this.testRunRepository.markAsCancelled(testRun.id, trx);
|
await this.testRunRepository.markAsCancelled(testRun.id, trx);
|
||||||
await this.testCaseExecutionRepository.markAllPendingAsCancelled(testRun.id, trx);
|
await this.testCaseExecutionRepository.markAllPendingAsCancelled(testRun.id, trx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testRunEndStatusForTelemetry = 'cancelled';
|
||||||
} else if (e instanceof TestRunError) {
|
} else if (e instanceof TestRunError) {
|
||||||
await this.testRunRepository.markAsError(testRun.id, e.code, e.extra as IDataObject);
|
await this.testRunRepository.markAsError(testRun.id, e.code, e.extra as IDataObject);
|
||||||
|
testRunEndStatusForTelemetry = 'error';
|
||||||
} else {
|
} else {
|
||||||
await this.testRunRepository.markAsError(testRun.id, 'UNKNOWN_ERROR');
|
await this.testRunRepository.markAsError(testRun.id, 'UNKNOWN_ERROR');
|
||||||
|
testRunEndStatusForTelemetry = 'error';
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up abort controller
|
// Clean up abort controller
|
||||||
this.abortControllers.delete(testRun.id);
|
this.abortControllers.delete(testRun.id);
|
||||||
|
|
||||||
|
// Send telemetry event
|
||||||
|
this.telemetry.track('Test run finished', {
|
||||||
|
test_id: test.id,
|
||||||
|
run_id: testRun.id,
|
||||||
|
status: testRunEndStatusForTelemetry,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { TestRunsRequest } from '@/evaluation.ee/test-definitions.types.ee';
|
||||||
import { TestRunnerService } from '@/evaluation.ee/test-runner/test-runner.service.ee';
|
import { TestRunnerService } from '@/evaluation.ee/test-runner/test-runner.service.ee';
|
||||||
import { listQueryMiddleware } from '@/middlewares';
|
import { listQueryMiddleware } from '@/middlewares';
|
||||||
import { getSharedWorkflowIds } from '@/public-api/v1/handlers/workflows/workflows.service';
|
import { getSharedWorkflowIds } from '@/public-api/v1/handlers/workflows/workflows.service';
|
||||||
|
import { Telemetry } from '@/telemetry';
|
||||||
|
|
||||||
import { TestDefinitionService } from './test-definition.service.ee';
|
import { TestDefinitionService } from './test-definition.service.ee';
|
||||||
|
|
||||||
|
@ -22,6 +23,7 @@ export class TestRunsController {
|
||||||
private readonly testCaseExecutionRepository: TestCaseExecutionRepository,
|
private readonly testCaseExecutionRepository: TestCaseExecutionRepository,
|
||||||
private readonly testRunnerService: TestRunnerService,
|
private readonly testRunnerService: TestRunnerService,
|
||||||
private readonly instanceSettings: InstanceSettings,
|
private readonly instanceSettings: InstanceSettings,
|
||||||
|
private readonly telemetry: Telemetry,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -92,7 +94,7 @@ export class TestRunsController {
|
||||||
|
|
||||||
@Delete('/:testDefinitionId/runs/:id')
|
@Delete('/:testDefinitionId/runs/:id')
|
||||||
async delete(req: TestRunsRequest.Delete) {
|
async delete(req: TestRunsRequest.Delete) {
|
||||||
const { id: testRunId } = req.params;
|
const { id: testRunId, testDefinitionId } = req.params;
|
||||||
|
|
||||||
// Check test definition and test run exist
|
// Check test definition and test run exist
|
||||||
await this.getTestDefinition(req);
|
await this.getTestDefinition(req);
|
||||||
|
@ -100,6 +102,8 @@ export class TestRunsController {
|
||||||
|
|
||||||
await this.testRunRepository.delete({ id: testRunId });
|
await this.testRunRepository.delete({ id: testRunId });
|
||||||
|
|
||||||
|
this.telemetry.track('User deleted a run', { run_id: testRunId, test_id: testDefinitionId });
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,12 +9,14 @@ import { createEventBus, N8nTooltip } from 'n8n-design-system';
|
||||||
import type { CanvasConnectionPort, CanvasEventBusEvents, CanvasNodeData } from '@/types';
|
import type { CanvasConnectionPort, CanvasEventBusEvents, CanvasNodeData } from '@/types';
|
||||||
import { useVueFlow } from '@vue-flow/core';
|
import { useVueFlow } from '@vue-flow/core';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
const { resetWorkspace, initializeWorkspace } = useCanvasOperations({ router });
|
const { resetWorkspace, initializeWorkspace } = useCanvasOperations({ router });
|
||||||
|
|
||||||
|
@ -101,6 +103,13 @@ function onPinButtonClick(data: CanvasNodeData) {
|
||||||
|
|
||||||
emit('update:modelValue', updatedNodes);
|
emit('update:modelValue', updatedNodes);
|
||||||
updateNodeClasses([data.id], !isPinned);
|
updateNodeClasses([data.id], !isPinned);
|
||||||
|
|
||||||
|
if (!isPinned) {
|
||||||
|
telemetry.track('User selected node to be mocked', {
|
||||||
|
node_id: data.id,
|
||||||
|
test_id: testId.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function isPinButtonVisible(outputs: CanvasConnectionPort[]) {
|
function isPinButtonVisible(outputs: CanvasConnectionPort[]) {
|
||||||
return outputs.length === 1;
|
return outputs.length === 1;
|
||||||
|
|
|
@ -21,7 +21,10 @@ const props = withDefaults(defineProps<WorkflowSelectorProps>(), {
|
||||||
sampleWorkflowName: undefined,
|
sampleWorkflowName: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits<{ 'update:modelValue': [value: WorkflowSelectorProps['modelValue']] }>();
|
defineEmits<{
|
||||||
|
'update:modelValue': [value: WorkflowSelectorProps['modelValue']];
|
||||||
|
workflowCreated: [workflowId: string];
|
||||||
|
}>();
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
|
|
||||||
const subworkflowName = computed(() => {
|
const subworkflowName = computed(() => {
|
||||||
|
@ -63,6 +66,7 @@ const sampleWorkflow = computed<IWorkflowDataCreate>(() => {
|
||||||
allow-new
|
allow-new
|
||||||
:sample-workflow="sampleWorkflow"
|
:sample-workflow="sampleWorkflow"
|
||||||
@update:model-value="$emit('update:modelValue', $event)"
|
@update:model-value="$emit('update:modelValue', $event)"
|
||||||
|
@workflow-created="$emit('workflowCreated', $event)"
|
||||||
/>
|
/>
|
||||||
</n8n-input-label>
|
</n8n-input-label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -29,6 +29,7 @@ defineProps<{
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
openPinningModal: [];
|
openPinningModal: [];
|
||||||
deleteMetric: [metric: Partial<TestMetricRecord>];
|
deleteMetric: [metric: Partial<TestMetricRecord>];
|
||||||
|
evaluationWorkflowCreated: [workflowId: string];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
|
@ -145,6 +146,7 @@ function showFieldIssues(fieldKey: string) {
|
||||||
:class="{ 'has-issues': getFieldIssues('evaluationWorkflow').length > 0 }"
|
:class="{ 'has-issues': getFieldIssues('evaluationWorkflow').length > 0 }"
|
||||||
:sample-workflow-name="sampleWorkflowName"
|
:sample-workflow-name="sampleWorkflowName"
|
||||||
@update:model-value="updateChangedFieldsKeys('evaluationWorkflow')"
|
@update:model-value="updateChangedFieldsKeys('evaluationWorkflow')"
|
||||||
|
@workflow-created="$emit('evaluationWorkflowCreated', $event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</EvaluationStep>
|
</EvaluationStep>
|
||||||
|
|
|
@ -55,6 +55,7 @@ const emit = defineEmits<{
|
||||||
modalOpenerClick: [];
|
modalOpenerClick: [];
|
||||||
focus: [];
|
focus: [];
|
||||||
blur: [];
|
blur: [];
|
||||||
|
workflowCreated: [workflowId: string];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
@ -232,6 +233,8 @@ const onAddResourceClicked = async () => {
|
||||||
hideDropdown();
|
hideDropdown();
|
||||||
|
|
||||||
window.open(href, '_blank');
|
window.open(href, '_blank');
|
||||||
|
|
||||||
|
emit('workflowCreated', newWorkflow.id);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -7,11 +7,13 @@ import { createEventBus } from 'n8n-design-system';
|
||||||
import VoteButtons from '@/components/executions/workflow/VoteButtons.vue';
|
import VoteButtons from '@/components/executions/workflow/VoteButtons.vue';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
|
||||||
const executionsStore = useExecutionsStore();
|
const executionsStore = useExecutionsStore();
|
||||||
|
|
||||||
const { showError } = useToast();
|
const { showError } = useToast();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
const tagsEventBus = createEventBus();
|
const tagsEventBus = createEventBus();
|
||||||
const isTagsEditEnabled = ref(false);
|
const isTagsEditEnabled = ref(false);
|
||||||
|
@ -81,6 +83,13 @@ const onTagsBlur = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await executionsStore.annotateExecution(activeExecution.value.id, { tags: newTagIds });
|
await executionsStore.annotateExecution(activeExecution.value.id, { tags: newTagIds });
|
||||||
|
|
||||||
|
if (newTagIds.length > 0) {
|
||||||
|
telemetry.track('User added execution annotation tag', {
|
||||||
|
tag_ids: newTagIds,
|
||||||
|
execution_id: activeExecution.value.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(e, 'executionAnnotationView.tag.error');
|
showError(e, 'executionAnnotationView.tag.error');
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,7 +186,7 @@ async function getExamplePinnedDataForTags() {
|
||||||
// Debounced watchers for auto-saving
|
// Debounced watchers for auto-saving
|
||||||
watch(
|
watch(
|
||||||
() => state.value.metrics,
|
() => state.value.metrics,
|
||||||
debounce(async () => await updateMetrics(testId.value), { debounceTime: 400 }),
|
debounce(async () => await updateMetrics(testId.value), { debounceTime: 400, trailing: true }),
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
watch(() => state.value.tags.value, getExamplePinnedDataForTags);
|
watch(() => state.value.tags.value, getExamplePinnedDataForTags);
|
||||||
|
@ -198,9 +198,16 @@ watch(
|
||||||
state.value.evaluationWorkflow,
|
state.value.evaluationWorkflow,
|
||||||
state.value.mockedNodes,
|
state.value.mockedNodes,
|
||||||
],
|
],
|
||||||
debounce(onSaveTest, { debounceTime: 400 }),
|
debounce(onSaveTest, { debounceTime: 400, trailing: true }),
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function onEvaluationWorkflowCreated(workflowId: string) {
|
||||||
|
telemetry.track('User created evaluation workflow from test', {
|
||||||
|
test_id: testId.value,
|
||||||
|
subworkflow_id: workflowId,
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -261,6 +268,7 @@ watch(
|
||||||
:sample-workflow-name="workflowName"
|
:sample-workflow-name="workflowName"
|
||||||
@open-pinning-modal="openPinningModal"
|
@open-pinning-modal="openPinningModal"
|
||||||
@delete-metric="onDeleteMetric"
|
@delete-metric="onDeleteMetric"
|
||||||
|
@evaluation-workflow-created="onEvaluationWorkflowCreated($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue