Skip to content

Commit

Permalink
feat(editor): Add workflow evaluation edit and list views (no-changel…
Browse files Browse the repository at this point in the history
…og) (#11719)
  • Loading branch information
OlegIvaniv authored Nov 27, 2024
1 parent a535e88 commit 132aa0b
Show file tree
Hide file tree
Showing 32 changed files with 2,490 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ElMenu, ElSubMenu, ElMenuItem, type MenuItemRegistered } from 'element-plus';
import { ref, defineProps, defineEmits, defineOptions } from 'vue';
import { ref } from 'vue';
import type { RouteLocationRaw } from 'vue-router';
import ConditionalRouterLink from '../ConditionalRouterLink';
Expand Down
73 changes: 73 additions & 0 deletions packages/editor-ui/src/api/testDefinition.ee.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { IRestApiContext } from '@/Interface';
import { makeRestApiRequest } from '@/utils/apiUtils';
export interface TestDefinitionRecord {
id: string;
name: string;
workflowId: string;
evaluationWorkflowId?: string | null;
annotationTagId?: string | null;
description?: string | null;
updatedAt?: string;
createdAt?: string;
}
interface CreateTestDefinitionParams {
name: string;
workflowId: string;
evaluationWorkflowId?: string | null;
}

export interface UpdateTestDefinitionParams {
name?: string;
evaluationWorkflowId?: string | null;
annotationTagId?: string | null;
description?: string | null;
}
export interface UpdateTestResponse {
createdAt: string;
updatedAt: string;
id: string;
name: string;
workflowId: string;
description: string | null;
annotationTag: string | null;
evaluationWorkflowId: string | null;
annotationTagId: string | null;
}

const endpoint = '/evaluation/test-definitions';

export async function getTestDefinitions(context: IRestApiContext) {
return await makeRestApiRequest<{ count: number; testDefinitions: TestDefinitionRecord[] }>(
context,
'GET',
endpoint,
);
}

export async function getTestDefinition(context: IRestApiContext, id: string) {
return await makeRestApiRequest<{ id: string }>(context, 'GET', `${endpoint}/${id}`);
}

export async function createTestDefinition(
context: IRestApiContext,
params: CreateTestDefinitionParams,
) {
return await makeRestApiRequest<TestDefinitionRecord>(context, 'POST', endpoint, params);
}

export async function updateTestDefinition(
context: IRestApiContext,
id: string,
params: UpdateTestDefinitionParams,
) {
return await makeRestApiRequest<UpdateTestResponse>(
context,
'PATCH',
`${endpoint}/${id}`,
params,
);
}

