<script setup lang="ts">
import { useStorage } from '@/composables/useStorage';
import { saveAs } from 'file-saver';
import type {
	IBinaryData,
	IConnectedNode,
	IDataObject,
	INodeExecutionData,
	INodeOutputConfiguration,
	IRunData,
	IRunExecutionData,
	ITaskMetadata,
	NodeError,
	NodeHint,
	Workflow,
} from 'n8n-workflow';
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
import { computed, defineAsyncComponent, onBeforeUnmount, onMounted, ref, toRef, watch } from 'vue';

import type {
	INodeUi,
	INodeUpdatePropertiesInformation,
	IRunDataDisplayMode,
	ITab,
	NodePanelType,
} from '@/Interface';

import {
	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,
	NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND,
	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 '@/composables/useI18n';
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 } from '@/event-bus';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useRootStore } from '@/stores/root.store';
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 } from '@/utils/typesUtils';
import { isEqual, isObject } from 'lodash-es';
import {
	N8nBlockUi,
	N8nButton,
	N8nCallout,
	N8nIconButton,
	N8nInfoTip,
	N8nLink,
	N8nOption,
	N8nRadioButtons,
	N8nSelect,
	N8nSpinner,
	N8nTabs,
	N8nText,
	N8nTooltip,
} from 'n8n-design-system';
import { storeToRefs } from 'pinia';
import { useRoute } from 'vue-router';
import { useExecutionHelpers } from '@/composables/useExecutionHelpers';

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 LazyRunDataSearch = defineAsyncComponent(
	async () => await import('@/components/RunDataSearch.vue'),
);

export type EnterEditModeArgs = {
	origin: 'editIconButton' | 'insertTestDataLink';
};

type Props = {
	workflow: Workflow;
	runIndex: number;
	tooMuchDataTitle: string;
	executingMessage: string;
	pushRef: string;
	paneType: NodePanelType;
	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;
};

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,
});
const emit = defineEmits<{
	search: [search: string];
	runChange: [runIndex: number];
	itemHover: [
		item: {
			outputIndex: number;
			itemIndex: number;
		} | null,
	];
	linkRun: [];
	unlinkRun: [];
	activatePane: [];
	tableMounted: [
		{
			avgRowHeight: number;
		},
	];
}>();

const connectionType = ref<NodeConnectionType>(NodeConnectionType.Main);
const dataSize = ref(0);
const showData = ref(false);
const userEnabledShowData = ref(false);
const outputIndex = ref(0);
const binaryDataDisplayVisible = ref(false);
const binaryDataDisplayData = ref<IBinaryData | null>(null);
const currentPage = ref(1);
const pageSize = ref(10);
const pageSizes = [1, 10, 25, 50, 100];

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 toast = useToast();
const route = useRoute();
const nodeHelpers = useNodeHelpers();
const externalHooks = useExternalHooks();
const telemetry = useTelemetry();
const i18n = useI18n();
const { trackOpeningRelatedExecution, resolveRelatedExecutionUrl } = useExecutionHelpers();

const node = toRef(props, 'node');

const pinnedData = usePinnedData(node, {
	runIndex: props.runIndex,
	displayMode:
		props.paneType === 'input' ? ndvStore.inputPanelDisplayMode : ndvStore.outputPanelDisplayMode,
});
const { isSubNodeType } = useNodeType({
	node,
});

const displayMode = computed(() =>
	props.paneType === 'input' ? ndvStore.inputPanelDisplayMode : ndvStore.outputPanelDisplayMode,
);

