mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
2302 lines
59 KiB
Vue
2302 lines
59 KiB
Vue
<script setup lang="ts">
|
||
import { useStorage } from '@/composables/useStorage';
|
||
import { saveAs } from 'file-saver';
|
||
import NodeSettingsHint from '@/components/NodeSettingsHint.vue';
|
||
import type {
|
||
IBinaryData,
|
||
IConnectedNode,
|
||
IDataObject,
|
||
INodeExecutionData,
|
||
INodeOutputConfiguration,
|
||
IRunData,
|
||
IRunExecutionData,
|
||
ITaskMetadata,
|
||
NodeError,
|
||
NodeHint,
|
||
Workflow,
|
||
NodeConnectionType,
|
||
} from 'n8n-workflow';
|
||
import {
|
||
parseErrorMetadata,
|
||
NodeConnectionTypes,
|
||
NodeHelpers,
|
||
TRIMMED_TASK_DATA_CONNECTIONS_KEY,
|
||
} from 'n8n-workflow';
|
||
import { computed, defineAsyncComponent, onBeforeUnmount, onMounted, ref, toRef, watch } from 'vue';
|
||
|
||
import type { INodeUi, IRunDataDisplayMode, ITab, NodePanelType } from '@/Interface';
|
||
|
||
import {
|
||
CORE_NODES_CATEGORY,
|
||
DATA_EDITING_DOCS_URL,
|
||
DATA_PINNING_DOCS_URL,
|
||
HTML_NODE_TYPE,
|
||
LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG,
|
||
LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG,
|
||
MAX_DISPLAY_DATA_SIZE,
|
||
MAX_DISPLAY_DATA_SIZE_SCHEMA_VIEW,
|
||
NDV_UI_OVERHAUL_EXPERIMENT,
|
||
NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND,
|
||
RUN_DATA_DEFAULT_PAGE_SIZE,
|
||
TEST_PIN_DATA,
|
||
} from '@/constants';
|
||
|
||
import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
|
||
import NodeErrorView from '@/components/Error/NodeErrorView.vue';
|
||
import JsonEditor from '@/components/JsonEditor/JsonEditor.vue';
|
||
|
||
import RunDataPinButton from '@/components/RunDataPinButton.vue';
|
||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||
import { useI18n } from '@n8n/i18n';
|
||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||
import { useNodeType } from '@/composables/useNodeType';
|
||
import type { PinDataSource, UnpinDataSource } from '@/composables/usePinnedData';
|
||
import { usePinnedData } from '@/composables/usePinnedData';
|
||
import { useTelemetry } from '@/composables/useTelemetry';
|
||
import { useToast } from '@/composables/useToast';
|
||
import { dataPinningEventBus, ndvEventBus } from '@/event-bus';
|
||
import { useNDVStore } from '@/stores/ndv.store';
|
||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||
import { executionDataToJson } from '@/utils/nodeTypesUtils';
|
||
import { getGenericHints } from '@/utils/nodeViewUtils';
|
||
import { searchInObject } from '@/utils/objectUtils';
|
||
import { clearJsonKey, isEmpty, isPresent } from '@/utils/typesUtils';
|
||
import isEqual from 'lodash/isEqual';
|
||
import isObject from 'lodash/isObject';
|
||
import {
|
||
N8nBlockUi,
|
||
N8nButton,
|
||
N8nCallout,
|
||
N8nIconButton,
|
||
N8nInfoTip,
|
||
N8nLink,
|
||
N8nOption,
|
||
N8nSelect,
|
||
N8nSpinner,
|
||
N8nTabs,
|
||
N8nText,
|
||
N8nTooltip,
|
||
} from '@n8n/design-system';
|
||
import { storeToRefs } from 'pinia';
|
||
import { useRoute } from 'vue-router';
|
||
import { useUIStore } from '@/stores/ui.store';
|
||
import { useSchemaPreviewStore } from '@/stores/schemaPreview.store';
|
||
import { asyncComputed } from '@vueuse/core';
|
||
import ViewSubExecution from './ViewSubExecution.vue';
|
||
import RunDataItemCount from '@/components/RunDataItemCount.vue';
|
||
import RunDataDisplayModeSelect from '@/components/RunDataDisplayModeSelect.vue';
|
||
import RunDataPaginationBar from '@/components/RunDataPaginationBar.vue';
|
||
import { parseAiContent } from '@/utils/aiUtils';
|
||
import { usePostHog } from '@/stores/posthog.store';
|
||
import { I18nT } from 'vue-i18n';
|
||
import RunDataBinary from '@/components/RunDataBinary.vue';
|
||
|
||
const LazyRunDataTable = defineAsyncComponent(
|
||
async () => await import('@/components/RunDataTable.vue'),
|
||
);
|
||
const LazyRunDataJson = defineAsyncComponent(
|
||
async () => await import('@/components/RunDataJson.vue'),
|
||
);
|
||
|
||
const LazyRunDataSchema = defineAsyncComponent(
|
||
async () => await import('@/components/VirtualSchema.vue'),
|
||
);
|
||
const LazyRunDataHtml = defineAsyncComponent(
|
||
async () => await import('@/components/RunDataHtml.vue'),
|
||
);
|
||
const LazyRunDataAi = defineAsyncComponent(
|
||
async () => await import('@/components/RunDataParsedAiContent.vue'),
|
||
);
|
||
const LazyRunDataSearch = defineAsyncComponent(
|
||
async () => await import('@/components/RunDataSearch.vue'),
|
||
);
|
||
|
||
export type EnterEditModeArgs = {
|
||
origin: 'editIconButton' | 'insertTestDataLink';
|
||
};
|
||
|
||
type Props = {
|
||
workflowObject: Workflow;
|
||
workflowExecution?: IRunExecutionData;
|
||
runIndex: number;
|
||
tooMuchDataTitle: string;
|
||
executingMessage: string;
|
||
pushRef?: string;
|
||
paneType: NodePanelType;
|
||
displayMode: IRunDataDisplayMode;
|
||
noDataInBranchMessage: string;
|
||
node?: INodeUi | null;
|
||
nodes?: IConnectedNode[];
|
||
linkedRuns?: boolean;
|
||
canLinkRuns?: boolean;
|
||
isExecuting?: boolean;
|
||
overrideOutputs?: number[];
|
||
mappingEnabled?: boolean;
|
||
distanceFromActive?: number;
|
||
blockUI?: boolean;
|
||
isProductionExecutionPreview?: boolean;
|
||
isPaneActive?: boolean;
|
||
hidePagination?: boolean;
|
||
calloutMessage?: string;
|
||
disableRunIndexSelection?: boolean;
|
||
disableDisplayModeSelection?: boolean;
|
||
disableEdit?: boolean;
|
||
disablePin?: boolean;
|
||
compact?: boolean;
|
||
tableHeaderBgColor?: 'base' | 'light';
|
||
disableHoverHighlight?: boolean;
|
||
disableSettingsHint?: boolean;
|
||
disableAiContent?: boolean;
|
||
collapsingTableColumnName: string | null;
|
||
};
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
node: null,
|
||
nodes: () => [],
|
||
overrideOutputs: undefined,
|
||
distanceFromActive: 0,
|
||
blockUI: false,
|
||
isPaneActive: false,
|
||
isProductionExecutionPreview: false,
|
||
mappingEnabled: false,
|
||
isExecuting: false,
|
||
hidePagination: false,
|
||
calloutMessage: undefined,
|
||
disableRunIndexSelection: false,
|
||
disableDisplayModeSelection: false,
|
||
disableEdit: false,
|
||
disablePin: false,
|
||
disableHoverHighlight: false,
|
||
disableSettingsHint: false,
|
||
compact: false,
|
||
tableHeaderBgColor: 'base',
|
||
workflowExecution: undefined,
|
||
disableAiContent: false,
|
||
});
|
||
|
||
defineSlots<{
|
||
content: {};
|
||
'callout-message': {};
|
||
header: {};
|
||
'input-select': {};
|
||
'before-data': {};
|
||
'run-info': {};
|
||
'node-waiting': {};
|
||
'node-not-run': {};
|
||
'no-output-data': {};
|
||
'recovered-artificial-output-data': {};
|
||
}>();
|
||
|
||
const emit = defineEmits<{
|
||
search: [search: string];
|
||
runChange: [runIndex: number];
|
||
itemHover: [
|
||
item: {
|
||
outputIndex: number;
|
||
itemIndex: number;
|
||
} | null,
|
||
];
|
||
linkRun: [];
|
||
unlinkRun: [];
|
||
activatePane: [];
|
||
tableMounted: [
|
||
{
|
||
avgRowHeight: number;
|
||
},
|
||
];
|
||
displayModeChange: [IRunDataDisplayMode];
|
||
collapsingTableColumnChanged: [columnName: string | null];
|
||
captureWheelDataContainer: [WheelEvent];
|
||
}>();
|
||
|
||
const connectionType = ref<NodeConnectionType>(NodeConnectionTypes.Main);
|
||
const dataSize = ref(0);
|
||
const showData = ref(false);
|
||
const userEnabledShowData = ref(false);
|
||
const outputIndex = ref(0);
|
||
const binaryDataDisplayData = ref<IBinaryData | null>(null);
|
||
const currentPage = ref(1);
|
||
const pageSize = ref(10);
|
||
|
||
const pinDataDiscoveryTooltipVisible = ref(false);
|
||
const isControlledPinDataTooltip = ref(false);
|
||
const search = ref('');
|
||
|
||
const dataContainerRef = ref<HTMLDivElement>();
|
||
|
||
const nodeTypesStore = useNodeTypesStore();
|
||
const ndvStore = useNDVStore();
|
||
const workflowsStore = useWorkflowsStore();
|
||
const sourceControlStore = useSourceControlStore();
|
||
const rootStore = useRootStore();
|
||
const uiStore = useUIStore();
|
||
const schemaPreviewStore = useSchemaPreviewStore();
|
||
const posthogStore = usePostHog();
|
||
|
||
const toast = useToast();
|
||
const route = useRoute();
|
||
const nodeHelpers = useNodeHelpers();
|
||
const externalHooks = useExternalHooks();
|
||
const telemetry = useTelemetry();
|
||
const i18n = useI18n();
|
||
|
||
const node = toRef(props, 'node');
|
||
|
||
const pinnedData = usePinnedData(node, {
|
||
runIndex: props.runIndex,
|
||
displayMode: props.displayMode,
|
||
});
|
||
const { isSubNodeType } = useNodeType({
|
||
node,
|
||
});
|
||
|
||
const isArchivedWorkflow = computed(() => workflowsStore.workflow.isArchived);
|
||
const isReadOnlyRoute = computed(() => route.meta.readOnlyCanvas === true);
|
||
const isWaitNodeWaiting = computed(() => {
|
||
return (
|
||
node.value?.name &&
|
||
workflowExecution.value?.resultData?.runData?.[node.value?.name]?.[props.runIndex]
|
||
?.executionStatus === 'waiting'
|
||
);
|
||
});
|
||
|
||
const { activeNode } = storeToRefs(ndvStore);
|
||
const nodeType = computed(() => {
|
||
if (!node.value) return null;
|
||
|
||
return nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion);
|
||
});
|
||
|
||
const isSchemaView = computed(() => props.displayMode === 'schema');
|
||
const isSearchInSchemaView = computed(() => isSchemaView.value && !!search.value);
|
||
const hasMultipleInputNodes = computed(() => props.paneType === 'input' && props.nodes.length > 0);
|
||
const displaysMultipleNodes = computed(() => isSchemaView.value && hasMultipleInputNodes.value);
|
||
|
||
const isTriggerNode = computed(() => !!node.value && nodeTypesStore.isTriggerNode(node.value.type));
|
||
|
||
const canPinData = computed(
|
||
() =>
|
||
!!node.value &&
|
||
pinnedData.canPinNode(false, currentOutputIndex.value) &&
|
||
!isPaneTypeInput.value &&
|
||
pinnedData.isValidNodeType.value &&
|
||
!(binaryData.value && binaryData.value.length > 0),
|
||
);
|
||
|
||
const hasNodeRun = computed(() =>
|
||
Boolean(
|
||
!props.isExecuting &&
|
||
node.value &&
|
||
((workflowRunData.value && workflowRunData.value.hasOwnProperty(node.value.name)) ||
|
||
pinnedData.hasData.value),
|
||
),
|
||
);
|
||
|
||
const isArtificialRecoveredEventItem = computed(
|
||
() => rawInputData.value?.[0]?.json?.isArtificialRecoveredEventItem,
|
||
);
|
||
|
||
const isTrimmedManualExecutionDataItem = computed(
|
||
() => rawInputData.value?.[0]?.json?.[TRIMMED_TASK_DATA_CONNECTIONS_KEY],
|
||
);
|
||
|
||
const subworkflowExecutionError = computed(() => {
|
||
if (!node.value) return null;
|
||
return {
|
||
node: node.value,
|
||
messages: [workflowsStore.subWorkflowExecutionError?.message ?? ''],
|
||
} as NodeError;
|
||
});
|
||
|
||
const hasSubworkflowExecutionError = computed(() => !!workflowsStore.subWorkflowExecutionError);
|
||
|
||
// Sub-nodes may wish to display the parent node error as it can contain additional metadata
|
||
const parentNodeError = computed(() => {
|
||
const parentNode = props.workflowObject.getChildNodes(node.value?.name ?? '', 'ALL_NON_MAIN')[0];
|
||
return workflowRunData.value?.[parentNode]?.[props.runIndex]?.error as NodeError;
|
||
});
|
||
const workflowRunErrorAsNodeError = computed(() => {
|
||
if (!node.value) {
|
||
return null;
|
||
}
|
||
|
||
// If the node is a sub-node, we need to get the parent node error to check for input errors
|
||
if (isSubNodeType.value && props.paneType === 'input') {
|
||
return parentNodeError.value;
|
||
}
|
||
return workflowRunData.value?.[node.value?.name]?.[props.runIndex]?.error as NodeError;
|
||
});
|
||
|
||
const hasRunError = computed(() => Boolean(node.value && workflowRunErrorAsNodeError.value));
|
||
|
||
const executionHints = computed(() => {
|
||
if (hasNodeRun.value) {
|
||
const hints = node.value && workflowRunData.value?.[node.value.name]?.[props.runIndex]?.hints;
|
||
|
||
if (hints) return hints;
|
||
}
|
||
|
||
return [];
|
||
});
|
||
|
||
const workflowExecution = computed(
|
||
() => props.workflowExecution ?? workflowsStore.getWorkflowExecution?.data ?? undefined,
|
||
);
|
||
const workflowRunData = computed(() => {
|
||
if (workflowExecution.value === undefined) {
|
||
return null;
|
||
}
|
||
const executionData: IRunExecutionData | undefined = workflowExecution.value;
|
||
if (executionData?.resultData) {
|
||
return executionData.resultData.runData;
|
||
}
|
||
return null;
|
||
});
|
||
const dataCount = computed(() =>
|
||
getDataCount(props.runIndex, currentOutputIndex.value, connectionType.value),
|
||
);
|
||
|
||
const unfilteredDataCount = computed(() =>
|
||
pinnedData.data.value ? pinnedData.data.value.length : rawInputData.value.length,
|
||
);
|
||
const dataSizeInMB = computed(() => (dataSize.value / (1024 * 1024)).toFixed(1));
|
||
const maxOutputIndex = computed(() => {
|
||
if (node.value === null || props.runIndex === undefined) {
|
||
return 0;
|
||
}
|
||
|
||
const runData: IRunData | null = workflowRunData.value;
|
||
|
||
if (!runData?.hasOwnProperty(node.value.name)) {
|
||
return 0;
|
||
}
|
||
|
||
if (runData[node.value.name].length < props.runIndex) {
|
||
return 0;
|
||
}
|
||
|
||
if (runData[node.value.name][props.runIndex]) {
|
||
const taskData = runData[node.value.name][props.runIndex].data;
|
||
if (taskData?.main) {
|
||
return taskData.main.length - 1;
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
});
|
||
const currentPageOffset = computed(() => pageSize.value * (currentPage.value - 1));
|
||
const showBranchSwitch = computed(
|
||
() => maxOutputIndex.value > 0 && branches.value.length > 1 && !displaysMultipleNodes.value,
|
||
);
|
||
const maxRunIndex = computed(() => {
|
||
if (!node.value) {
|
||
return 0;
|
||
}
|
||
|
||
const runData: IRunData | null = workflowRunData.value;
|
||
|
||
if (!runData?.hasOwnProperty(node.value.name)) {
|
||
return 0;
|
||
}
|
||
|
||
if (runData[node.value.name].length) {
|
||
return runData[node.value.name].length - 1;
|
||
}
|
||
|
||
return 0;
|
||
});
|
||
|
||
const runSelectorOptionsCount = computed(() => {
|
||
if (!node.value) {
|
||
return 0;
|
||
}
|
||
|
||
const runData: IRunData | null = workflowRunData.value;
|
||
|
||
if (!runData?.hasOwnProperty(node.value.name)) {
|
||
return 0;
|
||
}
|
||
|
||
// If there is branch selector – we show all runs in the run selector
|
||
if (showBranchSwitch.value) {
|
||
return maxRunIndex.value + 1;
|
||
}
|
||
|
||
// If there is only one branch - we show only the runs containing the data in the connected branch
|
||
return runData[node.value.name].filter((nodeRun) => {
|
||
const nodeOutput = nodeRun?.data?.[connectionType.value]?.[currentOutputIndex.value];
|
||
return nodeOutput && nodeOutput?.length > 0;
|
||
}).length;
|
||
});
|
||
|
||
const rawInputData = computed(() =>
|
||
getRawInputData(props.runIndex, currentOutputIndex.value, connectionType.value),
|
||
);
|
||
|
||
const unfilteredInputData = computed(() => getPinDataOrLiveData(rawInputData.value));
|
||
const inputData = computed(() => getFilteredData(unfilteredInputData.value));
|
||
const inputDataPage = computed(() => {
|
||
const offset = pageSize.value * (currentPage.value - 1);
|
||
return inputData.value.slice(offset, offset + pageSize.value);
|
||
});
|
||
const jsonData = computed(() => executionDataToJson(inputData.value));
|
||
const binaryData = computed(() => {
|
||
if (!node.value) {
|
||
return [];
|
||
}
|
||
|
||
return nodeHelpers
|
||
.getBinaryData(workflowRunData.value, node.value.name, props.runIndex, currentOutputIndex.value)
|
||
.filter((data) => Boolean(data && Object.keys(data).length));
|
||
});
|
||
const inputHtml = computed(() => String(inputData.value[0]?.json?.html ?? ''));
|
||
const currentOutputIndex = computed(() => {
|
||
if (props.overrideOutputs?.length && !props.overrideOutputs.includes(outputIndex.value)) {
|
||
return props.overrideOutputs[0];
|
||
}
|
||
|
||
// In some cases nodes may switch their outputCount while the user still
|
||
// has a higher outputIndex selected. We could adjust outputIndex directly,
|
||
// but that loses data as we can keep the user selection if the branch reappears.
|
||
return Math.min(outputIndex.value, maxOutputIndex.value);
|
||
});
|
||
const branches = computed(() => {
|
||
const capitalize = (name: string) => name.charAt(0).toLocaleUpperCase() + name.slice(1);
|
||
|
||
const result: Array<ITab<number>> = [];
|
||
|
||
for (let i = 0; i <= maxOutputIndex.value; i++) {
|
||
if (props.overrideOutputs && !props.overrideOutputs.includes(i)) {
|
||
continue;
|
||
}
|
||
const totalItemsCount = getRawInputData(props.runIndex, i).length;
|
||
const itemsCount = getDataCount(props.runIndex, i);
|
||
const items = search.value
|
||
? i18n.baseText('ndv.search.items', {
|
||
adjustToNumber: totalItemsCount,
|
||
interpolate: { matched: itemsCount, total: totalItemsCount },
|
||
})
|
||
: i18n.baseText('ndv.output.items', {
|
||
adjustToNumber: itemsCount,
|
||
interpolate: { count: itemsCount },
|
||
});
|
||
let outputName = getOutputName(i);
|
||
|
||
if (`${outputName}` === `${i}`) {
|
||
outputName = `${i18n.baseText('ndv.output')} ${outputName}`;
|
||
} else {
|
||
const appendBranchWord = NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND.includes(
|
||
node.value?.type ?? '',
|
||
)
|
||
? ''
|
||
: ` ${i18n.baseText('ndv.output.branch')}`;
|
||
outputName = capitalize(`${getOutputName(i)}${appendBranchWord}`);
|
||
}
|
||
result.push({
|
||
label:
|
||
(search.value && itemsCount) || totalItemsCount ? `${outputName} (${items})` : outputName,
|
||
value: i,
|
||
});
|
||
}
|
||
return result;
|
||
});
|
||
|
||
const editMode = computed(() => {
|
||
return isPaneTypeInput.value ? { enabled: false, value: '' } : ndvStore.outputPanelEditMode;
|
||
});
|
||
|
||
const isPaneTypeInput = computed(() => props.paneType === 'input');
|
||
const isPaneTypeOutput = computed(() => props.paneType === 'output');
|
||
|
||
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
|
||
const showIOSearch = computed(
|
||
() =>
|
||
hasNodeRun.value &&
|
||
!hasRunError.value &&
|
||
(unfilteredInputData.value.length > 0 || displaysMultipleNodes.value),
|
||
);
|
||
const inputSelectLocation = computed(() => {
|
||
if (isSchemaView.value) return 'none';
|
||
if (!hasNodeRun.value) return 'header';
|
||
if (maxRunIndex.value > 0) return 'runs';
|
||
if (maxOutputIndex.value > 0 && branches.value.length > 1) {
|
||
return 'outputs';
|
||
}
|
||
|
||
return 'items';
|
||
});
|
||
|
||
const showIoSearchNoMatchContent = computed(
|
||
() =>
|
||
hasNodeRun.value && !inputData.value.length && !!search.value && !displaysMultipleNodes.value,
|
||
);
|
||
|
||
const parentNodeOutputData = computed(() => {
|
||
const parentNode = props.workflowObject.getParentNodesByDepth(node.value?.name ?? '')[0];
|
||
let parentNodeData: INodeExecutionData[] = [];
|
||
|
||
if (parentNode?.name) {
|
||
parentNodeData = nodeHelpers.getNodeInputData(
|
||
props.workflowObject.getNode(parentNode?.name),
|
||
props.runIndex,
|
||
outputIndex.value,
|
||
'input',
|
||
connectionType.value,
|
||
);
|
||
}
|
||
|
||
return parentNodeData;
|
||
});
|
||
|
||
const parentNodePinnedData = computed(() => {
|
||
const parentNode = props.workflowObject.getParentNodesByDepth(node.value?.name ?? '')[0];
|
||
return props.workflowObject.pinData?.[parentNode?.name || ''] ?? [];
|
||
});
|
||
|
||
const showPinButton = computed(() => {
|
||
if (props.disablePin) {
|
||
return false;
|
||
}
|
||
if (!rawInputData.value.length && !pinnedData.hasData.value) {
|
||
return false;
|
||
}
|
||
if (editMode.value.enabled) {
|
||
return false;
|
||
}
|
||
if (binaryData.value?.length) {
|
||
return isPaneTypeOutput.value;
|
||
}
|
||
return canPinData.value;
|
||
});
|
||
|
||
const pinButtonDisabled = computed(
|
||
() =>
|
||
(!rawInputData.value.length && !pinnedData.hasData.value) ||
|
||
!!binaryData.value?.length ||
|
||
isReadOnlyRoute.value ||
|
||
readOnlyEnv.value ||
|
||
isArchivedWorkflow.value,
|
||
);
|
||
|
||
const activeTaskMetadata = computed((): ITaskMetadata | null => {
|
||
if (!node.value) {
|
||
return null;
|
||
}
|
||
const errorMetadata = parseErrorMetadata(workflowRunErrorAsNodeError.value);
|
||
if (errorMetadata !== undefined) {
|
||
return errorMetadata;
|
||
}
|
||
|
||
// This is needed for the WorkflowRetriever to display the associated execution
|
||
if (parentNodeError.value) {
|
||
const subNodeMetadata = parseErrorMetadata(parentNodeError.value);
|
||
if (subNodeMetadata !== undefined) {
|
||
return subNodeMetadata;
|
||
}
|
||
}
|
||
|
||
return workflowRunData.value?.[node.value.name]?.[props.runIndex]?.metadata ?? null;
|
||
});
|
||
|
||
const hasInputOverwrite = computed((): boolean => {
|
||
if (!node.value) {
|
||
return false;
|
||
}
|
||
const taskData = nodeHelpers.getNodeTaskData(node.value.name, props.runIndex);
|
||
return Boolean(taskData?.inputOverride);
|
||
});
|
||
|
||
const isSchemaPreviewEnabled = computed(
|
||
() =>
|
||
props.paneType === 'input' &&
|
||
!(nodeType.value?.codex?.categories ?? []).some((category) => category === CORE_NODES_CATEGORY),
|
||
);
|
||
|
||
const isNDVV2 = computed(() =>
|
||
posthogStore.isVariantEnabled(
|
||
NDV_UI_OVERHAUL_EXPERIMENT.name,
|
||
NDV_UI_OVERHAUL_EXPERIMENT.variant,
|
||
),
|
||
);
|
||
|
||
const hasPreviewSchema = asyncComputed(async () => {
|
||
if (!isSchemaPreviewEnabled.value || props.nodes.length === 0) return false;
|
||
const nodes = props.nodes
|
||
.filter((n) => n.depth === 1)
|
||
.map((n) => workflowsStore.getNodeByName(n.name))
|
||
.filter(isPresent);
|
||
|
||
for (const connectedNode of nodes) {
|
||
const { type, typeVersion, parameters } = connectedNode;
|
||
const hasPreview = await schemaPreviewStore.getSchemaPreview({
|
||
nodeType: type,
|
||
version: typeVersion,
|
||
resource: parameters.resource as string,
|
||
operation: parameters.operation as string,
|
||
});
|
||
|
||
if (hasPreview.ok) return true;
|
||
}
|
||
return false;
|
||
}, false);
|
||
|
||
const itemsCountProps = computed<InstanceType<typeof RunDataItemCount>['$props']>(() => ({
|
||
search: search.value,
|
||
dataCount: dataCount.value,
|
||
unfilteredDataCount: unfilteredDataCount.value,
|
||
subExecutionsCount: activeTaskMetadata.value?.subExecutionsCount,
|
||
}));
|
||
|
||
const parsedAiContent = computed(() =>
|
||
props.disableAiContent ? [] : parseAiContent(rawInputData.value, connectionType.value),
|
||
);
|
||
|
||
const hasParsedAiContent = computed(() =>
|
||
parsedAiContent.value.some((prr) => prr.parsedContent?.parsed),
|
||
);
|
||
|
||
const binaryDataDisplayVisible = computed(
|
||
() => binaryDataDisplayData.value !== null && props.displayMode === 'binary',
|
||
);
|
||
|
||
function setInputBranchIndex(value: number) {
|
||
if (props.paneType === 'input') {
|
||
outputIndex.value = value;
|
||
}
|
||
}
|
||
|
||
watch(node, (newNode, prevNode) => {
|
||
if (newNode?.id === prevNode?.id) return;
|
||
init();
|
||
});
|
||
|
||
watch(hasNodeRun, () => {
|
||
if (props.paneType === 'output') setDisplayMode();
|
||
else {
|
||
// InputPanel relies on the outputIndex to check if we have data
|
||
outputIndex.value = determineInitialOutputIndex();
|
||
}
|
||
});
|
||
|
||
watch(
|
||
inputDataPage,
|
||
(data: INodeExecutionData[]) => {
|
||
if (props.paneType && data) {
|
||
ndvStore.setNDVPanelDataIsEmpty({
|
||
panel: props.paneType,
|
||
isEmpty: data.every((item) => isEmpty(item.json)),
|
||
});
|
||
}
|
||
},
|
||
{ immediate: true, deep: true },
|
||
);
|
||
|
||
watch(jsonData, (data: IDataObject[], prevData: IDataObject[]) => {
|
||
if (isEqual(data, prevData)) return;
|
||
refreshDataSize();
|
||
if (dataCount.value) {
|
||
resetCurrentPageIfTooFar();
|
||
}
|
||
showPinDataDiscoveryTooltip(data);
|
||
});
|
||
|
||
watch(binaryData, (newData, prevData) => {
|
||
if (newData.length && !prevData.length && props.displayMode !== 'binary') {
|
||
switchToBinary();
|
||
} else if (!newData.length && props.displayMode === 'binary') {
|
||
onDisplayModeChange('table');
|
||
}
|
||
});
|
||
|
||
watch(currentOutputIndex, (branchIndex: number) => {
|
||
ndvStore.setNDVBranchIndex({
|
||
pane: props.paneType,
|
||
branchIndex,
|
||
});
|
||
});
|
||
|
||
watch(search, (newSearch) => {
|
||
emit('search', newSearch);
|
||
});
|
||
|
||
// Switch to AI display mode if it's most suitable
|
||
watch(
|
||
hasParsedAiContent,
|
||
(hasAiContent) => {
|
||
if (hasAiContent && props.displayMode !== 'ai') {
|
||
emit('displayModeChange', 'ai');
|
||
}
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
onMounted(() => {
|
||
init();
|
||
|
||
ndvEventBus.on('setInputBranchIndex', setInputBranchIndex);
|
||
|
||
if (!isPaneTypeInput.value) {
|
||
showPinDataDiscoveryTooltip(jsonData.value);
|
||
}
|
||
ndvStore.setNDVBranchIndex({
|
||
pane: props.paneType,
|
||
branchIndex: currentOutputIndex.value,
|
||
});
|
||
|
||
if (props.paneType === 'output') {
|
||
activatePane();
|
||
}
|
||
|
||
if (hasRunError.value && node.value) {
|
||
const error = workflowRunData.value?.[node.value.name]?.[props.runIndex]?.error;
|
||
const errorsToTrack = ['unknown error'];
|
||
|
||
if (error && errorsToTrack.some((e) => error.message?.toLowerCase().includes(e))) {
|
||
telemetry.track('User encountered an error', {
|
||
node: node.value.type,
|
||
errorMessage: error.message,
|
||
nodeVersion: node.value.typeVersion,
|
||
n8nVersion: rootStore.versionCli,
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
hidePinDataDiscoveryTooltip();
|
||
ndvEventBus.off('setInputBranchIndex', setInputBranchIndex);
|
||
});
|
||
|
||
function getResolvedNodeOutputs() {
|
||
if (node.value && nodeType.value) {
|
||
const workflowNode = props.workflowObject.getNode(node.value.name);
|
||
|
||
if (workflowNode) {
|
||
const outputs = NodeHelpers.getNodeOutputs(
|
||
props.workflowObject,
|
||
workflowNode,
|
||
nodeType.value,
|
||
);
|
||
return outputs;
|
||
}
|
||
}
|
||
return [];
|
||
}
|
||
function shouldHintBeDisplayed(hint: NodeHint): boolean {
|
||
const { location, whenToDisplay } = hint;
|
||
|
||
if (location) {
|
||
if (location === 'ndv' && !['input', 'output'].includes(props.paneType)) {
|
||
return false;
|
||
}
|
||
if (location === 'inputPane' && props.paneType !== 'input') {
|
||
return false;
|
||
}
|
||
|
||
if (location === 'outputPane' && props.paneType !== 'output') {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (whenToDisplay === 'afterExecution' && !hasNodeRun.value) {
|
||
return false;
|
||
}
|
||
|
||
if (whenToDisplay === 'beforeExecution' && hasNodeRun.value) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
const nodeHints = computed<NodeHint[]>(() => {
|
||
try {
|
||
if (node.value && nodeType.value) {
|
||
const workflowNode = props.workflowObject.getNode(node.value.name);
|
||
|
||
if (workflowNode) {
|
||
const hints = nodeHelpers.getNodeHints(props.workflowObject, workflowNode, nodeType.value, {
|
||
runExecutionData: workflowExecution.value ?? null,
|
||
runIndex: props.runIndex,
|
||
connectionInputData: parentNodeOutputData.value,
|
||
});
|
||
|
||
const hasMultipleInputItems =
|
||
parentNodeOutputData.value.length > 1 || parentNodePinnedData.value.length > 1;
|
||
|
||
const nodeOutputData =
|
||
workflowRunData.value?.[node.value.name]?.[props.runIndex]?.data?.main?.[0] ?? [];
|
||
|
||
const genericHints = getGenericHints({
|
||
workflowNode,
|
||
node: node.value,
|
||
nodeType: nodeType.value,
|
||
nodeOutputData,
|
||
nodes: props.workflowObject.nodes,
|
||
connections: props.workflowObject.connectionsBySourceNode,
|
||
hasNodeRun: hasNodeRun.value,
|
||
hasMultipleInputItems,
|
||
});
|
||
|
||
return executionHints.value.concat(hints, genericHints).filter(shouldHintBeDisplayed);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error while getting node hints', error);
|
||
}
|
||
|
||
return [];
|
||
});
|
||
|
||
function onItemHover(itemIndex: number | null) {
|
||
if (itemIndex === null) {
|
||
emit('itemHover', null);
|
||
|
||
return;
|
||
}
|
||
emit('itemHover', {
|
||
outputIndex: currentOutputIndex.value,
|
||
itemIndex,
|
||
});
|
||
}
|
||
|
||
function onClickDataPinningDocsLink() {
|
||
telemetry.track('User clicked ndv link', {
|
||
workflow_id: workflowsStore.workflowId,
|
||
push_ref: props.pushRef,
|
||
node_type: activeNode.value?.type,
|
||
pane: 'output',
|
||
type: 'data-pinning-docs',
|
||
});
|
||
}
|
||
|
||
function showPinDataDiscoveryTooltip(value: IDataObject[]) {
|
||
if (!isTriggerNode.value) {
|
||
return;
|
||
}
|
||
|
||
const pinDataDiscoveryFlag = useStorage(LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG).value;
|
||
|
||
if (
|
||
value &&
|
||
value.length > 0 &&
|
||
!isReadOnlyRoute.value &&
|
||
!isArchivedWorkflow.value &&
|
||
!pinDataDiscoveryFlag
|
||
) {
|
||
pinDataDiscoveryComplete();
|
||
|
||
setTimeout(() => {
|
||
isControlledPinDataTooltip.value = true;
|
||
pinDataDiscoveryTooltipVisible.value = true;
|
||
dataPinningEventBus.emit('data-pinning-discovery', { isTooltipVisible: true });
|
||
}, 500); // Wait for NDV to open
|
||
}
|
||
}
|
||
|
||
function hidePinDataDiscoveryTooltip() {
|
||
if (pinDataDiscoveryTooltipVisible.value) {
|
||
isControlledPinDataTooltip.value = false;
|
||
pinDataDiscoveryTooltipVisible.value = false;
|
||
dataPinningEventBus.emit('data-pinning-discovery', { isTooltipVisible: false });
|
||
}
|
||
}
|
||
|
||
function pinDataDiscoveryComplete() {
|
||
useStorage(LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG).value = 'true';
|
||
useStorage(LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG).value = 'true';
|
||
}
|
||
|
||
function enterEditMode({ origin }: EnterEditModeArgs) {
|
||
const inputData = pinnedData.data.value
|
||
? clearJsonKey(pinnedData.data.value)
|
||
: executionDataToJson(rawInputData.value);
|
||
|
||
const inputDataLength = Array.isArray(inputData)
|
||
? inputData.length
|
||
: Object.keys(inputData ?? {}).length;
|
||
|
||
const data = inputDataLength > 0 ? inputData : TEST_PIN_DATA;
|
||
|
||
ndvStore.setOutputPanelEditModeEnabled(true);
|
||
ndvStore.setOutputPanelEditModeValue(JSON.stringify(data, null, 2));
|
||
|
||
telemetry.track('User opened ndv edit state', {
|
||
node_type: activeNode.value?.type,
|
||
click_type: origin === 'editIconButton' ? 'button' : 'link',
|
||
push_ref: props.pushRef,
|
||
run_index: props.runIndex,
|
||
is_output_present: hasNodeRun.value || pinnedData.hasData.value,
|
||
view: !hasNodeRun.value && !pinnedData.hasData.value ? 'undefined' : props.displayMode,
|
||
is_data_pinned: pinnedData.hasData.value,
|
||
});
|
||
}
|
||
|
||
function onClickCancelEdit() {
|
||
ndvStore.setOutputPanelEditModeEnabled(false);
|
||
ndvStore.setOutputPanelEditModeValue('');
|
||
onExitEditMode({ type: 'cancel' });
|
||
}
|
||
|
||
function onClickSaveEdit() {
|
||
if (!node.value) {
|
||
return;
|
||
}
|
||
|
||
const { value } = editMode.value;
|
||
|
||
toast.clearAllStickyNotifications();
|
||
|
||
try {
|
||
const clearedValue = clearJsonKey(value) as INodeExecutionData[];
|
||
try {
|
||
pinnedData.setData(clearedValue, 'save-edit');
|
||
} catch (error) {
|
||
// setData function already shows toasts on error, so just return here
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
toast.showError(error, i18n.baseText('ndv.pinData.error.syntaxError.title'));
|
||
return;
|
||
}
|
||
|
||
ndvStore.setOutputPanelEditModeEnabled(false);
|
||
|
||
onExitEditMode({ type: 'save' });
|
||
}
|
||
|
||
function onExitEditMode({ type }: { type: 'save' | 'cancel' }) {
|
||
telemetry.track('User closed ndv edit state', {
|
||
node_type: activeNode.value?.type,
|
||
push_ref: props.pushRef,
|
||
run_index: props.runIndex,
|
||
view: props.displayMode,
|
||
type,
|
||
});
|
||
}
|
||
|
||
async function onTogglePinData({ source }: { source: PinDataSource | UnpinDataSource }) {
|
||
if (!node.value) {
|
||
return;
|
||
}
|
||
|
||
if (source === 'pin-icon-click') {
|
||
const telemetryPayload = {
|
||
node_type: activeNode.value?.type,
|
||
push_ref: props.pushRef,
|
||
run_index: props.runIndex,
|
||
view: !hasNodeRun.value && !pinnedData.hasData.value ? 'none' : props.displayMode,
|
||
};
|
||
|
||
void externalHooks.run('runData.onTogglePinData', telemetryPayload);
|
||
telemetry.track('User clicked pin data icon', telemetryPayload);
|
||
}
|
||
|
||
nodeHelpers.updateNodeParameterIssues(node.value);
|
||
|
||
if (pinnedData.hasData.value) {
|
||
pinnedData.unsetData(source);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
pinnedData.setData(rawInputData.value, 'pin-icon-click');
|
||
} catch (error) {
|
||
console.error(error);
|
||
return;
|
||
}
|
||
|
||
if (maxRunIndex.value > 0) {
|
||
toast.showToast({
|
||
title: i18n.baseText('ndv.pinData.pin.multipleRuns.title', {
|
||
interpolate: {
|
||
index: `${props.runIndex}`,
|
||
},
|
||
}),
|
||
message: i18n.baseText('ndv.pinData.pin.multipleRuns.description'),
|
||
type: 'success',
|
||
duration: 2000,
|
||
});
|
||
}
|
||
|
||
hidePinDataDiscoveryTooltip();
|
||
pinDataDiscoveryComplete();
|
||
}
|
||
|
||
function switchToBinary() {
|
||
onDisplayModeChange('binary');
|
||
}
|
||
|
||
function onBranchChange(value: number) {
|
||
outputIndex.value = value;
|
||
|
||
telemetry.track('User changed ndv branch', {
|
||
push_ref: props.pushRef,
|
||
branch_index: value,
|
||
node_type: activeNode.value?.type,
|
||
node_type_input_selection: nodeType.value ? nodeType.value.name : '',
|
||
pane: props.paneType,
|
||
});
|
||
}
|
||
|
||
function showTooMuchData() {
|
||
showData.value = true;
|
||
userEnabledShowData.value = true;
|
||
telemetry.track('User clicked ndv button', {
|
||
node_type: activeNode.value?.type,
|
||
workflow_id: workflowsStore.workflowId,
|
||
push_ref: props.pushRef,
|
||
pane: props.paneType,
|
||
type: 'showTooMuchData',
|
||
});
|
||
}
|
||
|
||
function toggleLinkRuns() {
|
||
if (props.linkedRuns) {
|
||
unlinkRun();
|
||
} else {
|
||
linkRun();
|
||
}
|
||
}
|
||
|
||
function linkRun() {
|
||
emit('linkRun');
|
||
}
|
||
|
||
function unlinkRun() {
|
||
emit('unlinkRun');
|
||
}
|
||
|
||
function onCurrentPageChange(value: number) {
|
||
currentPage.value = value;
|
||
telemetry.track('User changed ndv page', {
|
||
node_type: activeNode.value?.type,
|
||
workflow_id: workflowsStore.workflowId,
|
||
push_ref: props.pushRef,
|
||
pane: props.paneType,
|
||
page_selected: currentPage.value,
|
||
page_size: pageSize.value,
|
||
items_total: dataCount.value,
|
||
});
|
||
}
|
||
|
||
function resetCurrentPageIfTooFar() {
|
||
const maxPage = Math.ceil(dataCount.value / pageSize.value);
|
||
if (maxPage < currentPage.value) {
|
||
currentPage.value = maxPage;
|
||
}
|
||
}
|
||
|
||
function onPageSizeChange(newPageSize: number) {
|
||
pageSize.value = newPageSize;
|
||
|
||
resetCurrentPageIfTooFar();
|
||
|
||
telemetry.track('User changed ndv page size', {
|
||
node_type: activeNode.value?.type,
|
||
workflow_id: workflowsStore.workflowId,
|
||
push_ref: props.pushRef,
|
||
pane: props.paneType,
|
||
page_selected: currentPage.value,
|
||
page_size: pageSize.value,
|
||
items_total: dataCount.value,
|
||
});
|
||
}
|
||
|
||
function onDisplayModeChange(newDisplayMode: IRunDataDisplayMode) {
|
||
const previous = props.displayMode;
|
||
emit('displayModeChange', newDisplayMode);
|
||
|
||
if (!userEnabledShowData.value) updateShowData();
|
||
|
||
if (dataContainerRef.value) {
|
||
const dataDisplay = dataContainerRef.value.children[0];
|
||
|
||
if (dataDisplay) {
|
||
dataDisplay.scrollTo(0, 0);
|
||
}
|
||
}
|
||
|
||
closeBinaryDataDisplay();
|
||
void externalHooks.run('runData.displayModeChanged', {
|
||
newValue: newDisplayMode,
|
||
oldValue: previous,
|
||
});
|
||
if (activeNode.value) {
|
||
telemetry.track('User changed ndv item view', {
|
||
previous_view: previous,
|
||
new_view: newDisplayMode,
|
||
node_type: activeNode.value.type,
|
||
workflow_id: workflowsStore.workflowId,
|
||
push_ref: props.pushRef,
|
||
pane: props.paneType,
|
||
});
|
||
}
|
||
}
|
||
|
||
function getRunLabel(option: number) {
|
||
if (!node.value) {
|
||
return;
|
||
}
|
||
|
||
let itemsCount = 0;
|
||
for (let i = 0; i <= maxOutputIndex.value; i++) {
|
||
itemsCount += getPinDataOrLiveData(getRawInputData(option - 1, i)).length;
|
||
}
|
||
const items = i18n.baseText('ndv.output.items', {
|
||
adjustToNumber: itemsCount,
|
||
interpolate: { count: itemsCount },
|
||
});
|
||
|
||
const metadata = workflowRunData.value?.[node.value.name]?.[option - 1]?.metadata ?? null;
|
||
const subexecutions = metadata?.subExecutionsCount
|
||
? i18n.baseText('ndv.output.andSubExecutions', {
|
||
adjustToNumber: metadata.subExecutionsCount,
|
||
interpolate: {
|
||
count: metadata.subExecutionsCount,
|
||
},
|
||
})
|
||
: '';
|
||
|
||
const itemsLabel = itemsCount > 0 ? ` (${items}${subexecutions})` : '';
|
||
return option + i18n.baseText('ndv.output.of') + runSelectorOptionsCount.value + itemsLabel;
|
||
}
|
||
|
||
function getRawInputData(
|
||
runIndex: number,
|
||
outputIndex: number,
|
||
connectionType: NodeConnectionType = NodeConnectionTypes.Main,
|
||
): INodeExecutionData[] {
|
||
let inputData: INodeExecutionData[] = [];
|
||
|
||
if (node.value) {
|
||
inputData = nodeHelpers.getNodeInputData(
|
||
node.value,
|
||
runIndex,
|
||
outputIndex,
|
||
props.paneType,
|
||
connectionType,
|
||
workflowExecution.value,
|
||
);
|
||
}
|
||
|
||
if (inputData.length === 0 || !Array.isArray(inputData)) {
|
||
return [];
|
||
}
|
||
|
||
return inputData;
|
||
}
|
||
|
||
function getPinDataOrLiveData(data: INodeExecutionData[]): INodeExecutionData[] {
|
||
if (pinnedData.data.value && !props.isProductionExecutionPreview) {
|
||
return Array.isArray(pinnedData.data.value)
|
||
? pinnedData.data.value.map((value) => ({
|
||
json: value,
|
||
}))
|
||
: [
|
||
{
|
||
json: pinnedData.data.value,
|
||
},
|
||
];
|
||
}
|
||
return data;
|
||
}
|
||
|
||
function getFilteredData(data: INodeExecutionData[]): INodeExecutionData[] {
|
||
if (!search.value || isSchemaView.value) {
|
||
return data;
|
||
}
|
||
|
||
currentPage.value = 1;
|
||
return data.filter(({ json }) => searchInObject(json, search.value));
|
||
}
|
||
|
||
function getDataCount(
|
||
runIndex: number,
|
||
outputIndex: number,
|
||
connectionType: NodeConnectionType = NodeConnectionTypes.Main,
|
||
) {
|
||
if (!node.value) {
|
||
return 0;
|
||
}
|
||
|
||
if (workflowRunData.value?.[node.value.name]?.[runIndex]?.hasOwnProperty('error')) {
|
||
return 1;
|
||
}
|
||
|
||
const rawInputData = getRawInputData(runIndex, outputIndex, connectionType);
|
||
const pinOrLiveData = getPinDataOrLiveData(rawInputData);
|
||
return getFilteredData(pinOrLiveData).length;
|
||
}
|
||
|
||
function determineInitialOutputIndex() {
|
||
for (let i = 0; i <= maxOutputIndex.value; i++) {
|
||
if (getRawInputData(props.runIndex, i).length) {
|
||
return i;
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
function init() {
|
||
// Reset the selected output index every time another node gets selected
|
||
outputIndex.value = determineInitialOutputIndex();
|
||
refreshDataSize();
|
||
closeBinaryDataDisplay();
|
||
|
||
let outputTypes: NodeConnectionType[] = [];
|
||
if (node.value && nodeType.value) {
|
||
const outputs = getResolvedNodeOutputs();
|
||
outputTypes = NodeHelpers.getConnectionTypes(outputs);
|
||
}
|
||
connectionType.value = outputTypes.length === 0 ? NodeConnectionTypes.Main : outputTypes[0];
|
||
if (binaryData.value.length > 0) {
|
||
emit('displayModeChange', 'binary');
|
||
} else if (props.displayMode === 'binary') {
|
||
emit('displayModeChange', 'schema');
|
||
}
|
||
|
||
if (isNDVV2.value) {
|
||
pageSize.value = RUN_DATA_DEFAULT_PAGE_SIZE;
|
||
}
|
||
|
||
if (props.paneType === 'output') {
|
||
setDisplayMode();
|
||
}
|
||
}
|
||
|
||
function closeBinaryDataDisplay() {
|
||
binaryDataDisplayData.value = null;
|
||
}
|
||
|
||
function downloadJsonData() {
|
||
const fileName = (node.value?.name ?? '').replace(/[^\w\d]/g, '_');
|
||
const blob = new Blob([JSON.stringify(rawInputData.value, null, 2)], {
|
||
type: 'application/json',
|
||
});
|
||
|
||
saveAs(blob, `${fileName}.json`);
|
||
}
|
||
|
||
function displayBinaryData(index: number, key: string | number) {
|
||
const { data, mimeType } = binaryData.value[index][key];
|
||
|
||
binaryDataDisplayData.value = {
|
||
node: node.value?.name,
|
||
runIndex: props.runIndex,
|
||
outputIndex: currentOutputIndex.value,
|
||
index,
|
||
key,
|
||
data,
|
||
mimeType,
|
||
};
|
||
}
|
||
|
||
function getOutputName(outputIndex: number) {
|
||
if (node.value === null) {
|
||
return outputIndex + 1;
|
||
}
|
||
|
||
const outputs = getResolvedNodeOutputs();
|
||
const outputConfiguration = outputs?.[outputIndex] as INodeOutputConfiguration;
|
||
|
||
if (outputConfiguration && isObject(outputConfiguration)) {
|
||
return outputConfiguration?.displayName;
|
||
}
|
||
if (!nodeType.value?.outputNames || nodeType.value.outputNames.length <= outputIndex) {
|
||
return outputIndex + 1;
|
||
}
|
||
|
||
return nodeType.value.outputNames[outputIndex];
|
||
}
|
||
|
||
function refreshDataSize() {
|
||
// Hide by default the data from being displayed
|
||
showData.value = false;
|
||
const jsonItems = inputDataPage.value.map((item) => item.json);
|
||
const byteSize = new Blob([JSON.stringify(jsonItems)]).size;
|
||
dataSize.value = byteSize;
|
||
updateShowData();
|
||
}
|
||
|
||
function updateShowData() {
|
||
// Display data if it is reasonably small (< 1MB)
|
||
showData.value =
|
||
(isSchemaView.value && dataSize.value < MAX_DISPLAY_DATA_SIZE_SCHEMA_VIEW) ||
|
||
dataSize.value < MAX_DISPLAY_DATA_SIZE;
|
||
}
|
||
|
||
function onRunIndexChange(run: number) {
|
||
emit('runChange', run);
|
||
}
|
||
|
||
function enableNode() {
|
||
if (node.value) {
|
||
const updateInformation = {
|
||
name: node.value.name,
|
||
properties: {
|
||
disabled: !node.value.disabled,
|
||
},
|
||
};
|
||
|
||
workflowsStore.updateNodeProperties(updateInformation);
|
||
}
|
||
}
|
||
|
||
const shouldDisplayHtml = computed(
|
||
() =>
|
||
node.value?.type === HTML_NODE_TYPE &&
|
||
node.value.parameters.operation === 'generateHtmlTemplate',
|
||
);
|
||
|
||
function setDisplayMode() {
|
||
if (shouldDisplayHtml.value) {
|
||
emit('displayModeChange', 'html');
|
||
}
|
||
}
|
||
|
||
function activatePane() {
|
||
emit('activatePane');
|
||
}
|
||
|
||
function onSearchClear() {
|
||
search.value = '';
|
||
document.dispatchEvent(new KeyboardEvent('keyup', { key: '/' }));
|
||
}
|
||
|
||
defineExpose({ enterEditMode });
|
||
</script>
|
||
|
||
<template>
|
||
<div
|
||
:class="[
|
||
'run-data',
|
||
$style.container,
|
||
{ [$style['ndv-v2']]: isNDVV2, [$style.compact]: compact },
|
||
]"
|
||
@mouseover="activatePane"
|
||
>
|
||
<N8nCallout
|
||
v-if="
|
||
!isPaneTypeInput &&
|
||
pinnedData.hasData.value &&
|
||
!editMode.enabled &&
|
||
!isProductionExecutionPreview
|
||
"
|
||
theme="secondary"
|
||
icon="pin"
|
||
:class="$style.pinnedDataCallout"
|
||
data-test-id="ndv-pinned-data-callout"
|
||
>
|
||
{{ i18n.baseText('runData.pindata.thisDataIsPinned') }}
|
||
<span v-if="!isReadOnlyRoute && !isArchivedWorkflow && !readOnlyEnv" class="ml-4xs">
|
||
<N8nLink
|
||
theme="secondary"
|
||
size="small"
|
||
underline
|
||
bold
|
||
data-test-id="ndv-unpin-data"
|
||
@click.stop="onTogglePinData({ source: 'banner-link' })"
|
||
>
|
||
{{ i18n.baseText('runData.pindata.unpin') }}
|
||
</N8nLink>
|
||
</span>
|
||
<template #trailingContent>
|
||
<N8nLink
|
||
:to="DATA_PINNING_DOCS_URL"
|
||
size="small"
|
||
theme="secondary"
|
||
bold
|
||
underline
|
||
@click="onClickDataPinningDocsLink"
|
||
>
|
||
{{ i18n.baseText('runData.pindata.learnMore') }}
|
||
</N8nLink>
|
||
</template>
|
||
</N8nCallout>
|
||
|
||
<div :class="$style.header">
|
||
<div :class="$style.title">
|
||
<slot name="header"></slot>
|
||
</div>
|
||
|
||
<div
|
||
v-show="!hasRunError && !isTrimmedManualExecutionDataItem"
|
||
:class="$style.displayModes"
|
||
data-test-id="run-data-pane-header"
|
||
@click.stop
|
||
>
|
||
<Suspense>
|
||
<LazyRunDataSearch
|
||
v-if="showIOSearch"
|
||
v-model="search"
|
||
:class="$style.search"
|
||
:pane-type="paneType"
|
||
:display-mode="displayMode"
|
||
:is-area-active="isPaneActive"
|
||
@focus="activatePane"
|
||
/>
|
||
</Suspense>
|
||
|
||
<N8nIconButton
|
||
v-if="displayMode === 'table' && collapsingTableColumnName !== null"
|
||
:class="$style.resetCollapseButton"
|
||
text
|
||
icon="chevrons-up-down"
|
||
size="xmini"
|
||
type="tertiary"
|
||
@click="emit('collapsingTableColumnChanged', null)"
|
||
/>
|
||
|
||
<RunDataDisplayModeSelect
|
||
v-if="!disableDisplayModeSelection"
|
||
v-show="
|
||
hasPreviewSchema ||
|
||
(hasNodeRun &&
|
||
(inputData.length || binaryData.length || search || hasMultipleInputNodes) &&
|
||
!editMode.enabled)
|
||
"
|
||
:compact="props.compact"
|
||
:value="displayMode"
|
||
:has-binary-data="binaryData.length > 0"
|
||
:pane-type="paneType"
|
||
:node-generates-html="shouldDisplayHtml"
|
||
:has-renderable-data="hasParsedAiContent"
|
||
@change="onDisplayModeChange"
|
||
/>
|
||
|
||
<N8nIconButton
|
||
v-if="!props.disableEdit && canPinData && !isReadOnlyRoute && !readOnlyEnv"
|
||
v-show="!editMode.enabled"
|
||
:title="i18n.baseText('runData.editOutput')"
|
||
:circle="false"
|
||
:disabled="node?.disabled"
|
||
icon="pencil"
|
||
type="tertiary"
|
||
data-test-id="ndv-edit-pinned-data"
|
||
@click="enterEditMode({ origin: 'editIconButton' })"
|
||
/>
|
||
|
||
<RunDataPinButton
|
||
v-if="showPinButton"
|
||
:disabled="pinButtonDisabled"
|
||
:tooltip-contents-visibility="{
|
||
binaryDataTooltipContent: !!binaryData?.length,
|
||
pinDataDiscoveryTooltipContent:
|
||
isControlledPinDataTooltip && pinDataDiscoveryTooltipVisible,
|
||
}"
|
||
:data-pinning-docs-url="DATA_PINNING_DOCS_URL"
|
||
:pinned-data="pinnedData"
|
||
@toggle-pin-data="onTogglePinData({ source: 'pin-icon-click' })"
|
||
/>
|
||
|
||
<div v-if="!props.disableEdit" v-show="editMode.enabled" :class="$style.editModeActions">
|
||
<N8nButton
|
||
type="tertiary"
|
||
:label="i18n.baseText('runData.editor.cancel')"
|
||
@click="onClickCancelEdit"
|
||
/>
|
||
<N8nButton
|
||
class="ml-2xs"
|
||
type="primary"
|
||
:label="i18n.baseText('runData.editor.save')"
|
||
@click="onClickSaveEdit"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<RunDataItemCount v-if="props.compact" v-bind="itemsCountProps" />
|
||
</div>
|
||
|
||
<div v-show="!binaryDataDisplayVisible">
|
||
<div v-if="inputSelectLocation === 'header'" :class="$style.inputSelect">
|
||
<slot name="input-select"></slot>
|
||
</div>
|
||
|
||
<div
|
||
v-if="maxRunIndex > 0 && !displaysMultipleNodes && !props.disableRunIndexSelection"
|
||
v-show="!editMode.enabled"
|
||
:class="$style.runSelector"
|
||
>
|
||
<div :class="$style.runSelectorInner">
|
||
<slot v-if="inputSelectLocation === 'runs'" name="input-select"></slot>
|
||
|
||
<N8nSelect
|
||
:model-value="runIndex"
|
||
:class="$style.runSelectorSelect"
|
||
size="small"
|
||
teleported
|
||
data-test-id="run-selector"
|
||
@update:model-value="onRunIndexChange"
|
||
@click.stop
|
||
>
|
||
<template #prepend>{{ i18n.baseText('ndv.output.run') }}</template>
|
||
<N8nOption
|
||
v-for="option in runSelectorOptionsCount"
|
||
:key="option"
|
||
:label="getRunLabel(option)"
|
||
:value="option - 1"
|
||
data-test-id="run-selection-option"
|
||
></N8nOption>
|
||
</N8nSelect>
|
||
|
||
<N8nTooltip v-if="canLinkRuns" placement="right">
|
||
<template #content>
|
||
{{ i18n.baseText(linkedRuns ? 'runData.unlinking.hint' : 'runData.linking.hint') }}
|
||
</template>
|
||
<N8nIconButton
|
||
:icon="linkedRuns ? 'unlink' : 'link'"
|
||
:class="['linkRun', linkedRuns ? 'linked' : '']"
|
||
text
|
||
type="tertiary"
|
||
size="small"
|
||
data-test-id="link-run"
|
||
@click="toggleLinkRuns"
|
||
/>
|
||
</N8nTooltip>
|
||
|
||
<slot name="run-info"></slot>
|
||
</div>
|
||
<ViewSubExecution
|
||
v-if="activeTaskMetadata && !(paneType === 'input' && hasInputOverwrite)"
|
||
:task-metadata="activeTaskMetadata"
|
||
:display-mode="displayMode"
|
||
/>
|
||
</div>
|
||
|
||
<slot v-if="!displaysMultipleNodes" name="before-data" />
|
||
|
||
<div v-if="props.calloutMessage || $slots['callout-message']" :class="$style.hintCallout">
|
||
<N8nCallout theme="info" data-test-id="run-data-callout">
|
||
<slot name="callout-message">
|
||
<N8nText v-n8n-html="props.calloutMessage" size="small"></N8nText>
|
||
</slot>
|
||
</N8nCallout>
|
||
</div>
|
||
<NodeSettingsHint
|
||
v-if="!props.disableSettingsHint && props.paneType === 'output'"
|
||
:node="node"
|
||
/>
|
||
<N8nCallout
|
||
v-for="hint in nodeHints"
|
||
:key="hint.message"
|
||
:class="$style.hintCallout"
|
||
:theme="hint.type || 'info'"
|
||
data-test-id="node-hint"
|
||
>
|
||
<N8nText v-n8n-html="hint.message" size="small"></N8nText>
|
||
</N8nCallout>
|
||
|
||
<div v-if="showBranchSwitch" :class="$style.outputs" data-test-id="branches">
|
||
<slot v-if="inputSelectLocation === 'outputs'" name="input-select"></slot>
|
||
<ViewSubExecution
|
||
v-if="activeTaskMetadata && !(paneType === 'input' && hasInputOverwrite)"
|
||
:task-metadata="activeTaskMetadata"
|
||
:display-mode="displayMode"
|
||
/>
|
||
|
||
<div :class="$style.tabs">
|
||
<N8nTabs
|
||
size="small"
|
||
:model-value="currentOutputIndex"
|
||
:options="branches"
|
||
@update:model-value="onBranchChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-else-if="
|
||
!props.compact &&
|
||
hasNodeRun &&
|
||
!isSearchInSchemaView &&
|
||
((dataCount > 0 && maxRunIndex === 0) || search) &&
|
||
!isArtificialRecoveredEventItem &&
|
||
!displaysMultipleNodes
|
||
"
|
||
v-show="!editMode.enabled"
|
||
:class="$style.itemsCount"
|
||
data-test-id="ndv-items-count"
|
||
>
|
||
<slot v-if="inputSelectLocation === 'items'" name="input-select"></slot>
|
||
|
||
<RunDataItemCount v-bind="itemsCountProps" />
|
||
<ViewSubExecution
|
||
v-if="activeTaskMetadata && !(paneType === 'input' && hasInputOverwrite)"
|
||
:task-metadata="activeTaskMetadata"
|
||
:display-mode="displayMode"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
ref="dataContainerRef"
|
||
:class="$style.dataContainer"
|
||
data-test-id="ndv-data-container"
|
||
@wheel.capture="emit('captureWheelDataContainer', $event)"
|
||
>
|
||
<BinaryDataDisplay
|
||
v-if="binaryDataDisplayData"
|
||
:window-visible="binaryDataDisplayVisible"
|
||
:display-data="binaryDataDisplayData"
|
||
@close="closeBinaryDataDisplay"
|
||
/>
|
||
|
||
<div
|
||
v-if="isExecuting && !isWaitNodeWaiting"
|
||
:class="[$style.center, $style.executingMessage]"
|
||
data-test-id="ndv-executing"
|
||
>
|
||
<div v-if="!props.compact" :class="$style.spinner">
|
||
<N8nSpinner type="ring" />
|
||
</div>
|
||
<N8nText>{{ executingMessage }}</N8nText>
|
||
</div>
|
||
|
||
<div v-else-if="editMode.enabled" :class="$style.editMode">
|
||
<div :class="[$style.editModeBody, 'ignore-key-press-canvas']">
|
||
<JsonEditor
|
||
:model-value="editMode.value"
|
||
:fill-parent="true"
|
||
@update:model-value="ndvStore.setOutputPanelEditModeValue($event)"
|
||
/>
|
||
</div>
|
||
<div :class="$style.editModeFooter">
|
||
<N8nInfoTip :bold="false" :class="$style.editModeFooterInfotip">
|
||
{{ i18n.baseText('runData.editor.copyDataInfo') }}
|
||
<N8nLink :to="DATA_EDITING_DOCS_URL" size="small">
|
||
{{ i18n.baseText('generic.learnMore') }}
|
||
</N8nLink>
|
||
</N8nInfoTip>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-else-if="
|
||
paneType === 'output' && hasSubworkflowExecutionError && subworkflowExecutionError
|
||
"
|
||
:class="$style.stretchVertically"
|
||
>
|
||
<NodeErrorView
|
||
:compact="compact"
|
||
:error="subworkflowExecutionError"
|
||
:class="$style.errorDisplay"
|
||
show-details
|
||
/>
|
||
</div>
|
||
|
||
<div v-else-if="isWaitNodeWaiting" :class="$style.center">
|
||
<slot name="node-waiting">xxx</slot>
|
||
</div>
|
||
|
||
<div
|
||
v-else-if="!hasNodeRun && !(displaysMultipleNodes && (node?.disabled || hasPreviewSchema))"
|
||
:class="$style.center"
|
||
>
|
||
<slot name="node-not-run"></slot>
|
||
</div>
|
||
|
||
<div
|
||
v-else-if="paneType === 'input' && !displaysMultipleNodes && node?.disabled"
|
||
:class="$style.center"
|
||
>
|
||
<N8nText>
|
||
{{ i18n.baseText('ndv.input.disabled', { interpolate: { nodeName: node.name } }) }}
|
||
<N8nLink @click="enableNode">
|
||
{{ i18n.baseText('ndv.input.disabled.cta') }}
|
||
</N8nLink>
|
||
</N8nText>
|
||
</div>
|
||
|
||
<div
|
||
v-else-if="isTrimmedManualExecutionDataItem && uiStore.isProcessingExecutionResults"
|
||
:class="$style.center"
|
||
>
|
||
<div :class="$style.spinner"><N8nSpinner type="ring" /></div>
|
||
<N8nText color="text-dark" size="large">
|
||
{{ i18n.baseText('runData.trimmedData.loading') }}
|
||
</N8nText>
|
||
</div>
|
||
|
||
<div v-else-if="isTrimmedManualExecutionDataItem" :class="$style.center">
|
||
<N8nText bold color="text-dark" size="large">
|
||
{{ i18n.baseText('runData.trimmedData.title') }}
|
||
</N8nText>
|
||
<N8nText>
|
||
{{ i18n.baseText('runData.trimmedData.message') }}
|
||
</N8nText>
|
||
</div>
|
||
|
||
<div v-else-if="hasNodeRun && isArtificialRecoveredEventItem" :class="$style.center">
|
||
<slot name="recovered-artificial-output-data"></slot>
|
||
</div>
|
||
|
||
<div v-else-if="hasNodeRun && hasRunError" :class="$style.stretchVertically">
|
||
<N8nText v-if="isPaneTypeInput" :class="$style.center" size="large" tag="p" bold>
|
||
{{
|
||
i18n.baseText('nodeErrorView.inputPanel.previousNodeError.title', {
|
||
interpolate: { nodeName: node?.name ?? '' },
|
||
})
|
||
}}
|
||
</N8nText>
|
||
<div v-else-if="$slots['content']">
|
||
<NodeErrorView
|
||
v-if="workflowRunErrorAsNodeError"
|
||
:error="workflowRunErrorAsNodeError"
|
||
:class="$style.inlineError"
|
||
:compact="compact"
|
||
/>
|
||
<slot name="content"></slot>
|
||
</div>
|
||
<NodeErrorView
|
||
v-else-if="workflowRunErrorAsNodeError"
|
||
:error="workflowRunErrorAsNodeError"
|
||
:class="$style.dataDisplay"
|
||
:compact="compact"
|
||
show-details
|
||
/>
|
||
</div>
|
||
|
||
<div
|
||
v-else-if="
|
||
hasNodeRun &&
|
||
(!unfilteredDataCount || (search && !dataCount)) &&
|
||
!displaysMultipleNodes &&
|
||
branches.length > 1
|
||
"
|
||
:class="$style.center"
|
||
>
|
||
<div v-if="search">
|
||
<N8nText tag="h3" size="large">{{ i18n.baseText('ndv.search.noMatch.title') }}</N8nText>
|
||
<N8nText>
|
||
<I18nT keypath="ndv.search.noMatch.description" tag="span" scope="global">
|
||
<template #link>
|
||
<a href="#" @click="onSearchClear">
|
||
{{ i18n.baseText('ndv.search.noMatch.description.link') }}
|
||
</a>
|
||
</template>
|
||
</I18nT>
|
||
</N8nText>
|
||
</div>
|
||
<N8nText v-else>
|
||
{{ noDataInBranchMessage }}
|
||
</N8nText>
|
||
</div>
|
||
|
||
<div
|
||
v-else-if="hasNodeRun && !inputData.length && !displaysMultipleNodes && !search"
|
||
:class="$style.center"
|
||
>
|
||
<slot name="no-output-data"></slot>
|
||
</div>
|
||
|
||
<div
|
||
v-else-if="hasNodeRun && !showData"
|
||
data-test-id="ndv-data-size-warning"
|
||
:class="$style.center"
|
||
>
|
||
<N8nText :bold="true" color="text-dark" size="large">{{ tooMuchDataTitle }}</N8nText>
|
||
<N8nText align="center" tag="div"
|
||
><span
|
||
v-n8n-html="
|
||
i18n.baseText('ndv.output.tooMuchData.message', {
|
||
interpolate: { size: dataSizeInMB },
|
||
})
|
||
"
|
||
></span
|
||
></N8nText>
|
||
|
||
<N8nButton
|
||
outline
|
||
:label="i18n.baseText('ndv.output.tooMuchData.showDataAnyway')"
|
||
@click="showTooMuchData"
|
||
/>
|
||
|
||
<N8nButton
|
||
size="small"
|
||
:label="i18n.baseText('runData.downloadBinaryData')"
|
||
@click="downloadJsonData()"
|
||
/>
|
||
</div>
|
||
|
||
<!-- V-else slot named content which only renders if $slots.content is passed and hasNodeRun -->
|
||
<slot v-else-if="hasNodeRun && $slots['content']" name="content"></slot>
|
||
|
||
<div
|
||
v-else-if="
|
||
hasNodeRun &&
|
||
displayMode === 'table' &&
|
||
binaryData.length > 0 &&
|
||
inputData.length === 1 &&
|
||
Object.keys(jsonData[0] || {}).length === 0
|
||
"
|
||
:class="$style.center"
|
||
>
|
||
<N8nText>
|
||
{{ i18n.baseText('runData.switchToBinary.info') }}
|
||
<a @click="switchToBinary">
|
||
{{ i18n.baseText('runData.switchToBinary.binary') }}
|
||
</a>
|
||
</N8nText>
|
||
</div>
|
||
|
||
<div v-else-if="showIoSearchNoMatchContent" :class="$style.center">
|
||
<N8nText tag="h3" size="large">{{ i18n.baseText('ndv.search.noMatch.title') }}</N8nText>
|
||
<N8nText>
|
||
<I18nT keypath="ndv.search.noMatch.description" tag="span" scope="global">
|
||
<template #link>
|
||
<a href="#" @click="onSearchClear">
|
||
{{ i18n.baseText('ndv.search.noMatch.description.link') }}
|
||
</a>
|
||
</template>
|
||
</I18nT>
|
||
</N8nText>
|
||
</div>
|
||
|
||
<Suspense v-else-if="hasNodeRun && displayMode === 'table' && node">
|
||
<LazyRunDataTable
|
||
:node="node"
|
||
:input-data="inputDataPage"
|
||
:mapping-enabled="mappingEnabled"
|
||
:distance-from-active="distanceFromActive"
|
||
:run-index="runIndex"
|
||
:page-offset="currentPageOffset"
|
||
:total-runs="maxRunIndex"
|
||
:has-default-hover-state="paneType === 'input' && !search"
|
||
:search="search"
|
||
:header-bg-color="tableHeaderBgColor"
|
||
:compact="props.compact"
|
||
:disable-hover-highlight="props.disableHoverHighlight"
|
||
:collapsing-column-name="collapsingTableColumnName"
|
||
@mounted="emit('tableMounted', $event)"
|
||
@active-row-changed="onItemHover"
|
||
@display-mode-change="onDisplayModeChange"
|
||
@collapsing-column-changed="emit('collapsingTableColumnChanged', $event)"
|
||
/>
|
||
</Suspense>
|
||
|
||
<Suspense v-else-if="hasNodeRun && displayMode === 'json' && node">
|
||
<LazyRunDataJson
|
||
:pane-type="paneType"
|
||
:edit-mode="editMode"
|
||
:push-ref="pushRef"
|
||
:node="node"
|
||
:input-data="inputDataPage"
|
||
:mapping-enabled="mappingEnabled"
|
||
:distance-from-active="distanceFromActive"
|
||
:run-index="runIndex"
|
||
:output-index="currentOutputIndex"
|
||
:total-runs="maxRunIndex"
|
||
:search="search"
|
||
:compact="props.compact"
|
||
/>
|
||
</Suspense>
|
||
|
||
<Suspense v-else-if="hasNodeRun && isPaneTypeOutput && displayMode === 'html'">
|
||
<LazyRunDataHtml :input-html="inputHtml" />
|
||
</Suspense>
|
||
|
||
<Suspense v-else-if="hasNodeRun && displayMode === 'ai'">
|
||
<LazyRunDataAi
|
||
render-type="rendered"
|
||
:compact="compact"
|
||
:content="parsedAiContent"
|
||
:search="search"
|
||
/>
|
||
</Suspense>
|
||
|
||
<Suspense v-else-if="(hasNodeRun || hasPreviewSchema) && isSchemaView">
|
||
<LazyRunDataSchema
|
||
:nodes="nodes"
|
||
:mapping-enabled="mappingEnabled"
|
||
:node="node"
|
||
:data="jsonData"
|
||
:pane-type="paneType"
|
||
:connection-type="connectionType"
|
||
:output-index="currentOutputIndex"
|
||
:search="search"
|
||
:class="$style.schema"
|
||
:compact="props.compact"
|
||
@clear:search="onSearchClear"
|
||
/>
|
||
</Suspense>
|
||
|
||
<RunDataBinary
|
||
v-else-if="displayMode === 'binary'"
|
||
:binary-data="binaryData"
|
||
@preview="displayBinaryData"
|
||
/>
|
||
|
||
<div v-else-if="!hasNodeRun" :class="$style.center">
|
||
<slot name="node-not-run"></slot>
|
||
</div>
|
||
</div>
|
||
<RunDataPaginationBar
|
||
v-if="
|
||
hidePagination === false &&
|
||
hasNodeRun &&
|
||
!hasRunError &&
|
||
displayMode !== 'binary' &&
|
||
dataCount > pageSize &&
|
||
!isSchemaView &&
|
||
!isArtificialRecoveredEventItem
|
||
"
|
||
v-show="!editMode.enabled"
|
||
:current-page="currentPage"
|
||
:page-size="pageSize"
|
||
:total="dataCount"
|
||
@update:current-page="onCurrentPageChange"
|
||
@update:page-size="onPageSizeChange"
|
||
/>
|
||
<N8nBlockUi :show="blockUI" :class="$style.uiBlocker" />
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="scss" module>
|
||
.infoIcon {
|
||
color: var(--color-foreground-dark);
|
||
}
|
||
|
||
.center {
|
||
display: flex;
|
||
height: 100%;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: var(--ndv-spacing) var(--ndv-spacing) var(--spacing-xl) var(--ndv-spacing);
|
||
text-align: center;
|
||
|
||
> * {
|
||
max-width: 316px;
|
||
margin-bottom: var(--spacing-2xs);
|
||
}
|
||
}
|
||
|
||
.container {
|
||
--ndv-spacing: var(--spacing-s);
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.pinnedDataCallout {
|
||
border-radius: inherit;
|
||
border-bottom-right-radius: 0;
|
||
border-top: 0;
|
||
border-left: 0;
|
||
border-right: 0;
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: var(--ndv-spacing);
|
||
padding: var(--ndv-spacing) var(--ndv-spacing) 0 var(--ndv-spacing);
|
||
position: relative;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
min-height: calc(30px + var(--ndv-spacing));
|
||
scrollbar-width: thin;
|
||
container-type: inline-size;
|
||
|
||
.compact & {
|
||
margin-bottom: var(--spacing-4xs);
|
||
padding: var(--spacing-2xs);
|
||
margin-bottom: 0;
|
||
flex-shrink: 0;
|
||
flex-grow: 0;
|
||
min-height: auto;
|
||
gap: var(--spacing-2xs);
|
||
}
|
||
|
||
> *:first-child {
|
||
flex-grow: 1;
|
||
}
|
||
}
|
||
|
||
.dataContainer {
|
||
position: relative;
|
||
overflow-y: auto;
|
||
height: 100%;
|
||
}
|
||
|
||
.dataDisplay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
padding: 0 var(--ndv-spacing) var(--spacing-3xl) var(--ndv-spacing);
|
||
right: 0;
|
||
overflow-y: auto;
|
||
line-height: var(--font-line-height-xloose);
|
||
word-break: normal;
|
||
height: 100%;
|
||
|
||
.compact & {
|
||
padding: 0 var(--spacing-2xs);
|
||
}
|
||
}
|
||
|
||
.inlineError {
|
||
line-height: var(--font-line-height-xloose);
|
||
padding-left: var(--ndv-spacing);
|
||
padding-right: var(--ndv-spacing);
|
||
padding-bottom: var(--ndv-spacing);
|
||
}
|
||
|
||
.outputs {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--ndv-spacing);
|
||
padding-left: var(--ndv-spacing);
|
||
padding-right: var(--ndv-spacing);
|
||
padding-bottom: var(--ndv-spacing);
|
||
|
||
.compact & {
|
||
padding-left: var(--spacing-2xs);
|
||
padding-right: var(--spacing-2xs);
|
||
padding-bottom: var(--spacing-2xs);
|
||
font-size: var(--font-size-2xs);
|
||
}
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
min-height: 30px;
|
||
--color-tabs-arrow-buttons: var(--color-run-data-background);
|
||
}
|
||
|
||
.itemsCount {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-2xs);
|
||
padding-left: var(--ndv-spacing);
|
||
padding-right: var(--ndv-spacing);
|
||
padding-bottom: var(--ndv-spacing);
|
||
flex-flow: wrap;
|
||
}
|
||
|
||
.ndv-v2 .itemsCount {
|
||
padding-left: var(--spacing-xs);
|
||
}
|
||
|
||
.inputSelect {
|
||
padding-left: var(--ndv-spacing);
|
||
padding-right: var(--ndv-spacing);
|
||
padding-bottom: var(--ndv-spacing);
|
||
}
|
||
|
||
.runSelector {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-flow: wrap;
|
||
padding-left: var(--ndv-spacing);
|
||
padding-right: var(--ndv-spacing);
|
||
margin-bottom: var(--ndv-spacing);
|
||
gap: var(--spacing-3xs);
|
||
|
||
:global(.el-input--suffix .el-input__inner) {
|
||
padding-right: var(--spacing-l);
|
||
}
|
||
}
|
||
|
||
.runSelectorInner {
|
||
display: flex;
|
||
gap: var(--spacing-4xs);
|
||
align-items: center;
|
||
}
|
||
|
||
.runSelectorSelect {
|
||
max-width: 205px;
|
||
}
|
||
|
||
.search {
|
||
margin-left: auto;
|
||
}
|
||
|
||
.displayModes {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
flex-grow: 1;
|
||
gap: var(--spacing-2xs);
|
||
|
||
.compact & {
|
||
/* let title text alone decide the height */
|
||
height: 0;
|
||
visibility: hidden;
|
||
|
||
:global(.el-input__prefix) {
|
||
transition-duration: 0ms;
|
||
}
|
||
}
|
||
|
||
.compact:hover & {
|
||
visibility: visible;
|
||
}
|
||
}
|
||
|
||
.tooltipContain {
|
||
max-width: 240px;
|
||
}
|
||
|
||
.spinner {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-bottom: var(--ndv-spacing);
|
||
|
||
* {
|
||
color: var(--color-primary);
|
||
min-height: 40px;
|
||
min-width: 40px;
|
||
}
|
||
}
|
||
|
||
.editMode {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: stretch;
|
||
padding-left: var(--ndv-spacing);
|
||
padding-right: var(--ndv-spacing);
|
||
}
|
||
|
||
.editModeBody {
|
||
flex: 1 1 auto;
|
||
max-height: 100%;
|
||
width: 100%;
|
||
overflow: auto;
|
||
}
|
||
|
||
.editModeFooter {
|
||
flex: 0 1 auto;
|
||
display: flex;
|
||
width: 100%;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding-top: var(--ndv-spacing);
|
||
padding-bottom: var(--ndv-spacing);
|
||
}
|
||
|
||
.editModeFooterInfotip {
|
||
display: flex;
|
||
flex: 1;
|
||
width: 100%;
|
||
}
|
||
|
||
.editModeActions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
margin-left: var(--ndv-spacing);
|
||
}
|
||
|
||
.stretchVertically {
|
||
height: 100%;
|
||
}
|
||
|
||
.uiBlocker {
|
||
border-top-left-radius: 0;
|
||
border-bottom-left-radius: 0;
|
||
}
|
||
|
||
.hintCallout {
|
||
margin-bottom: var(--spacing-xs);
|
||
margin-left: var(--ndv-spacing);
|
||
margin-right: var(--ndv-spacing);
|
||
|
||
.compact & {
|
||
margin: 0 var(--spacing-2xs) var(--spacing-2xs) var(--spacing-2xs);
|
||
}
|
||
}
|
||
|
||
.schema {
|
||
padding: 0 var(--ndv-spacing);
|
||
}
|
||
|
||
.messageSection {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
}
|
||
|
||
.singleIcon {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.multipleIcons {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: var(--spacing-2xs, 8px);
|
||
}
|
||
|
||
.multipleIcons .iconStack {
|
||
margin-right: 0;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.iconStack {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-4xs, 4px);
|
||
flex-shrink: 0;
|
||
margin-right: var(--spacing-xs);
|
||
}
|
||
|
||
.icon {
|
||
color: var(--color-callout-info-icon);
|
||
line-height: 1;
|
||
font-size: var(--font-size-xs);
|
||
}
|
||
|
||
.executingMessage {
|
||
.compact & {
|
||
color: var(--color-text-light);
|
||
}
|
||
}
|
||
|
||
.resetCollapseButton {
|
||
color: var(--color-foreground-xdark);
|
||
}
|
||
|
||
@container (max-width: 240px) {
|
||
/* Hide title when the panel is too narrow */
|
||
.compact:hover .title {
|
||
visibility: hidden;
|
||
width: 0;
|
||
}
|
||
}
|
||
|
||
.ndv-v2,
|
||
.compact {
|
||
--ndv-spacing: var(--spacing-2xs);
|
||
}
|
||
</style>
|
||
|
||
<style lang="scss" scoped>
|
||
.run-data {
|
||
.code-node-editor {
|
||
height: 100%;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style lang="scss" scoped>
|
||
:deep(.highlight) {
|
||
background-color: #f7dc55;
|
||
color: black;
|
||
border-radius: var(--border-radius-base);
|
||
padding: 0 1px;
|
||
font-weight: var(--font-weight-regular);
|
||
font-style: normal;
|
||
}
|
||
</style>
|