export async function deleteTestDefinition(context: IRestApiContext, id: string) {
return await makeRestApiRequest<{ success: boolean }>(context, 'DELETE', `${endpoint}/${id}`);
}
29 changes: 25 additions & 4 deletions packages/editor-ui/src/components/MainHeader/MainHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
PLACEHOLDER_EMPTY_WORKFLOW_ID,
STICKY_NODE_TYPE,
VIEWS,
WORKFLOW_EVALUATION_EXPERIMENT,
} from '@/constants';
import { useI18n } from '@/composables/useI18n';
import { useNDVStore } from '@/stores/ndv.store';
Expand All @@ -19,6 +20,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { useExecutionsStore } from '@/stores/executions.store';
import { useSettingsStore } from '@/stores/settings.store';
import { usePushConnection } from '@/composables/usePushConnection';
import { usePostHog } from '@/stores/posthog.store';
import GithubButton from 'vue-github-button';
import { useLocalStorage } from '@vueuse/core';
Expand All @@ -33,17 +35,28 @@ const sourceControlStore = useSourceControlStore();
const workflowsStore = useWorkflowsStore();
const executionsStore = useExecutionsStore();
const settingsStore = useSettingsStore();
const posthogStore = usePostHog();
const activeHeaderTab = ref(MAIN_HEADER_TABS.WORKFLOW);
const workflowToReturnTo = ref('');
const executionToReturnTo = ref('');
const dirtyState = ref(false);
const githubButtonHidden = useLocalStorage(LOCAL_STORAGE_HIDE_GITHUB_STAR_BUTTON, false);
const tabBarItems = computed(() => [
{ value: MAIN_HEADER_TABS.WORKFLOW, label: locale.baseText('generic.editor') },
{ value: MAIN_HEADER_TABS.EXECUTIONS, label: locale.baseText('generic.executions') },
]);
const tabBarItems = computed(() => {
const items = [
{ value: MAIN_HEADER_TABS.WORKFLOW, label: locale.baseText('generic.editor') },
{ value: MAIN_HEADER_TABS.EXECUTIONS, label: locale.baseText('generic.executions') },
];
if (posthogStore.isFeatureEnabled(WORKFLOW_EVALUATION_EXPERIMENT)) {
items.push({
value: MAIN_HEADER_TABS.TEST_DEFINITION,
label: locale.baseText('generic.tests'),
});
}
return items;
});
const activeNode = computed(() => ndvStore.activeNode);
const hideMenuBar = computed(() =>
Expand Down Expand Up @@ -80,6 +93,9 @@ onMounted(async () => {
});
function syncTabsWithRoute(to: RouteLocation, from?: RouteLocation): void {
if (to.matched.some((record) => record.name === VIEWS.TEST_DEFINITION)) {
activeHeaderTab.value = MAIN_HEADER_TABS.TEST_DEFINITION;
}
if (
to.name === VIEWS.EXECUTION_HOME ||
to.name === VIEWS.WORKFLOW_EXECUTIONS ||
Expand Down Expand Up @@ -119,6 +135,11 @@ function onTabSelected(tab: MAIN_HEADER_TABS, event: MouseEvent) {
void navigateToExecutionsView(openInNewTab);
break;
case MAIN_HEADER_TABS.TEST_DEFINITION:
activeHeaderTab.value = MAIN_HEADER_TABS.TEST_DEFINITION;
void router.push({ name: VIEWS.TEST_DEFINITION });
break;
default:
break;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/editor-ui/src/components/TagsDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import { useToast } from '@/composables/useToast';
interface TagsDropdownProps {
placeholder: string;
modelValue: string[];
createTag: (name: string) => Promise<ITag>;
eventBus: EventBus | null;
allTags: ITag[];
isLoading: boolean;
tagsById: Record<string, ITag>;
createTag?: (name: string) => Promise<ITag>;
}
const i18n = useI18n();
Expand Down Expand Up @@ -109,6 +109,8 @@ function filterOptions(value = '') {
}
async function onCreate() {
if (!props.createTag) return;
const name = filter.value;
try {
const newTag = await props.createTag(name);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<template>
<div :class="$style.arrowConnector"></div>
</template>

<style module lang="scss">
.arrowConnector {
$arrow-width: 12px;
$arrow-height: 8px;
$stalk-width: 2px;
$color: var(--color-text-dark);
position: relative;
height: var(--arrow-height, 3rem);
margin: 0.5rem 0;
&::before,
&::after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
}
&::before {
top: 0;
width: $stalk-width;
height: calc(100% - #{$arrow-height});
background-color: $color;
}
&::after {
bottom: 0;
width: 0;
height: 0;
border-left: calc($arrow-width / 2) solid transparent;
border-right: calc($arrow-width / 2) solid transparent;
border-top: $arrow-height solid $color;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script setup lang="ts">
import { useI18n } from '@/composables/useI18n';
interface Props {
modelValue: string;
}
withDefaults(defineProps<Props>(), {
modelValue: '',
});
defineEmits<{ 'update:modelValue': [value: 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)"
/>
</n8n-input-label>
</div>
</template>

<style module lang="scss">
.field {
width: 100%;
margin-top: var(--spacing-xs);
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<script setup lang="ts">
import { useI18n } from '@/composables/useI18n';
export interface EvaluationHeaderProps {
modelValue: {
value: string;
isEditing: boolean;
tempValue: string;
};
startEditing: (field: string) => void;
saveChanges: (field: string) => void;
handleKeydown: (e: KeyboardEvent, field: string) => void;
}
defineEmits<{ 'update:modelValue': [value: EvaluationHeaderProps['modelValue']] }>();
defineProps<EvaluationHeaderProps>();
const locale = useI18n();
</script>

<template>
<div :class="$style.header">
<n8n-icon-button
icon="arrow-left"
:class="$style.backButton"
type="tertiary"
:title="locale.baseText('testDefinition.edit.backButtonTitle')"
@click="$router.back()"
/>
<h2 :class="$style.title">
<template v-if="!modelValue.isEditing">
<span :class="$style.titleText">
{{ modelValue.value }}
</span>
<n8n-icon-button
:class="$style.editInputButton"
icon="pen"
type="tertiary"
@click="startEditing('name')"
/>
</template>
<N8nInput
v-else
ref="nameInput"
data-test-id="evaluation-name-input"
:model-value="modelValue.tempValue"
type="text"
:placeholder="locale.baseText('testDefinition.edit.namePlaceholder')"
@update:model-value="$emit('update:modelValue', { ...modelValue, tempValue: $event })"
@blur="() => saveChanges('name')"
@keydown="(e: KeyboardEvent) => handleKeydown(e, 'name')"
/>
</h2>
</div>
</template>

<style module lang="scss">
.header {
display: flex;
align-items: center;
gap: var(--spacing-2xs);
margin-bottom: var(--spacing-l);
&:hover {
.editInputButton {
opacity: 1;
}
}
}
.title {
margin: 0;
flex-grow: 1;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-dark);
display: flex;
align-items: center;
max-width: 100%;
overflow: hidden;
.titleText {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.editInputButton {
--button-font-color: var(--prim-gray-490);
opacity: 0;
border: none;
}
.backButton {
--button-font-color: var(--color-text-light);
border: none;
}
</style>
Loading

0 comments on commit 132aa0b

Please sign in to comment.