mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
2271 lines
58 KiB
Vue
2271 lines
58 KiB
Vue
<script setup lang="ts">
|
|
import { useStorage } from '@/composables/useStorage';
|
|
import { saveAs } from 'file-saver';
|
|
import {
|
|
type IBinaryData,
|
|
type IConnectedNode,
|
|
type IDataObject,
|
|
type INodeExecutionData,
|
|
type INodeOutputConfiguration,
|
|
type IRunData,
|
|
type IRunExecutionData,
|
|
type ITaskMetadata,
|
|
type NodeError,
|
|
type NodeHint,
|
|
type Workflow,
|
|
TRIMMED_TASK_DATA_CONNECTIONS_KEY,
|
|
} 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,
|
|
N8nRoute,
|
|
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 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(() =>
|
|
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 onExecutionHistoryNavigate() {
|
|
ndvStore.setActiveNodeName(null);
|
|
}
|
|
|
|
function getExecutionLinkLabel(task: ITaskMetadata): string | undefined {
|
|
if (task.parentExecution) {
|
|
return i18n.baseText('runData.openParentExecution', {
|
|
interpolate: { id: task.parentExecution.executionId },
|
|
});
|
|
}
|
|
|
|
if (task.subExecution) {
|
|
if (activeTaskMetadata.value?.subExecutionsCount === 1) {
|
|
return i18n.baseText('runData.openSubExecutionSingle');
|
|
} else {
|
|
return i18n.baseText('runData.openSubExecutionWithId', {
|
|
interpolate: { id: task.subExecution.executionId },
|
|
});
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
defineExpose({ enterEditMode });
|
|
</script>
|
|
|
|
<template>
|
|
<div :class="['run-data', $style.container]" @mouseover="activatePane">
|
|
<N8nCallout
|
|
v-if="
|
|
!isPaneTypeInput &&
|
|
pinnedData.hasData.value &&
|
|
!editMode.enabled &&
|
|
!isProductionExecutionPreview
|
|
"
|
|
theme="secondary"
|
|
icon="thumbtack"
|
|
:class="$style.pinnedDataCallout"
|
|
data-test-id="ndv-pinned-data-callout"
|
|
>
|
|
{{ 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 && !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>
|
|
|
|
<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="isTrimmedManualExecutionDataItem" :class="$style.center">
|
|
<N8nText bold color="text-dark" size="large">
|
|
{{ i18n.baseText('runData.trimmedData.title') }}
|
|
</N8nText>
|
|
<N8nText>
|
|
{{ i18n.baseText('runData.trimmedData.message') }}
|
|
</N8nText>
|
|
<N8nButton size="small" @click="onExecutionHistoryNavigate">
|
|
<N8nRoute :to="`/workflow/${workflowsStore.workflowId}/executions`">
|
|
{{ i18n.baseText('runData.trimmedData.button') }}
|
|
</N8nRoute>
|
|
</N8nButton>
|
|
</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>
|