const isReadOnlyRoute = computed(() => route.meta.readOnlyCanvas === true);
const isWaitNodeWaiting = computed(() => {
	return (
		node.value?.name &&
		workflowExecution.value?.data?.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(() => displayMode.value === 'schema');
const isSearchInSchemaView = computed(() => isSchemaView.value && !!search.value);
const displaysMultipleNodes = computed(
	() => isSchemaView.value && props.paneType === 'input' && props.nodes.length > 0,
);

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 displayModes = computed(() => {
	const defaults: Array<{ label: string; value: IRunDataDisplayMode }> = [
		{ label: i18n.baseText('runData.table'), value: 'table' },
		{ label: i18n.baseText('runData.json'), value: 'json' },
	];

	if (binaryData.value.length) {
		defaults.push({ label: i18n.baseText('runData.binary'), value: 'binary' });
	}

	const schemaView = { label: i18n.baseText('runData.schema'), value: 'schema' } as const;
	if (isPaneTypeInput.value) {
		defaults.unshift(schemaView);
	} else {
		defaults.push(schemaView);
	}

	if (
		isPaneTypeOutput.value &&
		activeNode.value?.type === HTML_NODE_TYPE &&
		activeNode.value.parameters.operation === 'generateHtmlTemplate'
	) {
		defaults.unshift({ label: 'HTML', value: 'html' });
	}

	return defaults;
});

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 subworkflowExecutionError = computed(() => {
	if (!node.value) return null;
	return {
		node: node.value,
		messages: [workflowsStore.subWorkflowExecutionError?.message ?? ''],
	} as NodeError;
});

const hasSubworkflowExecutionError = computed(() =>
	Boolean(workflowsStore.subWorkflowExecutionError),
);
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') {
		const parentNode = props.workflow.getChildNodes(node.value?.name ?? '', 'ALL_NON_MAIN')[0];
		return workflowRunData.value?.[parentNode]?.[props.runIndex]?.error as NodeError;
	}
	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(() => workflowsStore.getWorkflowExecution);
const workflowRunData = computed(() => {
	if (workflowExecution.value === null) {
		return null;
	}
	const executionData: IRunExecutionData | undefined = workflowExecution.value.data;
	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 === null || !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 maxRunIndex = computed(() => {
	if (!node.value) {
		return 0;
	}

	const runData: IRunData | null = workflowRunData.value;

	if (runData === null || !runData.hasOwnProperty(node.value.name)) {
		return 0;
	}

	if (runData[node.value.name].length) {
		return runData[node.value.name].length - 1;
	}

	return 0;
});

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,
);
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,
);

const parentNodeOutputData = computed(() => {
	const parentNode = props.workflow.getParentNodesByDepth(node.value?.name ?? '')[0];
	let parentNodeData: INodeExecutionData[] = [];

	if (parentNode?.name) {
		parentNodeData = nodeHelpers.getNodeInputData(
			props.workflow.getNode(parentNode?.name),
			props.runIndex,
			outputIndex.value,
			'input',
			connectionType.value,
		);
	}

	return parentNodeData;
});

const parentNodePinnedData = computed(() => {
	const parentNode = props.workflow.getParentNodesByDepth(node.value?.name ?? '')[0];
	return props.workflow.pinData?.[parentNode?.name || ''] ?? [];
});

const showPinButton = computed(() => {
	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(
	() =>
		pinnedData.hasData.value ||
		!rawInputData.value.length ||
		!!binaryData.value?.length ||
		isReadOnlyRoute.value ||
		readOnlyEnv.value,
);

const activeTaskMetadata = computed((): ITaskMetadata | null => {
	if (!node.value) {
		return null;
	}

	return workflowRunData.value?.[node.value.name]?.[props.runIndex]?.metadata ?? null;
});

const hasReleatedExectuion = computed((): boolean => {
	return Boolean(
		activeTaskMetadata.value?.subExecution || activeTaskMetadata.value?.parentExecution,
	);
});

const hasInputOverwrite = computed((): boolean => {
	if (!node.value) {
		return false;
	}
	const taskData = nodeHelpers.getNodeTaskData(node.value, props.runIndex);
	return Boolean(taskData?.inputOverride);
});

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 && displayMode.value !== 'binary') {
		switchToBinary();
	} else if (!newData.length && displayMode.value === 'binary') {
		onDisplayModeChange('table');
	}
});

watch(currentOutputIndex, (branchIndex: number) => {
	ndvStore.setNDVBranchIndex({
		pane: props.paneType,
		branchIndex,
	});
});

watch(search, (newSearch) => {
	emit('search', newSearch);
});

onMounted(() => {
	init();

	if (!isPaneTypeInput.value) {
		showPinDataDiscoveryTooltip(jsonData.value);
	}
	ndvStore.setNDVBranchIndex({
		pane: props.paneType,
		branchIndex: currentOutputIndex.value,
	});

	if (props.paneType === 'output') {
		setDisplayMode();
		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,
				},
				{
					withPostHog: true,
				},
			);
		}
	}
});

