P0: implement schema preview frontend with hardcoded schema

This commit is contained in:
Elias Meire 2024-11-28 17:35:29 +01:00
parent 41e9e39b5b
commit 280cc619c5
No known key found for this signature in database
6 changed files with 254 additions and 9 deletions

View file

@ -4,6 +4,7 @@ import {
CRON_NODE_TYPE,
INTERVAL_NODE_TYPE,
MANUAL_TRIGGER_NODE_TYPE,
PREVIEW_SCHEMAS,
START_NODE_TYPE,
} from '@/constants';
import { useNDVStore } from '@/stores/ndv.store';
@ -185,6 +186,10 @@ const currentNode = computed(() => {
return workflowsStore.getNodeByName(props.currentNodeName ?? '');
});
const hasSchemaPreview = computed(() => {
return Boolean(currentNode.value && PREVIEW_SCHEMAS.hasOwnProperty(currentNode.value.type));
});
const connectedCurrentNodeOutputs = computed(() => {
const search = parentNodes.value.find(({ name }) => name === props.currentNodeName);
return search?.indicies;
@ -428,9 +433,22 @@ function activatePane() {
@execute="onNodeExecute"
/>
</N8nTooltip>
<N8nText v-if="!readOnly" tag="div" size="small">
<N8nText v-if="!readOnly && !hasSchemaPreview" tag="div" size="small">
{{ i18n.baseText('ndv.input.noOutputData.hint') }}
</N8nText>
<div v-if="!readOnly && hasSchemaPreview" :class="$style.schemaPreview">
<N8nText tag="div" size="small">
{{ i18n.baseText('ndv.input.noOutputData.or') }}
</N8nText>
<N8nText tag="div" size="small">
<i18n-t keypath="ndv.input.noOutputData.schemaPreviewHint">
<template #schema>
<b>{{ i18n.baseText('runData.schema') }}</b>
</template>
</i18n-t>
</N8nText>
</div>
</div>
<div v-else :class="$style.notConnected">
<div>
@ -534,4 +552,10 @@ function activatePane() {
font-size: var(--font-size-s);
font-weight: var(--font-weight-bold);
}
.schemaPreview {
display: flex;
flex-flow: column nowrap;
gap: var(--spacing-2xs);
}
</style>

View file

@ -34,6 +34,7 @@ import {
MAX_DISPLAY_DATA_SIZE,
MAX_DISPLAY_DATA_SIZE_SCHEMA_VIEW,
NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND,
PREVIEW_SCHEMAS,
TEST_PIN_DATA,
} from '@/constants';
@ -274,6 +275,10 @@ const hasNodeRun = computed(() =>
),
);
const hasPreviewSchema = computed(() =>
Boolean(node.value && PREVIEW_SCHEMAS.hasOwnProperty(node.value.type)),
);
const isArtificialRecoveredEventItem = computed(
() => rawInputData.value?.[0]?.json?.isArtificialRecoveredEventItem,
);
@ -1318,7 +1323,8 @@ defineExpose({ enterEditMode });
<N8nRadioButtons
v-show="
hasNodeRun && (inputData.length || binaryData.length || search) && !editMode.enabled
hasPreviewSchema ||
(hasNodeRun && (inputData.length || binaryData.length || search) && !editMode.enabled)
"
:model-value="displayMode"
:options="displayModes"
@ -1562,7 +1568,7 @@ defineExpose({ enterEditMode });
</div>
<div
v-else-if="!hasNodeRun && !(displaysMultipleNodes && node?.disabled)"
v-else-if="!hasNodeRun && !(displaysMultipleNodes && (node?.disabled || hasPreviewSchema))"
:class="$style.center"
>
<slot name="node-not-run"></slot>
@ -1734,7 +1740,7 @@ defineExpose({ enterEditMode });
<LazyRunDataHtml :input-html="inputHtml" />
</Suspense>
<Suspense v-else-if="hasNodeRun && isSchemaView">
<Suspense v-else-if="(hasNodeRun || hasPreviewSchema) && isSchemaView">
<LazyRunDataSchema
:nodes="nodes"
:mapping-enabled="mappingEnabled"

View file

@ -22,6 +22,8 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { executionDataToJson } from '@/utils/nodeTypesUtils';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useDebounce } from '@/composables/useDebounce';
import { PREVIEW_SCHEMAS, DATA_EDITING_DOCS_URL } from '@/constants';
import { N8nNotice } from 'n8n-design-system';
type Props = {
nodes?: IConnectedNode[];
@ -46,6 +48,7 @@ type SchemaNode = {
connectedOutputIndexes: number[];
itemsCount: number | null;
schema: Schema | null;
isPreview: boolean;
};
const props = withDefaults(defineProps<Props>(), {
@ -64,7 +67,9 @@ const props = withDefaults(defineProps<Props>(), {
const draggingPath = ref<string>('');
const nodesOpen = ref<Partial<Record<string, boolean>>>({});
const nodesData = ref<Partial<Record<string, { schema: Schema; itemsCount: number }>>>({});
const nodesData = ref<
Partial<Record<string, { schema: Schema; itemsCount: number; isPreview: boolean }>>
>({});
const nodesLoading = ref<Partial<Record<string, boolean>>>({});
const disableScrollInView = ref(false);
@ -90,7 +95,10 @@ const nodes = computed(() => {
if (!fullNode) return null;
const nodeType = nodeTypesStore.getNodeType(fullNode.type, fullNode.typeVersion);
const { itemsCount, schema } = nodesData.value[node.name] ?? {
if (!nodeType) return null;
const { itemsCount, schema, isPreview } = nodesData.value[node.name] ?? {
itemsCount: null,
schema: null,
};
@ -102,8 +110,9 @@ const nodes = computed(() => {
itemsCount,
nodeType,
schema: schema ? filterSchema(schema, props.search) : null,
loading: nodesLoading.value[node.name],
open: nodesOpen.value[node.name],
loading: nodesLoading.value[node.name] ?? false,
open: nodesOpen.value[node.name] ?? false,
isPreview,
};
})
.filter((node): node is SchemaNode => !!(node?.node && node.nodeType));
@ -147,7 +156,7 @@ const noNodesOpen = computed(() => nodes.value.every((node) => !node.open));
const loadNodeData = async ({ node, connectedOutputIndexes }: SchemaNode) => {
const pinData = workflowsStore.pinDataByNodeName(node.name);
const data =
let data =
pinData ??
connectedOutputIndexes
.map((outputIndex) =>
@ -157,9 +166,19 @@ const loadNodeData = async ({ node, connectedOutputIndexes }: SchemaNode) => {
)
.flat();
if (data.length === 0 && PREVIEW_SCHEMAS.hasOwnProperty(node.type)) {
nodesData.value[node.name] = {
schema: PREVIEW_SCHEMAS[node.type],
itemsCount: 0,
isPreview: true,
};
return;
}
nodesData.value[node.name] = {
schema: getSchemaForExecutionData(data),
itemsCount: data.length,
isPreview: false,
};
};
@ -316,6 +335,9 @@ watch(
<span v-if="nodeAdditionalInfo(currentNode.node)" :class="$style.subtitle">{{
nodeAdditionalInfo(currentNode.node)
}}</span>
<span v-if="currentNode.isPreview" :class="$style.preview">
{{ i18n.baseText('dataMapping.schemaView.previewNode') }}
</span>
</div>
<font-awesome-icon
v-if="currentNode.nodeType.group.includes('trigger')"
@ -359,6 +381,16 @@ watch(
@transitionstart="(event) => onTransitionStart(event, currentNode.node.name)"
>
<div :class="$style.innerSchema" @transitionstart.stop>
<N8nNotice v-if="currentNode.isPreview" :class="$style.previewNotice" theme="warning">
<i18n-t keypath="dataMapping.schemaView.preview">
<template #link>
<N8nLink :to="DATA_EDITING_DOCS_URL" size="small">
{{ i18n.baseText('generic.learnMore') }}
</N8nLink>
</template>
</i18n-t>
</N8nNotice>
<div
v-if="currentNode.node.disabled"
:class="$style.notice"
@ -387,6 +419,7 @@ watch(
:distance-from-active="currentNode.depth"
:node="currentNode.node"
:search="search"
:preview="currentNode.isPreview"
/>
</div>
</div>
@ -594,6 +627,10 @@ watch(
transform 0.2s $ease-out-expo;
}
.preview {
color: var(--color-text-light);
}
.triggerIcon {
margin-left: var(--spacing-2xs);
color: var(--color-primary);
@ -605,6 +642,11 @@ watch(
}
}
.previewNotice {
margin-left: var(--spacing-l);
margin-top: 0;
}
@container schema (max-width: 24em) {
.depth {
display: none;

View file

@ -18,6 +18,7 @@ type Props = {
distanceFromActive?: number;
node: INodeUi | null;
search: string;
preview?: boolean;
};
const props = defineProps<Props>();
@ -86,6 +87,7 @@ const getIconBySchemaType = (type: Schema['type']): string => {
[$style.pill]: true,
[$style.mappable]: mappingEnabled,
[$style.highlight]: dragged,
[$style.preview]: preview,
}"
>
<span
@ -139,6 +141,7 @@ const getIconBySchemaType = (type: Schema['type']): string => {
:distance-from-active="distanceFromActive"
:node="node"
:search="search"
:preview="preview"
/>
</div>
</div>
@ -267,6 +270,19 @@ const getIconBySchemaType = (type: Schema['type']): string => {
}
}
}
&.preview {
border-style: dashed;
border-width: 1.5px;
.label {
color: var(--color-text-light);
}
.label > span {
border-left: 1.5px dashed var(--color-foreground-light);
}
}
}
.label {

View file

@ -4,6 +4,7 @@ import type {
INodeUi,
IWorkflowDataCreate,
NodeCreatorOpenSource,
Schema,
} from './Interface';
import { NodeConnectionType } from 'n8n-workflow';
import type {
@ -120,6 +121,7 @@ export const FUNCTION_NODE_TYPE = 'n8n-nodes-base.function';
export const GITHUB_TRIGGER_NODE_TYPE = 'n8n-nodes-base.githubTrigger';
export const GIT_NODE_TYPE = 'n8n-nodes-base.git';
export const GOOGLE_GMAIL_NODE_TYPE = 'n8n-nodes-base.gmail';
export const GOOGLE_GMAIL_TRIGGER_NODE_TYPE = 'n8n-nodes-base.gmailTrigger';
export const GOOGLE_SHEETS_NODE_TYPE = 'n8n-nodes-base.googleSheets';
export const ERROR_TRIGGER_NODE_TYPE = 'n8n-nodes-base.errorTrigger';
export const ELASTIC_SECURITY_NODE_TYPE = 'n8n-nodes-base.elasticSecurity';
@ -932,3 +934,154 @@ export const SAMPLE_SUBWORKFLOW_WORKFLOW: IWorkflowDataCreate = {
},
pinData: {},
};
export const PREVIEW_SCHEMAS: Record<string, Schema> = {
[OPEN_AI_NODE_TYPE]: {
type: 'object',
value: [
{
type: 'number',
key: 'index',
value: '',
path: '.index',
},
{
type: 'object',
key: 'message',
value: [
{
type: 'string',
key: 'role',
value: '',
path: '.message.role',
},
{
type: 'string',
key: 'content',
value: '',
path: '.message.content',
},
{
type: 'null',
key: 'refusal',
value: '',
path: 'message.refusal',
},
],
path: '.message',
},
{
type: 'null',
key: 'logprobs',
value: '',
path: '.logprobs',
},
{
type: 'string',
key: 'finish_reason',
value: '',
path: '.finish_reason',
},
],
path: '',
},
[GOOGLE_GMAIL_TRIGGER_NODE_TYPE]: {
type: 'object',
value: [
{
type: 'string',
key: 'id',
value: '',
path: '.id',
},
{
type: 'string',
key: 'threadId',
value: '',
path: '.threadId',
},
{
type: 'string',
key: 'snippet',
value: '',
path: '.snippet',
},
{
type: 'object',
key: 'payload',
value: [
{
type: 'string',
key: 'mimeType',
value: '',
path: 'payload.mimeType',
},
],
path: '.payload',
},
{
type: 'number',
key: 'sizeEstimate',
value: '',
path: '.sizeEstimate',
},
{
type: 'string',
key: 'historyId',
value: '',
path: '.historyId',
},
{
type: 'string',
key: 'internalDate',
value: '',
path: '.internalDate',
},
{
type: 'array',
key: 'labels',
value: [
{
type: 'object',
key: '0',
value: [
{
type: 'string',
key: 'id',
value: '',
path: '.labels[0].id',
},
{
type: 'string',
key: 'name',
value: '',
path: '.labels[0].name',
},
],
path: '.labels[0]',
},
],
path: '.labels',
},
{
type: 'string',
key: 'From',
value: '',
path: '.From',
},
{
type: 'string',
key: 'To',
value: '',
path: '.To',
},
{
type: 'string',
key: 'Subject',
value: '',
path: '.Subject',
},
],
path: '',
},
};

View file

@ -642,6 +642,8 @@
"dataMapping.schemaView.emptyData": "No fields - item(s) exist, but they're empty",
"dataMapping.schemaView.disabled": "This node is disabled and will just pass data through",
"dataMapping.schemaView.noMatches": "No results for '{search}'",
"dataMapping.schemaView.preview": "This is a preview of the schema, execute the node to see the exact schema and data. {link}",
"dataMapping.schemaView.previewNode": "(schema preview)",
"displayWithChange.cancelEdit": "Cancel Edit",
"displayWithChange.clickToChange": "Click to Change",
"displayWithChange.setValue": "Set Value",
@ -959,6 +961,8 @@
"ndv.input.noOutputData.executePrevious": "Execute previous nodes",
"ndv.input.noOutputData.title": "No input data yet",
"ndv.input.noOutputData.hint": "(From the earliest node that has no output data yet)",
"ndv.input.noOutputData.schemaPreviewHint": "switch to {schema} to use the schema preview",
"ndv.input.noOutputData.or": "or",
"ndv.input.executingPrevious": "Executing previous nodes...",
"ndv.input.notConnected.title": "Wire me up",
"ndv.input.notConnected.message": "This node can only receive input data if you connect it to another node.",