n8n/packages/editor-ui/src/components/RunData.vue

2038 lines
53 KiB
Vue

<template>
<div :class="['run-data', $style.container]" @mouseover="activatePane">
<n8n-callout
v-if="
canPinData && pinnedData.hasData.value && !editMode.enabled && !isProductionExecutionPreview
"
theme="secondary"
icon="thumbtack"
:class="$style.pinnedDataCallout"
>
{{ $locale.baseText('runData.pindata.thisDataIsPinned') }}
<span v-if="!isReadOnlyRoute && !readOnlyEnv" class="ml-4xs">
<n8n-link
theme="secondary"
size="small"
underline
bold
@click.stop="onTogglePinData({ source: 'banner-link' })"
>
{{ $locale.baseText('runData.pindata.unpin') }}
</n8n-link>
</span>
<template #trailingContent>
<n8n-link
:to="dataPinningDocsUrl"
size="small"
theme="secondary"
bold
underline
@click="onClickDataPinningDocsLink"
>
{{ $locale.baseText('runData.pindata.learnMore') }}
</n8n-link>
</template>
</n8n-callout>
<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>
<n8n-radio-buttons
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"
/>
<n8n-icon-button
v-if="canPinData && !isReadOnlyRoute && !readOnlyEnv"
v-show="!editMode.enabled"
:title="$locale.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="(canPinData || !!binaryData?.length) && rawInputData.length && !editMode.enabled"
:disabled="
(!rawInputData.length && !pinnedData.hasData.value) ||
isReadOnlyRoute ||
readOnlyEnv ||
!!binaryData?.length
"
:tooltip-contents-visibility="{
binaryDataTooltipContent: !!binaryData?.length,
pinDataDiscoveryTooltipContent:
isControlledPinDataTooltip && pinDataDiscoveryTooltipVisible,
}"
:data-pinning-docs-url="dataPinningDocsUrl"
:pinned-data="pinnedData"
@toggle-pin-data="onTogglePinData({ source: 'pin-icon-click' })"
/>
<div v-show="editMode.enabled" :class="$style.editModeActions">
<n8n-button
type="tertiary"
:label="$locale.baseText('runData.editor.cancel')"
@click="onClickCancelEdit"
/>
<n8n-button
class="ml-2xs"
type="primary"
:label="$locale.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 && !isInputSchemaView"
v-show="!editMode.enabled"
:class="$style.runSelector"
>
<slot v-if="inputSelectLocation === 'runs'" name="input-select"></slot>
<n8n-select
:model-value="runIndex"
:class="$style.runSelectorInner"
size="small"
teleported
data-test-id="run-selector"
@update:model-value="onRunIndexChange"
@click.stop
>
<template #prepend>{{ $locale.baseText('ndv.output.run') }}</template>
<n8n-option
v-for="option in maxRunIndex + 1"
:key="option"
:label="getRunLabel(option)"
:value="option - 1"
></n8n-option>
</n8n-select>
<n8n-tooltip v-if="canLinkRuns" placement="right">
<template #content>
{{ $locale.baseText(linkedRuns ? 'runData.unlinking.hint' : 'runData.linking.hint') }}
</template>
<n8n-icon-button
:icon="linkedRuns ? 'unlink' : 'link'"
class="linkRun"
text
type="tertiary"
size="small"
data-test-id="link-run"
@click="toggleLinkRuns"
/>
</n8n-tooltip>
<slot name="run-info"></slot>
</div>
<slot v-if="!isInputSchemaView" name="before-data" />
<n8n-callout
v-for="hint in getNodeHints()"
:key="hint.message"
:class="$style.hintCallout"
:theme="hint.type || 'info'"
>
<n8n-text size="small" v-html="hint.message"></n8n-text>
</n8n-callout>
<div
v-if="maxOutputIndex > 0 && branches.length > 1 && !isInputSchemaView"
:class="$style.outputs"
data-test-id="branches"
>
<slot v-if="inputSelectLocation === 'outputs'" name="input-select"></slot>
<div :class="$style.tabs">
<n8n-tabs
:model-value="currentOutputIndex"
:options="branches"
@update:model-value="onBranchChange"
/>
</div>
</div>
<div
v-else-if="
!hasRunError &&
hasNodeRun &&
((dataCount > 0 && maxRunIndex === 0) || search) &&
!isArtificialRecoveredEventItem &&
!isInputSchemaView
"
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>
<n8n-text v-if="search" :class="$style.itemsText">
{{
$locale.baseText('ndv.search.items', {
adjustToNumber: unfilteredDataCount,
interpolate: { matched: dataCount, total: unfilteredDataCount },
})
}}
</n8n-text>
<n8n-text v-else :class="$style.itemsText">
{{
$locale.baseText('ndv.output.items', {
adjustToNumber: dataCount,
interpolate: { count: dataCount },
})
}}
</n8n-text>
</div>
<div ref="dataContainer" :class="$style.dataContainer" data-test-id="ndv-data-container">
<div v-if="isExecuting" :class="$style.center" data-test-id="ndv-executing">
<div :class="$style.spinner"><n8n-spinner type="ring" /></div>
<n8n-text>{{ executingMessage }}</n8n-text>
</div>
<div v-else-if="editMode.enabled" :class="$style.editMode">
<div :class="[$style.editModeBody, 'ignore-key-press']">
<JsonEditor
:model-value="editMode.value"
:fill-parent="true"
@update:model-value="ndvStore.setOutputPanelEditModeValue($event)"
/>
</div>
<div :class="$style.editModeFooter">
<n8n-info-tip :bold="false" :class="$style.editModeFooterInfotip">
{{ $locale.baseText('runData.editor.copyDataInfo') }}
<n8n-link :to="dataEditingDocsUrl" size="small">
{{ $locale.baseText('generic.learnMore') }}
</n8n-link>
</n8n-info-tip>
</div>
</div>
<div
v-else-if="paneType === 'output' && hasSubworkflowExecutionError"
:class="$style.stretchVertically"
>
<NodeErrorView :error="subworkflowExecutionError" :class="$style.errorDisplay" />
</div>
<div v-else-if="!hasNodeRun && !(isInputSchemaView && node?.disabled)" :class="$style.center">
<slot name="node-not-run"></slot>
</div>
<div
v-else-if="paneType === 'input' && !isInputSchemaView && node?.disabled"
:class="$style.center"
>
<n8n-text>
{{ $locale.baseText('ndv.input.disabled', { interpolate: { nodeName: node.name } }) }}
<n8n-link @click="enableNode">
{{ $locale.baseText('ndv.input.disabled.cta') }}
</n8n-link>
</n8n-text>
</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">
<n8n-text v-if="isPaneTypeInput" :class="$style.center" size="large" tag="p" bold>
{{
$locale.baseText('nodeErrorView.inputPanel.previousNodeError.title', {
interpolate: { nodeName: node?.name ?? '' },
})
}}
</n8n-text>
<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">
<n8n-text tag="h3" size="large">{{
$locale.baseText('ndv.search.noMatch.title')
}}</n8n-text>
<n8n-text>
<i18n-t keypath="ndv.search.noMatch.description" tag="span">
<template #link>
<a href="#" @click="onSearchClear">
{{ $locale.baseText('ndv.search.noMatch.description.link') }}
</a>
</template>
</i18n-t>
</n8n-text>
</div>
<n8n-text v-else>
{{ noDataInBranchMessage }}
</n8n-text>
</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" :class="$style.center">
<n8n-text :bold="true" color="text-dark" size="large">{{ tooMuchDataTitle }}</n8n-text>
<n8n-text align="center" tag="div"
><span
v-html="
$locale.baseText('ndv.output.tooMuchData.message', {
interpolate: { size: dataSizeInMB },
})
"
></span
></n8n-text>
<n8n-button
outline
:label="$locale.baseText('ndv.output.tooMuchData.showDataAnyway')"
@click="showTooMuchData"
/>
<n8n-button
size="small"
:label="$locale.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"
>
<n8n-text>
{{ $locale.baseText('runData.switchToBinary.info') }}
<a @click="switchToBinary">
{{ $locale.baseText('runData.switchToBinary.binary') }}
</a>
</n8n-text>
</div>
<div v-else-if="showIoSearchNoMatchContent" :class="$style.center">
<n8n-text tag="h3" size="large">{{
$locale.baseText('ndv.search.noMatch.title')
}}</n8n-text>
<n8n-text>
<i18n-t keypath="ndv.search.noMatch.description" tag="span">
<template #link>
<a href="#" @click="onSearchClear">
{{ $locale.baseText('ndv.search.noMatch.description.link') }}
</a>
</template>
</i18n-t>
</n8n-text>
</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">
<n8n-text align="center" tag="div">{{
$locale.baseText('runData.noBinaryDataFound')
}}</n8n-text>
</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>
<n8n-text size="small" :bold="true"
>{{ $locale.baseText('runData.fileName') }}:
</n8n-text>
</div>
<div :class="$style.binaryValue">{{ binaryData.fileName }}</div>
</div>
<div v-if="binaryData.directory">
<div>
<n8n-text size="small" :bold="true"
>{{ $locale.baseText('runData.directory') }}:
</n8n-text>
</div>
<div :class="$style.binaryValue">{{ binaryData.directory }}</div>
</div>
<div v-if="binaryData.fileExtension">
<div>
<n8n-text size="small" :bold="true"
>{{ $locale.baseText('runData.fileExtension') }}:</n8n-text
>
</div>
<div :class="$style.binaryValue">{{ binaryData.fileExtension }}</div>
</div>
<div v-if="binaryData.mimeType">
<div>
<n8n-text size="small" :bold="true"
>{{ $locale.baseText('runData.mimeType') }}:
</n8n-text>
</div>
<div :class="$style.binaryValue">{{ binaryData.mimeType }}</div>
</div>
<div v-if="binaryData.fileSize">
<div>
<n8n-text size="small" :bold="true"
>{{ $locale.baseText('runData.fileSize') }}:
</n8n-text>
</div>
<div :class="$style.binaryValue">{{ binaryData.fileSize }}</div>
</div>
<div :class="$style.binaryButtonContainer">
<n8n-button
v-if="isViewable(index, key)"
size="small"
:label="$locale.baseText('runData.showBinaryData')"
data-test-id="ndv-view-binary-data"
@click="displayBinaryData(index, key)"
/>
<n8n-button
v-if="isDownloadable(index, key)"
size="small"
type="secondary"
:label="$locale.baseText('runData.downloadBinaryData')"
data-test-id="ndv-download-binary-data"
@click="downloadBinaryData(index, key)"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
v-if="
hasNodeRun &&
!hasRunError &&
binaryData.length === 0 &&
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">
<n8n-select
size="mini"
:model-value="pageSize"
teleported
@update:model-value="onPageSizeChange"
>
<template #prepend>{{ $locale.baseText('ndv.output.pageSize') }}</template>
<n8n-option v-for="size in pageSizes" :key="size" :label="size" :value="size">
</n8n-option>
<n8n-option :label="$locale.baseText('ndv.output.all')" :value="dataCount"> </n8n-option>
</n8n-select>
</div>
</div>
<n8n-block-ui :show="blockUI" :class="$style.uiBlocker" />
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent, toRef } from 'vue';
import type { PropType } from 'vue';
import { mapStores } from 'pinia';
import { useStorage } from '@/composables/useStorage';
import { saveAs } from 'file-saver';
import type {
ConnectionTypes,
IBinaryData,
IBinaryKeyData,
IDataObject,
INodeExecutionData,
INodeOutputConfiguration,
INodeTypeDescription,
IRunData,
IRunExecutionData,
NodeHint,
NodeError,
Workflow,
IConnectedNode,
} from 'n8n-workflow';
import { NodeHelpers, NodeConnectionType } from 'n8n-workflow';
import type {
IExecutionResponse,
INodeUi,
INodeUpdatePropertiesInformation,
IRunDataDisplayMode,
ITab,
NodePanelType,
} from '@/Interface';
import {
DATA_PINNING_DOCS_URL,
DATA_EDITING_DOCS_URL,
NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND,
LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG,
LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG,
MAX_DISPLAY_DATA_SIZE,
MAX_DISPLAY_DATA_SIZE_SCHEMA_VIEW,
MAX_DISPLAY_ITEMS_AUTO_ALL,
TEST_PIN_DATA,
HTML_NODE_TYPE,
} from '@/constants';
import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
import NodeErrorView from '@/components/Error/NodeErrorView.vue';
import JsonEditor from '@/components/JsonEditor/JsonEditor.vue';
import type { PinDataSource, UnpinDataSource } from '@/composables/usePinnedData';
import { usePinnedData } from '@/composables/usePinnedData';
import { dataPinningEventBus } from '@/event-bus';
import { clearJsonKey, isEmpty } from '@/utils/typesUtils';
import { executionDataToJson } from '@/utils/nodeTypesUtils';
import { searchInObject } from '@/utils/objectUtils';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useToast } from '@/composables/useToast';
import { isEqual, isObject } from 'lodash-es';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useRootStore } from '@/stores/root.store';
import RunDataPinButton from '@/components/RunDataPinButton.vue';
const LazyRunDataTable = defineAsyncComponent(
async () => await import('@/components/RunDataTable.vue'),
);
const LazyRunDataJson = defineAsyncComponent(
async () => await import('@/components/RunDataJson.vue'),
);
const LazyRunDataSchema = defineAsyncComponent(
async () => await import('@/components/RunDataSchema.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';
};
export default defineComponent({
name: 'RunData',
components: {
BinaryDataDisplay,
NodeErrorView,
JsonEditor,
LazyRunDataTable,
LazyRunDataJson,
LazyRunDataSchema,
LazyRunDataHtml,
LazyRunDataSearch,
RunDataPinButton,
},
props: {
node: {
type: Object as PropType<INodeUi | null>,
default: null,
},
nodes: {
type: Array as PropType<IConnectedNode[]>,
default: () => [],
},
workflow: {
type: Object as PropType<Workflow>,
required: true,
},
runIndex: {
type: Number,
required: true,
},
linkedRuns: {
type: Boolean,
},
canLinkRuns: {
type: Boolean,
},
tooMuchDataTitle: {
type: String,
},
noDataInBranchMessage: {
type: String,
},
isExecuting: {
type: Boolean,
},
executingMessage: {
type: String,
},
pushRef: {
type: String,
},
paneType: {
type: String as PropType<NodePanelType>,
required: true,
},
overrideOutputs: {
type: Array as PropType<number[]>,
},
mappingEnabled: {
type: Boolean,
},
distanceFromActive: {
type: Number,
default: 0,
},
blockUI: {
type: Boolean,
default: false,
},
isProductionExecutionPreview: {
type: Boolean,
default: false,
},
isPaneActive: {
type: Boolean,
default: false,
},
},
setup(props) {
const ndvStore = useNDVStore();
const nodeHelpers = useNodeHelpers();
const externalHooks = useExternalHooks();
const node = toRef(props, 'node');
const pinnedData = usePinnedData(node, {
runIndex: props.runIndex,
displayMode: ndvStore.getPanelDisplayMode(props.paneType),
});
return {
...useToast(),
externalHooks,
nodeHelpers,
pinnedData,
};
},
data() {
return {
connectionType: NodeConnectionType.Main as ConnectionTypes,
binaryDataPreviewActive: false,
dataSize: 0,
showData: false,
userEnabledShowData: false,
outputIndex: 0,
binaryDataDisplayVisible: false,
binaryDataDisplayData: null as IBinaryData | null,
MAX_DISPLAY_DATA_SIZE,
MAX_DISPLAY_DATA_SIZE_SCHEMA_VIEW,
MAX_DISPLAY_ITEMS_AUTO_ALL,
currentPage: 1,
pageSize: 10,
pageSizes: [10, 25, 50, 100],
pinDataDiscoveryTooltipVisible: false,
isControlledPinDataTooltip: false,
search: '',
};
},
computed: {
...mapStores(
useNodeTypesStore,
useNDVStore,
useWorkflowsStore,
useSourceControlStore,
useRootStore,
),
isReadOnlyRoute() {
return this.$route?.meta?.readOnlyCanvas === true;
},
activeNode(): INodeUi | null {
return this.ndvStore.activeNode;
},
dataPinningDocsUrl(): string {
return DATA_PINNING_DOCS_URL;
},
dataEditingDocsUrl(): string {
return DATA_EDITING_DOCS_URL;
},
displayMode(): IRunDataDisplayMode {
return this.ndvStore.getPanelDisplayMode(this.paneType);
},
nodeType(): INodeTypeDescription | null {
if (this.node) {
return this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion);
}
return null;
},
isSchemaView(): boolean {
return this.displayMode === 'schema';
},
isInputSchemaView(): boolean {
return this.isSchemaView && this.paneType === 'input';
},
isTriggerNode(): boolean {
if (this.node === null) {
return false;
}
return this.nodeTypesStore.isTriggerNode(this.node.type);
},
canPinData(): boolean {
if (this.node === null) {
return false;
}
const canPinNode = usePinnedData(this.node).canPinNode(false);
return (
canPinNode &&
!this.isPaneTypeInput &&
this.pinnedData.isValidNodeType.value &&
!(this.binaryData && this.binaryData.length > 0)
);
},
displayModes(): Array<{ label: string; value: string }> {
const defaults = [
{ label: this.$locale.baseText('runData.table'), value: 'table' },
{ label: this.$locale.baseText('runData.json'), value: 'json' },
];
if (this.binaryData.length) {
defaults.push({ label: this.$locale.baseText('runData.binary'), value: 'binary' });
}
const schemaView = { label: this.$locale.baseText('runData.schema'), value: 'schema' };
if (this.isPaneTypeInput) {
defaults.unshift(schemaView);
} else {
defaults.push(schemaView);
}
if (
this.isPaneTypeOutput &&
this.activeNode?.type === HTML_NODE_TYPE &&
this.activeNode.parameters.operation === 'generateHtmlTemplate'
) {
defaults.unshift({ label: 'HTML', value: 'html' });
}
return defaults;
},
hasNodeRun(): boolean {
return Boolean(
!this.isExecuting &&
this.node &&
((this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name)) ||
this.pinnedData.hasData.value),
);
},
isArtificialRecoveredEventItem(): boolean {
return !!this.rawInputData?.[0]?.json?.isArtificialRecoveredEventItem;
},
subworkflowExecutionError(): NodeError {
return {
node: this.node,
messages: [this.workflowsStore.subWorkflowExecutionError?.message ?? ''],
} as NodeError;
},
hasSubworkflowExecutionError(): boolean {
return Boolean(this.workflowsStore.subWorkflowExecutionError);
},
workflowRunErrorAsNodeError(): NodeError | null {
if (!this.node) {
return null;
}
return this.workflowRunData?.[this.node?.name]?.[this.runIndex]?.error as NodeError;
},
hasRunError(): boolean {
return Boolean(this.node && this.workflowRunErrorAsNodeError);
},
executionHints(): NodeHint[] {
if (this.hasNodeRun) {
const hints = this.node && this.workflowRunData?.[this.node.name]?.[this.runIndex]?.hints;
if (hints) return hints;
}
return [];
},
workflowExecution(): IExecutionResponse | null {
return this.workflowsStore.getWorkflowExecution;
},
workflowRunData(): IRunData | null {
if (this.workflowExecution === null) {
return null;
}
const executionData: IRunExecutionData | undefined = this.workflowExecution.data;
if (executionData?.resultData) {
return executionData.resultData.runData;
}
return null;
},
dataCount(): number {
return this.getDataCount(this.runIndex, this.currentOutputIndex);
},
unfilteredDataCount(): number {
return this.pinnedData.data.value
? this.pinnedData.data.value.length
: this.rawInputData.length;
},
dataSizeInMB(): string {
return (this.dataSize / (1024 * 1024)).toFixed(1);
},
maxOutputIndex(): number {
if (this.node === null || this.runIndex === undefined) {
return 0;
}
const runData: IRunData | null = this.workflowRunData;
if (runData === null || !runData.hasOwnProperty(this.node.name)) {
return 0;
}
if (runData[this.node.name].length < this.runIndex) {
return 0;
}
if (runData[this.node.name][this.runIndex]) {
const taskData = runData[this.node.name][this.runIndex].data;
if (taskData?.main) {
return taskData.main.length - 1;
}
}
return 0;
},
currentPageOffset(): number {
return this.pageSize * (this.currentPage - 1);
},
maxRunIndex(): number {
if (this.node === null) {
return 0;
}
const runData: IRunData | null = this.workflowRunData;
if (runData === null || !runData.hasOwnProperty(this.node.name)) {
return 0;
}
if (runData[this.node.name].length) {
return runData[this.node.name].length - 1;
}
return 0;
},
rawInputData(): INodeExecutionData[] {
return this.getRawInputData(this.runIndex, this.currentOutputIndex, this.connectionType);
},
unfilteredInputData(): INodeExecutionData[] {
return this.getPinDataOrLiveData(this.rawInputData);
},
inputData(): INodeExecutionData[] {
return this.getFilteredData(this.unfilteredInputData);
},
inputDataPage(): INodeExecutionData[] {
const offset = this.pageSize * (this.currentPage - 1);
return this.inputData.slice(offset, offset + this.pageSize);
},
jsonData(): IDataObject[] {
return executionDataToJson(this.inputData);
},
binaryData(): IBinaryKeyData[] {
if (!this.node) {
return [];
}
const binaryData = this.nodeHelpers.getBinaryData(
this.workflowRunData,
this.node.name,
this.runIndex,
this.currentOutputIndex,
);
return binaryData.filter((data) => Boolean(data && Object.keys(data).length));
},
inputHtml(): string {
return String(this.inputData[0]?.json?.html ?? '');
},
currentOutputIndex(): number {
if (this.overrideOutputs?.length && !this.overrideOutputs.includes(this.outputIndex)) {
return this.overrideOutputs[0];
}
return this.outputIndex;
},
branches(): ITab[] {
const capitalize = (name: string) => name.charAt(0).toLocaleUpperCase() + name.slice(1);
const branches: ITab[] = [];
for (let i = 0; i <= this.maxOutputIndex; i++) {
if (this.overrideOutputs && !this.overrideOutputs.includes(i)) {
continue;
}
const totalItemsCount = this.getRawInputData(this.runIndex, i).length;
const itemsCount = this.getDataCount(this.runIndex, i);
const items = this.search
? this.$locale.baseText('ndv.search.items', {
adjustToNumber: totalItemsCount,
interpolate: { matched: itemsCount, total: totalItemsCount },
})
: this.$locale.baseText('ndv.output.items', {
adjustToNumber: itemsCount,
interpolate: { count: itemsCount },
});
let outputName = this.getOutputName(i);
if (`${outputName}` === `${i}`) {
outputName = `${this.$locale.baseText('ndv.output')} ${outputName}`;
} else {
const appendBranchWord = NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND.includes(
this.node?.type ?? '',
)
? ''
: ` ${this.$locale.baseText('ndv.output.branch')}`;
outputName = capitalize(`${this.getOutputName(i)}${appendBranchWord}`);
}
branches.push({
label:
(this.search && itemsCount) || totalItemsCount
? `${outputName} (${items})`
: outputName,
value: i,
});
}
return branches;
},
editMode(): { enabled: boolean; value: string } {
return this.isPaneTypeInput
? { enabled: false, value: '' }
: this.ndvStore.outputPanelEditMode;
},
isPaneTypeInput(): boolean {
return this.paneType === 'input';
},
isPaneTypeOutput(): boolean {
return this.paneType === 'output';
},
readOnlyEnv(): boolean {
return this.sourceControlStore.preferences.branchReadOnly;
},
showIOSearch(): boolean {
return this.hasNodeRun && !this.hasRunError && this.unfilteredInputData.length > 0;
},
inputSelectLocation() {
if (this.isSchemaView) return 'none';
if (!this.hasNodeRun) return 'header';
if (this.maxRunIndex > 0) return 'runs';
if (this.maxOutputIndex > 0 && this.branches.length > 1) {
return 'outputs';
}
return 'items';
},
showIoSearchNoMatchContent(): boolean {
return this.hasNodeRun && !this.inputData.length && !!this.search;
},
parentNodeOutputData(): INodeExecutionData[] {
const parentNode = this.workflow.getParentNodesByDepth(this.node?.name ?? '')[0];
let parentNodeData: INodeExecutionData[] = [];
if (parentNode?.name) {
parentNodeData = this.nodeHelpers.getNodeInputData(
this.workflow.getNode(parentNode?.name),
this.runIndex,
this.outputIndex,
'input',
this.connectionType,
);
}
return parentNodeData;
},
},
watch: {
node(newNode: INodeUi, prevNode: INodeUi) {
if (newNode?.id === prevNode?.id) return;
this.init();
},
hasNodeRun() {
if (this.paneType === 'output') this.setDisplayMode();
},
inputDataPage: {
handler(data: INodeExecutionData[]) {
if (this.paneType && data) {
this.ndvStore.setNDVPanelDataIsEmpty({
panel: this.paneType as 'input' | 'output',
isEmpty: data.every((item) => isEmpty(item.json)),
});
}
},
immediate: true,
deep: true,
},
jsonData(data: IDataObject[], prevData: IDataObject[]) {
if (isEqual(data, prevData)) return;
this.refreshDataSize();
if (this.dataCount) {
this.resetCurrentPageIfTooFar();
}
this.showPinDataDiscoveryTooltip(data);
},
binaryData(newData: IBinaryKeyData[], prevData: IBinaryKeyData[]) {
if (newData.length && !prevData.length && this.displayMode !== 'binary') {
this.switchToBinary();
} else if (!newData.length && this.displayMode === 'binary') {
this.onDisplayModeChange('table');
}
},
currentOutputIndex(branchIndex: number) {
this.ndvStore.setNDVBranchIndex({
pane: this.paneType as 'input' | 'output',
branchIndex,
});
},
search(newSearch: string) {
this.$emit('search', newSearch);
},
},
mounted() {
this.init();
if (!this.isPaneTypeInput) {
this.showPinDataDiscoveryTooltip(this.jsonData);
}
this.ndvStore.setNDVBranchIndex({
pane: this.paneType as 'input' | 'output',
branchIndex: this.currentOutputIndex,
});
if (this.paneType === 'output') {
this.setDisplayMode();
this.activatePane();
}
if (this.hasRunError && this.node) {
const error = this.workflowRunData?.[this.node.name]?.[this.runIndex]?.error;
const errorsToTrack = ['unknown error'];
if (error && errorsToTrack.some((e) => error.message?.toLowerCase().includes(e))) {
this.$telemetry.track(
`User encountered an error: "${error.message}"`,
{
node: this.node.type,
errorMessage: error.message,
nodeVersion: this.node.typeVersion,
n8nVersion: this.rootStore.versionCli,
},
{
withPostHog: true,
},
);
}
}
},
beforeUnmount() {
this.hidePinDataDiscoveryTooltip();
},
methods: {
getResolvedNodeOutputs() {
if (this.node && this.nodeType) {
const workflowNode = this.workflow.getNode(this.node.name);
if (workflowNode) {
const outputs = NodeHelpers.getNodeOutputs(this.workflow, workflowNode, this.nodeType);
return outputs;
}
}
return [];
},
shouldHintBeDisplayed(hint: NodeHint): boolean {
const { location, whenToDisplay } = hint;
if (location) {
if (location === 'ndv' && !['input', 'output'].includes(this.paneType)) {
return false;
}
if (location === 'inputPane' && this.paneType !== 'input') {
return false;
}
if (location === 'outputPane' && this.paneType !== 'output') {
return false;
}
}
if (whenToDisplay === 'afterExecution' && !this.hasNodeRun) {
return false;
}
if (whenToDisplay === 'beforeExecution' && this.hasNodeRun) {
return false;
}
return true;
},
getNodeHints(): NodeHint[] {
if (this.node && this.nodeType) {
const workflowNode = this.workflow.getNode(this.node.name);
if (workflowNode) {
const executionHints = this.executionHints;
const nodeHints = NodeHelpers.getNodeHints(this.workflow, workflowNode, this.nodeType, {
runExecutionData: this.workflowExecution?.data ?? null,
runIndex: this.runIndex,
connectionInputData: this.parentNodeOutputData,
});
return executionHints.concat(nodeHints).filter(this.shouldHintBeDisplayed);
}
}
return [];
},
onItemHover(itemIndex: number | null) {
if (itemIndex === null) {
this.$emit('itemHover', null);
return;
}
this.$emit('itemHover', {
outputIndex: this.currentOutputIndex,
itemIndex,
});
},
onClickDataPinningDocsLink() {
this.$telemetry.track('User clicked ndv link', {
workflow_id: this.workflowsStore.workflowId,
push_ref: this.pushRef,
node_type: this.activeNode?.type,
pane: 'output',
type: 'data-pinning-docs',
});
},
showPinDataDiscoveryTooltip(value: IDataObject[]) {
if (!this.isTriggerNode) {
return;
}
const pinDataDiscoveryFlag = useStorage(LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG).value;
if (value && value.length > 0 && !this.isReadOnlyRoute && !pinDataDiscoveryFlag) {
this.pinDataDiscoveryComplete();
setTimeout(() => {
this.isControlledPinDataTooltip = true;
this.pinDataDiscoveryTooltipVisible = true;
dataPinningEventBus.emit('data-pinning-discovery', { isTooltipVisible: true });
}, 500); // Wait for NDV to open
}
},
hidePinDataDiscoveryTooltip() {
if (this.pinDataDiscoveryTooltipVisible) {
this.isControlledPinDataTooltip = false;
this.pinDataDiscoveryTooltipVisible = false;
dataPinningEventBus.emit('data-pinning-discovery', { isTooltipVisible: false });
}
},
pinDataDiscoveryComplete() {
useStorage(LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG).value = 'true';
useStorage(LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG).value = 'true';
},
enterEditMode({ origin }: EnterEditModeArgs) {
const inputData = this.pinnedData.data.value
? clearJsonKey(this.pinnedData.data.value)
: executionDataToJson(this.rawInputData);
const inputDataLength = Array.isArray(inputData)
? inputData.length
: Object.keys(inputData ?? {}).length;
const data = inputDataLength > 0 ? inputData : TEST_PIN_DATA;
this.ndvStore.setOutputPanelEditModeEnabled(true);
this.ndvStore.setOutputPanelEditModeValue(JSON.stringify(data, null, 2));
this.$telemetry.track('User opened ndv edit state', {
node_type: this.activeNode?.type,
click_type: origin === 'editIconButton' ? 'button' : 'link',
push_ref: this.pushRef,
run_index: this.runIndex,
is_output_present: this.hasNodeRun || this.pinnedData.hasData.value,
view: !this.hasNodeRun && !this.pinnedData.hasData.value ? 'undefined' : this.displayMode,
is_data_pinned: this.pinnedData.hasData.value,
});
},
onClickCancelEdit() {
this.ndvStore.setOutputPanelEditModeEnabled(false);
this.ndvStore.setOutputPanelEditModeValue('');
this.onExitEditMode({ type: 'cancel' });
},
onClickSaveEdit() {
if (!this.node) {
return;
}
const { value } = this.editMode;
this.clearAllStickyNotifications();
try {
const clearedValue = clearJsonKey(value) as INodeExecutionData[];
try {
this.pinnedData.setData(clearedValue, 'save-edit');
} catch (error) {
// setData function already shows toasts on error, so just return here
return;
}
} catch (error) {
this.showError(error, this.$locale.baseText('ndv.pinData.error.syntaxError.title'));
return;
}
this.ndvStore.setOutputPanelEditModeEnabled(false);
this.onExitEditMode({ type: 'save' });
},
onExitEditMode({ type }: { type: 'save' | 'cancel' }) {
this.$telemetry.track('User closed ndv edit state', {
node_type: this.activeNode?.type,
push_ref: this.pushRef,
run_index: this.runIndex,
view: this.displayMode,
type,
});
},
async onTogglePinData({ source }: { source: PinDataSource | UnpinDataSource }) {
if (!this.node) {
return;
}
if (source === 'pin-icon-click') {
const telemetryPayload = {
node_type: this.activeNode?.type,
push_ref: this.pushRef,
run_index: this.runIndex,
view: !this.hasNodeRun && !this.pinnedData.hasData.value ? 'none' : this.displayMode,
};
void this.externalHooks.run('runData.onTogglePinData', telemetryPayload);
this.$telemetry.track('User clicked pin data icon', telemetryPayload);
}
this.nodeHelpers.updateNodeParameterIssues(this.node);
if (this.pinnedData.hasData.value) {
this.pinnedData.unsetData(source);
return;
}
try {
this.pinnedData.setData(this.rawInputData, 'pin-icon-click');
} catch (error) {
console.error(error);
return;
}
if (this.maxRunIndex > 0) {
this.showToast({
title: this.$locale.baseText('ndv.pinData.pin.multipleRuns.title', {
interpolate: {
index: `${this.runIndex}`,
},
}),
message: this.$locale.baseText('ndv.pinData.pin.multipleRuns.description'),
type: 'success',
duration: 2000,
});
}
this.hidePinDataDiscoveryTooltip();
this.pinDataDiscoveryComplete();
},
switchToBinary() {
this.onDisplayModeChange('binary');
},
onBranchChange(value: number) {
this.outputIndex = value;
this.$telemetry.track('User changed ndv branch', {
push_ref: this.pushRef,
branch_index: value,
node_type: this.activeNode?.type,
node_type_input_selection: this.nodeType ? this.nodeType.name : '',
pane: this.paneType,
});
},
showTooMuchData() {
this.showData = true;
this.userEnabledShowData = true;
this.$telemetry.track('User clicked ndv button', {
node_type: this.activeNode?.type,
workflow_id: this.workflowsStore.workflowId,
push_ref: this.pushRef,
pane: this.paneType,
type: 'showTooMuchData',
});
},
toggleLinkRuns() {
this.linkedRuns ? this.unlinkRun() : this.linkRun();
},
linkRun() {
this.$emit('linkRun');
},
unlinkRun() {
this.$emit('unlinkRun');
},
onCurrentPageChange(value: number) {
this.currentPage = value;
this.$telemetry.track('User changed ndv page', {
node_type: this.activeNode?.type,
workflow_id: this.workflowsStore.workflowId,
push_ref: this.pushRef,
pane: this.paneType,
page_selected: this.currentPage,
page_size: this.pageSize,
items_total: this.dataCount,
});
},
resetCurrentPageIfTooFar() {
const maxPage = Math.ceil(this.dataCount / this.pageSize);
if (maxPage < this.currentPage) {
this.currentPage = maxPage;
}
},
onPageSizeChange(pageSize: number) {
this.pageSize = pageSize;
this.resetCurrentPageIfTooFar();
this.$telemetry.track('User changed ndv page size', {
node_type: this.activeNode?.type,
workflow_id: this.workflowsStore.workflowId,
push_ref: this.pushRef,
pane: this.paneType,
page_selected: this.currentPage,
page_size: this.pageSize,
items_total: this.dataCount,
});
},
onDisplayModeChange(displayMode: IRunDataDisplayMode) {
const previous = this.displayMode;
this.ndvStore.setPanelDisplayMode({ pane: this.paneType, mode: displayMode });
if (!this.userEnabledShowData) this.updateShowData();
const dataContainerRef = this.$refs.dataContainer as Element | undefined;
if (dataContainerRef) {
const dataDisplay = dataContainerRef.children[0];
if (dataDisplay) {
dataDisplay.scrollTo(0, 0);
}
}
this.closeBinaryDataDisplay();
void this.externalHooks.run('runData.displayModeChanged', {
newValue: displayMode,
oldValue: previous,
});
if (this.activeNode) {
this.$telemetry.track('User changed ndv item view', {
previous_view: previous,
new_view: displayMode,
node_type: this.activeNode.type,
workflow_id: this.workflowsStore.workflowId,
push_ref: this.pushRef,
pane: this.paneType,
});
}
},
getRunLabel(option: number) {
let itemsCount = 0;
for (let i = 0; i <= this.maxOutputIndex; i++) {
itemsCount += this.getPinDataOrLiveData(this.getRawInputData(option - 1, i)).length;
}
const items = this.$locale.baseText('ndv.output.items', {
adjustToNumber: itemsCount,
interpolate: { count: itemsCount },
});
const itemsLabel = itemsCount > 0 ? ` (${items})` : '';
return option + this.$locale.baseText('ndv.output.of') + (this.maxRunIndex + 1) + itemsLabel;
},
getRawInputData(
runIndex: number,
outputIndex: number,
connectionType: ConnectionTypes = NodeConnectionType.Main,
): INodeExecutionData[] {
let inputData: INodeExecutionData[] = [];
if (this.node) {
inputData = this.nodeHelpers.getNodeInputData(
this.node,
runIndex,
outputIndex,
this.paneType,
connectionType,
);
}
if (inputData.length === 0 || !Array.isArray(inputData)) {
return [];
}
return inputData;
},
getPinDataOrLiveData(inputData: INodeExecutionData[]): INodeExecutionData[] {
if (this.pinnedData.data.value && !this.isProductionExecutionPreview) {
return Array.isArray(this.pinnedData.data.value)
? this.pinnedData.data.value.map((value) => ({
json: value,
}))
: [
{
json: this.pinnedData.data.value,
},
];
}
return inputData;
},
getFilteredData(inputData: INodeExecutionData[]): INodeExecutionData[] {
if (!this.search || this.isSchemaView) {
return inputData;
}
this.currentPage = 1;
return inputData.filter(({ json }) => searchInObject(json, this.search));
},
getDataCount(
runIndex: number,
outputIndex: number,
connectionType: ConnectionTypes = NodeConnectionType.Main,
) {
if (!this.node) {
return 0;
}
if (this.workflowRunData?.[this.node.name]?.[runIndex]?.hasOwnProperty('error')) {
return 1;
}
const rawInputData = this.getRawInputData(runIndex, outputIndex, connectionType);
const pinOrLiveData = this.getPinDataOrLiveData(rawInputData);
return this.getFilteredData(pinOrLiveData).length;
},
init() {
// Reset the selected output index every time another node gets selected
this.outputIndex = 0;
this.refreshDataSize();
this.closeBinaryDataDisplay();
let outputTypes: ConnectionTypes[] = [];
if (this.nodeType !== null && this.node !== null) {
const outputs = this.getResolvedNodeOutputs();
outputTypes = NodeHelpers.getConnectionTypes(outputs);
}
this.connectionType = outputTypes.length === 0 ? NodeConnectionType.Main : outputTypes[0];
if (this.binaryData.length > 0) {
this.ndvStore.setPanelDisplayMode({
pane: this.paneType as 'input' | 'output',
mode: 'binary',
});
} else if (this.displayMode === 'binary') {
this.ndvStore.setPanelDisplayMode({
pane: this.paneType as 'input' | 'output',
mode: 'table',
});
}
},
closeBinaryDataDisplay() {
this.binaryDataDisplayVisible = false;
this.binaryDataDisplayData = null;
},
clearExecutionData() {
this.workflowsStore.setWorkflowExecutionData(null);
this.nodeHelpers.updateNodesExecutionIssues();
},
isViewable(index: number, key: string | number): boolean {
const { fileType } = this.binaryData[index][key];
return (
!!fileType && ['image', 'audio', 'video', 'text', 'json', 'pdf', 'html'].includes(fileType)
);
},
isDownloadable(index: number, key: string | number): boolean {
const { mimeType, fileName } = this.binaryData[index][key];
return !!(mimeType && fileName);
},
async downloadBinaryData(index: number, key: string | number) {
const { id, data, fileName, fileExtension, mimeType } = this.binaryData[index][key];
if (id) {
const url = this.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 downloadJsonData() {
const fileName = (this.node?.name ?? '').replace(/[^\w\d]/g, '_');
const blob = new Blob([JSON.stringify(this.rawInputData, null, 2)], {
type: 'application/json',
});
saveAs(blob, `${fileName}.json`);
},
displayBinaryData(index: number, key: string | number) {
const { data, mimeType } = this.binaryData[index][key];
this.binaryDataDisplayVisible = true;
this.binaryDataDisplayData = {
node: this.node?.name,
runIndex: this.runIndex,
outputIndex: this.currentOutputIndex,
index,
key,
data,
mimeType,
};
},
getOutputName(outputIndex: number) {
if (this.node === null) {
return outputIndex + 1;
}
const nodeType = this.nodeType;
const outputs = this.getResolvedNodeOutputs();
const outputConfiguration = outputs?.[outputIndex] as INodeOutputConfiguration;
if (outputConfiguration && isObject(outputConfiguration)) {
return outputConfiguration?.displayName;
}
if (!nodeType?.outputNames || nodeType.outputNames.length <= outputIndex) {
return outputIndex + 1;
}
return nodeType.outputNames[outputIndex];
},
refreshDataSize() {
// Hide by default the data from being displayed
this.showData = false;
const jsonItems = this.inputDataPage.map((item) => item.json);
const byteSize = new Blob([JSON.stringify(jsonItems)]).size;
this.dataSize = byteSize;
this.updateShowData();
},
updateShowData() {
// Display data if it is reasonably small (< 1MB)
this.showData =
(this.isSchemaView && this.dataSize < this.MAX_DISPLAY_DATA_SIZE_SCHEMA_VIEW) ||
this.dataSize < this.MAX_DISPLAY_DATA_SIZE;
},
onRunIndexChange(run: number) {
this.$emit('runChange', run);
},
enableNode() {
if (this.node) {
const updateInformation = {
name: this.node.name,
properties: {
disabled: !this.node.disabled,
} as IDataObject,
} as INodeUpdatePropertiesInformation;
this.workflowsStore.updateNodeProperties(updateInformation);
}
},
setDisplayMode() {
if (!this.activeNode) return;
const shouldDisplayHtml =
this.activeNode.type === HTML_NODE_TYPE &&
this.activeNode.parameters.operation === 'generateHtmlTemplate';
if (shouldDisplayHtml) {
this.ndvStore.setPanelDisplayMode({
pane: 'output',
mode: 'html',
});
}
},
activatePane() {
this.$emit('activatePane');
},
onSearchClear() {
this.search = '';
document.dispatchEvent(new KeyboardEvent('keyup', { key: '/' }));
},
},
});
</script>
<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));
> *: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;
}
.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);
.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 {
padding-left: var(--spacing-s);
padding-right: var(--spacing-s);
padding-bottom: var(--spacing-s);
display: flex;
gap: var(--spacing-4xs);
align-items: center;
:global(.el-input--suffix .el-input__inner) {
padding-right: var(--spacing-l);
}
}
.search {
margin-left: auto;
}
.runSelectorInner {
max-width: 172px;
}
.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);
}
</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>