onBeforeUnmount(() => {
	hidePinDataDiscoveryTooltip();
});

function getResolvedNodeOutputs() {
	if (node.value && nodeType.value) {
		const workflowNode = props.workflow.getNode(node.value.name);

		if (workflowNode) {
			const outputs = NodeHelpers.getNodeOutputs(props.workflow, 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;
}
function getNodeHints(): NodeHint[] {
	try {
		if (node.value && nodeType.value) {
			const workflowNode = props.workflow.getNode(node.value.name);

			if (workflowNode) {
				const nodeHints = NodeHelpers.getNodeHints(props.workflow, workflowNode, nodeType.value, {
					runExecutionData: workflowExecution.value?.data ?? 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,
					workflow: props.workflow,
					hasNodeRun: hasNodeRun.value,
					hasMultipleInputItems,
				});

				return executionHints.value.concat(nodeHints, 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 && !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' : displayMode.value,
		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: displayMode.value,
		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' : displayMode.value,
		};

		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 = displayMode.value;
	ndvStore.setPanelDisplayMode({ pane: props.paneType, mode: 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') + (maxRunIndex.value + 1) + itemsLabel;
}

function getRawInputData(
	runIndex: number,
	outputIndex: number,
	connectionType: NodeConnectionType = NodeConnectionType.Main,
): INodeExecutionData[] {
	let inputData: INodeExecutionData[] = [];

	if (node.value) {
		inputData = nodeHelpers.getNodeInputData(
			node.value,
			runIndex,
			outputIndex,
			props.paneType,
			connectionType,
		);
	}

	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 = NodeConnectionType.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 ? NodeConnectionType.Main : outputTypes[0];
	if (binaryData.value.length > 0) {
		ndvStore.setPanelDisplayMode({
			pane: props.paneType,
			mode: 'binary',
		});
	} else if (displayMode.value === 'binary') {
		ndvStore.setPanelDisplayMode({
			pane: props.paneType,
			mode: 'table',
		});
	}
}

function closeBinaryDataDisplay() {
	binaryDataDisplayVisible.value = false;
	binaryDataDisplayData.value = null;
}

function isViewable(index: number, key: string | number): boolean {
	const { fileType } = binaryData.value[index][key];
	return (
		!!fileType && ['image', 'audio', 'video', 'text', 'json', 'pdf', 'html'].includes(fileType)
	);
}

function isDownloadable(index: number, key: string | number): boolean {
	const { mimeType, fileName } = binaryData.value[index][key];
	return !!(mimeType && fileName);
}

async function downloadBinaryData(index: number, key: string | number) {
	const { id, data, fileName, fileExtension, mimeType } = binaryData.value[index][key];

	if (id) {
		const url = workflowsStore.getBinaryUrl(id, 'download', fileName ?? '', mimeType);
		saveAs(url, [fileName, fileExtension].join('.'));
		return;
	} else {
		const bufferString = 'data:' + mimeType + ';base64,' + data;
		const blob = await fetch(bufferString).then(async (d) => await d.blob());
		saveAs(blob, fileName);
	}
}

async 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];
	binaryDataDisplayVisible.value = true;

	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,
			} as IDataObject,
		} as INodeUpdatePropertiesInformation;

		workflowsStore.updateNodeProperties(updateInformation);
	}
}

function setDisplayMode() {
	if (!activeNode.value) return;

	const shouldDisplayHtml =
		activeNode.value.type === HTML_NODE_TYPE &&
		activeNode.value.parameters.operation === 'generateHtmlTemplate';

	if (shouldDisplayHtml) {
		ndvStore.setPanelDisplayMode({
			pane: 'output',
			mode: 'html',
		});
	}
}

function activatePane() {
	emit('activatePane');
}

function onSearchClear() {
	search.value = '';
	document.dispatchEvent(new KeyboardEvent('keyup', { key: '/' }));
}

function getExecutionLinkLabel(task: ITaskMetadata): string | undefined {
	if (task.parentExecution) {
		return i18n.baseText('runData.openParentExecution', {
			interpolate: { id: task.parentExecution.executionId },
		});
	}

	if (task.subExecution) {
		return i18n.baseText('runData.openSubExecution', {
			interpolate: { id: task.subExecution.executionId },
		});
	}

	return;
}

defineExpose({ enterEditMode });
</script>

<template>
	<div :class="['run-data', $style.container]" @mouseover="activatePane">
		<N8nCallout
			v-if="pinnedData.hasData.value && !editMode.enabled && !isProductionExecutionPreview"
			theme="secondary"
			icon="thumbtack"
			:class="$style.pinnedDataCallout"
		>
			{{ i18n.baseText('runData.pindata.thisDataIsPinned') }}
			<span v-if="!isReadOnlyRoute && !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>

		<BinaryDataDisplay
			v-if="binaryDataDisplayData"
			:window-visible="binaryDataDisplayVisible"
			:display-data="binaryDataDisplayData"
			@close="closeBinaryDataDisplay"
		/>

		<div :class="$style.header">
			<slot name="header"></slot>

			<div
				v-show="!hasRunError"
				: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>

				<N8nRadioButtons
					v-show="
						hasNodeRun && (inputData.length || binaryData.length || search) && !editMode.enabled
					"
					:model-value="displayMode"
					:options="displayModes"
					data-test-id="ndv-run-data-display-mode"
					@update:model-value="onDisplayModeChange"
				/>

				<N8nIconButton
					v-if="canPinData && !isReadOnlyRoute && !readOnlyEnv"
					v-show="!editMode.enabled"
					:title="i18n.baseText('runData.editOutput')"
					:circle="false"
					:disabled="node?.disabled"
					icon="pencil-alt"
					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-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>
		</div>

		<div v-if="inputSelectLocation === 'header'" :class="$style.inputSelect">
			<slot name="input-select"></slot>
		</div>

		<div
			v-if="maxRunIndex > 0 && !displaysMultipleNodes"
			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 maxRunIndex + 1"
						:key="option"
						:label="getRunLabel(option)"
						:value="option - 1"
					></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"
						text
						type="tertiary"
						size="small"
						data-test-id="link-run"
						@click="toggleLinkRuns"
					/>
				</N8nTooltip>

				<slot name="run-info"></slot>
			</div>

			<a
				v-if="
					activeTaskMetadata && hasReleatedExectuion && !(paneType === 'input' && hasInputOverwrite)
				"
				:class="$style.relatedExecutionInfo"
				data-test-id="related-execution-link"
				:href="resolveRelatedExecutionUrl(activeTaskMetadata)"
				target="_blank"
				@click.stop="trackOpeningRelatedExecution(activeTaskMetadata, displayMode)"
			>
				<N8nIcon icon="external-link-alt" size="xsmall" />
				{{ getExecutionLinkLabel(activeTaskMetadata) }}
			</a>
		</div>

		<slot v-if="!displaysMultipleNodes" name="before-data" />

		<div v-if="props.calloutMessage" :class="$style.hintCallout">
			<N8nCallout theme="secondary" data-test-id="run-data-callout">
				<N8nText v-n8n-html="props.calloutMessage" size="small"></N8nText>
			</N8nCallout>
		</div>

		<N8nCallout
			v-for="hint in getNodeHints()"
			:key="hint.message"
			:class="$style.hintCallout"
			:theme="hint.type || 'info'"
		>
			<N8nText v-n8n-html="hint.message" size="small"></N8nText>
		</N8nCallout>

		<div
			v-if="maxOutputIndex > 0 && branches.length > 1 && !displaysMultipleNodes"
			:class="$style.outputs"
			data-test-id="branches"
		>
			<slot v-if="inputSelectLocation === 'outputs'" name="input-select"></slot>

			<div :class="$style.tabs">
				<N8nTabs
					:model-value="currentOutputIndex"
					:options="branches"
					@update:model-value="onBranchChange"
				/>
			</div>
		</div>

		<div
			v-else-if="
				!hasRunError &&
				hasNodeRun &&
				!isSearchInSchemaView &&
				((dataCount > 0 && maxRunIndex === 0) || search) &&
				!isArtificialRecoveredEventItem &&
				!displaysMultipleNodes
			"
			v-show="!editMode.enabled && !hasRunError"
			:class="[$style.itemsCount, { [$style.muted]: paneType === 'input' && maxRunIndex === 0 }]"
			data-test-id="ndv-items-count"
		>
			<slot v-if="inputSelectLocation === 'items'" name="input-select"></slot>

			<N8nText v-if="search" :class="$style.itemsText">
				{{
					i18n.baseText('ndv.search.items', {
						adjustToNumber: unfilteredDataCount,
						interpolate: { matched: dataCount, total: unfilteredDataCount },
					})
				}}
			</N8nText>
			<N8nText v-else :class="$style.itemsText">
				<span>
					{{
						i18n.baseText('ndv.output.items', {
							adjustToNumber: dataCount,
							interpolate: { count: dataCount },
						})
					}}
				</span>
				<span v-if="activeTaskMetadata?.subExecutionsCount">
					{{
						i18n.baseText('ndv.output.andSubExecutions', {
							adjustToNumber: activeTaskMetadata.subExecutionsCount,
							interpolate: { count: activeTaskMetadata.subExecutionsCount },
						})
					}}
				</span>
			</N8nText>

			<a
				v-if="
					activeTaskMetadata && hasReleatedExectuion && !(paneType === 'input' && hasInputOverwrite)
				"
				:class="$style.relatedExecutionInfo"
				data-test-id="related-execution-link"
				:href="resolveRelatedExecutionUrl(activeTaskMetadata)"
				target="_blank"
				@click.stop="trackOpeningRelatedExecution(activeTaskMetadata, displayMode)"
			>
				<N8nIcon icon="external-link-alt" size="xsmall" />
				{{ getExecutionLinkLabel(activeTaskMetadata) }}
			</a>
		</div>

		<div ref="dataContainerRef" :class="$style.dataContainer" data-test-id="ndv-data-container">
			<div
				v-if="isExecuting && !isWaitNodeWaiting"
				:class="$style.center"
				data-test-id="ndv-executing"
			>
				<div :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 :error="subworkflowExecutionError" :class="$style.errorDisplay" />
			</div>

			<div v-else-if="isWaitNodeWaiting" :class="$style.center">
				<slot name="node-waiting">xxx</slot>
			</div>

			<div
				v-else-if="!hasNodeRun && !(displaysMultipleNodes && node?.disabled)"
				: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="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
					/>
					<slot name="content"></slot>
				</div>
				<NodeErrorView
					v-else-if="workflowRunErrorAsNodeError"
					:error="workflowRunErrorAsNodeError"
					:class="$style.dataDisplay"
				/>
			</div>

			<div
				v-else-if="
					hasNodeRun && (!unfilteredDataCount || (search && !dataCount)) && branches.length > 1
				"
				:class="$style.center"
			>
				<div v-if="search">
					<N8nText tag="h3" size="large">{{ i18n.baseText('ndv.search.noMatch.title') }}</N8nText>
					<N8nText>
						<i18n-t keypath="ndv.search.noMatch.description" tag="span">
							<template #link>
								<a href="#" @click="onSearchClear">
									{{ i18n.baseText('ndv.search.noMatch.description.link') }}
								</a>
							</template>
						</i18n-t>
					</N8nText>
				</div>
				<N8nText v-else>
					{{ noDataInBranchMessage }}
				</N8nText>
			</div>

			<div v-else-if="hasNodeRun && !inputData.length && !search" :class="$style.center">
				<slot name="no-output-data">xxx</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>
					<i18n-t keypath="ndv.search.noMatch.description" tag="span">
						<template #link>
							<a href="#" @click="onSearchClear">
								{{ i18n.baseText('ndv.search.noMatch.description.link') }}
							</a>
						</template>
					</i18n-t>
				</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"
					@mounted="emit('tableMounted', $event)"
					@active-row-changed="onItemHover"
					@display-mode-change="onDisplayModeChange"
				/>
			</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"
					:total-runs="maxRunIndex"
					:search="search"
				/>
			</Suspense>

			<Suspense v-else-if="hasNodeRun && isPaneTypeOutput && displayMode === 'html'">
				<LazyRunDataHtml :input-html="inputHtml" />
			</Suspense>

			<Suspense v-else-if="hasNodeRun && isSchemaView">
				<LazyRunDataSchema
					:nodes="nodes"
					:mapping-enabled="mappingEnabled"
					:node="node"
					:data="jsonData"
					:pane-type="paneType"
					:connection-type="connectionType"
					:run-index="runIndex"
					:output-index="currentOutputIndex"
					:total-runs="maxRunIndex"
					:search="search"
					:class="$style.schema"
					@clear:search="onSearchClear"
				/>
			</Suspense>

			<div v-else-if="displayMode === 'binary' && binaryData.length === 0" :class="$style.center">
				<N8nText align="center" tag="div">{{ i18n.baseText('runData.noBinaryDataFound') }}</N8nText>
			</div>

			<div v-else-if="displayMode === 'binary'" :class="$style.dataDisplay">
				<div v-for="(binaryDataEntry, index) in binaryData" :key="index">
					<div v-if="binaryData.length > 1" :class="$style.binaryIndex">
						<div>
							{{ index + 1 }}
						</div>
					</div>

					<div :class="$style.binaryRow">
						<div
							v-for="(binaryData, key) in binaryDataEntry"
							:key="index + '_' + key"
							:class="$style.binaryCell"
						>
							<div :data-test-id="'ndv-binary-data_' + index">
								<div :class="$style.binaryHeader">
									{{ key }}
								</div>
								<div v-if="binaryData.fileName">
									<div>
										<N8nText size="small" :bold="true"
											>{{ i18n.baseText('runData.fileName') }}:
										</N8nText>
									</div>
									<div :class="$style.binaryValue">{{ binaryData.fileName }}</div>
								</div>
								<div v-if="binaryData.directory">
									<div>
										<N8nText size="small" :bold="true"
											>{{ i18n.baseText('runData.directory') }}:
										</N8nText>
									</div>
									<div :class="$style.binaryValue">{{ binaryData.directory }}</div>
								</div>
								<div v-if="binaryData.fileExtension">
									<div>
										<N8nText size="small" :bold="true"
											>{{ i18n.baseText('runData.fileExtension') }}:</N8nText
										>
									</div>
									<div :class="$style.binaryValue">{{ binaryData.fileExtension }}</div>
								</div>
								<div v-if="binaryData.mimeType">
									<div>
										<N8nText size="small" :bold="true"
											>{{ i18n.baseText('runData.mimeType') }}:
										</N8nText>
									</div>
									<div :class="$style.binaryValue">{{ binaryData.mimeType }}</div>
								</div>
								<div v-if="binaryData.fileSize">
									<div>
										<N8nText size="small" :bold="true"
											>{{ i18n.baseText('runData.fileSize') }}:
										</N8nText>
									</div>
									<div :class="$style.binaryValue">{{ binaryData.fileSize }}</div>
								</div>

								<div :class="$style.binaryButtonContainer">
									<N8nButton
										v-if="isViewable(index, key)"
										size="small"
										:label="i18n.baseText('runData.showBinaryData')"
										data-test-id="ndv-view-binary-data"
										@click="displayBinaryData(index, key)"
									/>
									<N8nButton
										v-if="isDownloadable(index, key)"
										size="small"
										type="secondary"
										:label="i18n.baseText('runData.downloadBinaryData')"
										data-test-id="ndv-download-binary-data"
										@click="downloadBinaryData(index, key)"
									/>
								</div>
							</div>
						</div>
					</div>
				</div>
			</div>
		</div>
		<div
			v-if="
				hidePagination === false &&
				hasNodeRun &&
				!hasRunError &&
				displayMode !== 'binary' &&
				dataCount > pageSize &&
				!isSchemaView &&
				!isArtificialRecoveredEventItem
			"
			v-show="!editMode.enabled"
			:class="$style.pagination"
			data-test-id="ndv-data-pagination"
		>
			<el-pagination
				background
				:hide-on-single-page="true"
				:current-page="currentPage"
				:pager-count="5"
				:page-size="pageSize"
				layout="prev, pager, next"
				:total="dataCount"
				@update:current-page="onCurrentPageChange"
			>
			</el-pagination>

			<div :class="$style.pageSizeSelector">
				<N8nSelect
					size="mini"
					:model-value="pageSize"
					teleported
					@update:model-value="onPageSizeChange"
				>
					<template #prepend>{{ i18n.baseText('ndv.output.pageSize') }}</template>
					<N8nOption v-for="size in pageSizes" :key="size" :label="size" :value="size"> </N8nOption>
					<N8nOption :label="i18n.baseText('ndv.output.all')" :value="dataCount"> </N8nOption>
				</N8nSelect>
			</div>
		</div>
		<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(--spacing-s) var(--spacing-s) var(--spacing-xl) var(--spacing-s);
	text-align: center;

	> * {
		max-width: 316px;
		margin-bottom: var(--spacing-2xs);
	}
}

.container {
	position: relative;
	width: 100%;
	height: 100%;
	background-color: var(--color-run-data-background);
	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(--spacing-s);
	padding: var(--spacing-s) var(--spacing-s) 0 var(--spacing-s);
	position: relative;
	overflow-x: auto;
	overflow-y: hidden;
	min-height: calc(30px + var(--spacing-s));
	scrollbar-width: thin;

	> *:first-child {
		flex-grow: 1;
	}
}

.dataContainer {
	position: relative;
	overflow-y: auto;
	height: 100%;

	&:hover {
		.actions-group {
			opacity: 1;
		}
	}
}

.dataDisplay {
	position: absolute;
	top: 0;
	left: 0;
	padding: 0 var(--spacing-s) var(--spacing-3xl) var(--spacing-s);
	right: 0;
	overflow-y: auto;
	line-height: var(--font-line-height-xloose);
	word-break: normal;
	height: 100%;
}

.inlineError {
	line-height: var(--font-line-height-xloose);
	padding-left: var(--spacing-s);
	padding-right: var(--spacing-s);
	padding-bottom: var(--spacing-s);
}

.outputs {
	display: flex;
	flex-direction: column;
	gap: var(--spacing-s);
	padding-left: var(--spacing-s);
	padding-right: var(--spacing-s);
	padding-bottom: var(--spacing-s);
}

.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(--spacing-s);
	padding-right: var(--spacing-s);
	padding-bottom: var(--spacing-s);
	flex-flow: wrap;

	.itemsText {
		flex-shrink: 0;
		overflow: hidden;
		white-space: nowrap;
		text-overflow: ellipsis;
	}

	&.muted .itemsText {
		color: var(--color-text-light);
		font-size: var(--font-size-2xs);
	}
}

.inputSelect {
	padding-left: var(--spacing-s);
	padding-right: var(--spacing-s);
	padding-bottom: var(--spacing-s);
}

.runSelector {
	display: flex;
	align-items: center;
	flex-flow: wrap;
	padding-left: var(--spacing-s);
	padding-right: var(--spacing-s);
	margin-bottom: var(--spacing-s);
	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;
}

.pagination {
	width: 100%;
	display: flex;
	justify-content: center;
	align-items: center;
	bottom: 0;
	padding: 5px;
	overflow-y: hidden;
}

.pageSizeSelector {
	text-transform: capitalize;
	max-width: 150px;
	flex: 0 1 auto;
}

.binaryIndex {
	display: block;
	padding: var(--spacing-2xs);
	font-size: var(--font-size-2xs);

	> * {
		display: inline-block;
		width: 30px;
		height: 30px;
		line-height: 30px;
		border-radius: var(--border-radius-base);
		text-align: center;
		background-color: var(--color-foreground-xdark);
		font-weight: var(--font-weight-bold);
		color: var(--color-text-xlight);
	}
}

.binaryRow {
	display: inline-flex;
	font-size: var(--font-size-2xs);
}

.binaryCell {
	display: inline-block;
	width: 300px;
	overflow: hidden;
	background-color: var(--color-foreground-xlight);
	margin-right: var(--spacing-s);
	margin-bottom: var(--spacing-s);
	border-radius: var(--border-radius-base);
	border: var(--border-base);
	padding: var(--spacing-s);
}

.binaryHeader {
	color: $color-primary;
	font-weight: var(--font-weight-bold);
	font-size: 1.2em;
	padding-bottom: var(--spacing-2xs);
	margin-bottom: var(--spacing-2xs);
	border-bottom: 1px solid var(--color-text-light);
}

.binaryButtonContainer {
	margin-top: 1.5em;
	display: flex;
	flex-direction: row;
	justify-content: center;

	> * {
		flex-grow: 0;
		margin-right: var(--spacing-3xs);
	}
}

.binaryValue {
	white-space: initial;
	word-wrap: break-word;
}

.displayModes {
	display: flex;
	justify-content: flex-end;
	flex-grow: 1;
	gap: var(--spacing-2xs);
}

.tooltipContain {
	max-width: 240px;
}

.spinner {
	* {
		color: var(--color-primary);
		min-height: 40px;
		min-width: 40px;
	}

	display: flex;
	justify-content: center;
	margin-bottom: var(--spacing-s);
}

.editMode {
	height: 100%;
	display: flex;
	flex-direction: column;
	justify-content: stretch;
	padding-left: var(--spacing-s);
	padding-right: var(--spacing-s);
}

.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(--spacing-s);
	padding-bottom: var(--spacing-s);
}

.editModeFooterInfotip {
	display: flex;
	flex: 1;
	width: 100%;
}

.editModeActions {
	display: flex;
	justify-content: flex-end;
	align-items: center;
	margin-left: var(--spacing-s);
}

.stretchVertically {
	height: 100%;
}

.uiBlocker {
	border-top-left-radius: 0;
	border-bottom-left-radius: 0;
}

.hintCallout {
	margin-bottom: var(--spacing-xs);
	margin-left: var(--spacing-s);
	margin-right: var(--spacing-s);
}

.schema {
	padding: 0 var(--spacing-s);
}

.relatedExecutionInfo {
	font-size: var(--font-size-s);
	margin-left: var(--spacing-3xs);

	svg {
		padding-bottom: 2px;
	}
}
</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: normal;
	font-style: normal;
}
</style>
