mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
fix(editor): Fix layout of binary data preview in the log view (#17584)
This commit is contained in:
@@ -95,11 +95,11 @@ function closeWindow() {
|
||||
<style lang="scss">
|
||||
.binary-data-window {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
height: 100%;
|
||||
background-color: var(--color-run-data-background);
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
@@ -111,7 +111,7 @@ function closeWindow() {
|
||||
.binary-data-window-wrapper {
|
||||
margin-top: 0.5em;
|
||||
padding: 0 1em;
|
||||
height: calc(100% - 50px);
|
||||
height: 100%;
|
||||
|
||||
.el-row,
|
||||
.el-col {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { ViewableMimeTypes } from '@n8n/api-types';
|
||||
import { useStorage } from '@/composables/useStorage';
|
||||
import { saveAs } from 'file-saver';
|
||||
import NodeSettingsHint from '@/components/NodeSettingsHint.vue';
|
||||
@@ -99,6 +98,7 @@ import RunDataPaginationBar from '@/components/RunDataPaginationBar.vue';
|
||||
import { parseAiContent } from '@/utils/aiUtils';
|
||||
import { usePostHog } from '@/stores/posthog.store';
|
||||
import { I18nT } from 'vue-i18n';
|
||||
import RunDataBinary from '@/components/RunDataBinary.vue';
|
||||
|
||||
const LazyRunDataTable = defineAsyncComponent(
|
||||
async () => await import('@/components/RunDataTable.vue'),
|
||||
@@ -220,7 +220,6 @@ 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);
|
||||
@@ -636,6 +635,10 @@ const hasParsedAiContent = computed(() =>
|
||||
parsedAiContent.value.some((prr) => prr.parsedContent?.parsed),
|
||||
);
|
||||
|
||||
const binaryDataDisplayVisible = computed(
|
||||
() => binaryDataDisplayData.value !== null && props.displayMode === 'binary',
|
||||
);
|
||||
|
||||
function setInputBranchIndex(value: number) {
|
||||
if (props.paneType === 'input') {
|
||||
outputIndex.value = value;
|
||||
@@ -1238,35 +1241,10 @@ function init() {
|
||||
}
|
||||
|
||||
function closeBinaryDataDisplay() {
|
||||
binaryDataDisplayVisible.value = false;
|
||||
binaryDataDisplayData.value = null;
|
||||
}
|
||||
|
||||
function isViewable(index: number, key: string | number): boolean {
|
||||
const { mimeType } = binaryData.value[index][key];
|
||||
return ViewableMimeTypes.includes(mimeType);
|
||||
}
|
||||
|
||||
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() {
|
||||
function downloadJsonData() {
|
||||
const fileName = (node.value?.name ?? '').replace(/[^\w\d]/g, '_');
|
||||
const blob = new Blob([JSON.stringify(rawInputData.value, null, 2)], {
|
||||
type: 'application/json',
|
||||
@@ -1277,7 +1255,6 @@ async function downloadJsonData() {
|
||||
|
||||
function displayBinaryData(index: number, key: string | number) {
|
||||
const { data, mimeType } = binaryData.value[index][key];
|
||||
binaryDataDisplayVisible.value = true;
|
||||
|
||||
binaryDataDisplayData.value = {
|
||||
node: node.value?.name,
|
||||
@@ -1413,13 +1390,6 @@ defineExpose({ enterEditMode });
|
||||
</template>
|
||||
</N8nCallout>
|
||||
|
||||
<BinaryDataDisplay
|
||||
v-if="binaryDataDisplayData"
|
||||
:window-visible="binaryDataDisplayVisible"
|
||||
:display-data="binaryDataDisplayData"
|
||||
@close="closeBinaryDataDisplay"
|
||||
/>
|
||||
|
||||
<div :class="$style.header">
|
||||
<div :class="$style.title">
|
||||
<slot name="header"></slot>
|
||||
@@ -1513,126 +1483,135 @@ defineExpose({ enterEditMode });
|
||||
<RunDataItemCount v-if="props.compact" v-bind="itemsCountProps" />
|
||||
</div>
|
||||
|
||||
<div v-if="inputSelectLocation === 'header'" :class="$style.inputSelect">
|
||||
<slot name="input-select"></slot>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="maxRunIndex > 0 && !displaysMultipleNodes && !props.disableRunIndexSelection"
|
||||
v-show="!editMode.enabled"
|
||||
:class="$style.runSelector"
|
||||
>
|
||||
<div :class="$style.runSelectorInner">
|
||||
<slot v-if="inputSelectLocation === 'runs'" name="input-select"></slot>
|
||||
|
||||
<N8nSelect
|
||||
:model-value="runIndex"
|
||||
:class="$style.runSelectorSelect"
|
||||
size="small"
|
||||
teleported
|
||||
data-test-id="run-selector"
|
||||
@update:model-value="onRunIndexChange"
|
||||
@click.stop
|
||||
>
|
||||
<template #prepend>{{ i18n.baseText('ndv.output.run') }}</template>
|
||||
<N8nOption
|
||||
v-for="option in 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', linkedRuns ? 'linked' : '']"
|
||||
text
|
||||
type="tertiary"
|
||||
size="small"
|
||||
data-test-id="link-run"
|
||||
@click="toggleLinkRuns"
|
||||
/>
|
||||
</N8nTooltip>
|
||||
|
||||
<slot name="run-info"></slot>
|
||||
<div v-show="!binaryDataDisplayVisible">
|
||||
<div v-if="inputSelectLocation === 'header'" :class="$style.inputSelect">
|
||||
<slot name="input-select"></slot>
|
||||
</div>
|
||||
<ViewSubExecution
|
||||
v-if="activeTaskMetadata && !(paneType === 'input' && hasInputOverwrite)"
|
||||
:task-metadata="activeTaskMetadata"
|
||||
:display-mode="displayMode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<slot v-if="!displaysMultipleNodes" name="before-data" />
|
||||
<div
|
||||
v-if="maxRunIndex > 0 && !displaysMultipleNodes && !props.disableRunIndexSelection"
|
||||
v-show="!editMode.enabled"
|
||||
:class="$style.runSelector"
|
||||
>
|
||||
<div :class="$style.runSelectorInner">
|
||||
<slot v-if="inputSelectLocation === 'runs'" name="input-select"></slot>
|
||||
|
||||
<div v-if="props.calloutMessage || $slots['callout-message']" :class="$style.hintCallout">
|
||||
<N8nCallout theme="info" data-test-id="run-data-callout">
|
||||
<slot name="callout-message">
|
||||
<N8nText v-n8n-html="props.calloutMessage" size="small"></N8nText>
|
||||
</slot>
|
||||
<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', linkedRuns ? 'linked' : '']"
|
||||
text
|
||||
type="tertiary"
|
||||
size="small"
|
||||
data-test-id="link-run"
|
||||
@click="toggleLinkRuns"
|
||||
/>
|
||||
</N8nTooltip>
|
||||
|
||||
<slot name="run-info"></slot>
|
||||
</div>
|
||||
<ViewSubExecution
|
||||
v-if="activeTaskMetadata && !(paneType === 'input' && hasInputOverwrite)"
|
||||
:task-metadata="activeTaskMetadata"
|
||||
:display-mode="displayMode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<slot v-if="!displaysMultipleNodes" name="before-data" />
|
||||
|
||||
<div v-if="props.calloutMessage || $slots['callout-message']" :class="$style.hintCallout">
|
||||
<N8nCallout theme="info" data-test-id="run-data-callout">
|
||||
<slot name="callout-message">
|
||||
<N8nText v-n8n-html="props.calloutMessage" size="small"></N8nText>
|
||||
</slot>
|
||||
</N8nCallout>
|
||||
</div>
|
||||
<NodeSettingsHint v-if="props.paneType === 'output'" :node="node" />
|
||||
<N8nCallout
|
||||
v-for="hint in getNodeHints()"
|
||||
:key="hint.message"
|
||||
:class="$style.hintCallout"
|
||||
:theme="hint.type || 'info'"
|
||||
data-test-id="node-hint"
|
||||
>
|
||||
<N8nText v-n8n-html="hint.message" size="small"></N8nText>
|
||||
</N8nCallout>
|
||||
</div>
|
||||
<NodeSettingsHint v-if="props.paneType === 'output'" :node="node" />
|
||||
<N8nCallout
|
||||
v-for="hint in getNodeHints()"
|
||||
:key="hint.message"
|
||||
:class="$style.hintCallout"
|
||||
:theme="hint.type || 'info'"
|
||||
data-test-id="node-hint"
|
||||
>
|
||||
<N8nText v-n8n-html="hint.message" size="small"></N8nText>
|
||||
</N8nCallout>
|
||||
|
||||
<div
|
||||
v-if="maxOutputIndex > 0 && branches.length > 1 && !displaysMultipleNodes"
|
||||
:class="$style.outputs"
|
||||
data-test-id="branches"
|
||||
>
|
||||
<slot v-if="inputSelectLocation === 'outputs'" name="input-select"></slot>
|
||||
<ViewSubExecution
|
||||
v-if="activeTaskMetadata && !(paneType === 'input' && hasInputOverwrite)"
|
||||
:task-metadata="activeTaskMetadata"
|
||||
:display-mode="displayMode"
|
||||
/>
|
||||
<div
|
||||
v-if="maxOutputIndex > 0 && branches.length > 1 && !displaysMultipleNodes"
|
||||
:class="$style.outputs"
|
||||
data-test-id="branches"
|
||||
>
|
||||
<slot v-if="inputSelectLocation === 'outputs'" name="input-select"></slot>
|
||||
<ViewSubExecution
|
||||
v-if="activeTaskMetadata && !(paneType === 'input' && hasInputOverwrite)"
|
||||
:task-metadata="activeTaskMetadata"
|
||||
:display-mode="displayMode"
|
||||
/>
|
||||
|
||||
<div :class="$style.tabs">
|
||||
<N8nTabs
|
||||
size="small"
|
||||
:model-value="currentOutputIndex"
|
||||
:options="branches"
|
||||
@update:model-value="onBranchChange"
|
||||
<div :class="$style.tabs">
|
||||
<N8nTabs
|
||||
size="small"
|
||||
:model-value="currentOutputIndex"
|
||||
:options="branches"
|
||||
@update:model-value="onBranchChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="
|
||||
!props.compact &&
|
||||
hasNodeRun &&
|
||||
!isSearchInSchemaView &&
|
||||
((dataCount > 0 && maxRunIndex === 0) || search) &&
|
||||
!isArtificialRecoveredEventItem &&
|
||||
!displaysMultipleNodes
|
||||
"
|
||||
v-show="!editMode.enabled"
|
||||
:class="$style.itemsCount"
|
||||
data-test-id="ndv-items-count"
|
||||
>
|
||||
<slot v-if="inputSelectLocation === 'items'" name="input-select"></slot>
|
||||
|
||||
<RunDataItemCount v-bind="itemsCountProps" />
|
||||
<ViewSubExecution
|
||||
v-if="activeTaskMetadata && !(paneType === 'input' && hasInputOverwrite)"
|
||||
:task-metadata="activeTaskMetadata"
|
||||
:display-mode="displayMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="
|
||||
!props.compact &&
|
||||
hasNodeRun &&
|
||||
!isSearchInSchemaView &&
|
||||
((dataCount > 0 && maxRunIndex === 0) || search) &&
|
||||
!isArtificialRecoveredEventItem &&
|
||||
!displaysMultipleNodes
|
||||
"
|
||||
v-show="!editMode.enabled"
|
||||
:class="$style.itemsCount"
|
||||
data-test-id="ndv-items-count"
|
||||
>
|
||||
<slot v-if="inputSelectLocation === 'items'" name="input-select"></slot>
|
||||
|
||||
<RunDataItemCount v-bind="itemsCountProps" />
|
||||
<ViewSubExecution
|
||||
v-if="activeTaskMetadata && !(paneType === 'input' && hasInputOverwrite)"
|
||||
:task-metadata="activeTaskMetadata"
|
||||
:display-mode="displayMode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div ref="dataContainerRef" :class="$style.dataContainer" data-test-id="ndv-data-container">
|
||||
<BinaryDataDisplay
|
||||
v-if="binaryDataDisplayData"
|
||||
:window-visible="binaryDataDisplayVisible"
|
||||
:display-data="binaryDataDisplayData"
|
||||
@close="closeBinaryDataDisplay"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="isExecuting && !isWaitNodeWaiting"
|
||||
:class="[$style.center, $style.executingMessage]"
|
||||
@@ -1912,91 +1891,12 @@ defineExpose({ enterEditMode });
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<div v-else-if="displayMode === 'binary' && binaryData.length === 0" :class="$style.center">
|
||||
<N8nText align="center" tag="div">{{ i18n.baseText('runData.noBinaryDataFound') }}</N8nText>
|
||||
</div>
|
||||
<RunDataBinary
|
||||
v-else-if="displayMode === 'binary'"
|
||||
:binary-data="binaryData"
|
||||
@preview="displayBinaryData"
|
||||
/>
|
||||
|
||||
<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 v-else-if="!hasNodeRun" :class="$style.center">
|
||||
<slot name="node-not-run"></slot>
|
||||
</div>
|
||||
@@ -2187,67 +2087,6 @@ defineExpose({ enterEditMode });
|
||||
margin-left: 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(--ndv-spacing);
|
||||
margin-bottom: var(--ndv-spacing);
|
||||
border-radius: var(--border-radius-base);
|
||||
border: var(--border-base);
|
||||
padding: var(--ndv-spacing);
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
201
packages/frontend/editor-ui/src/components/RunDataBinary.vue
Normal file
201
packages/frontend/editor-ui/src/components/RunDataBinary.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
import { saveAs } from 'file-saver';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { ViewableMimeTypes } from '@n8n/api-types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import type { IBinaryKeyData } from 'n8n-workflow';
|
||||
import { N8nButton, N8nText } from '@n8n/design-system';
|
||||
|
||||
const { binaryData } = defineProps<{ binaryData: IBinaryKeyData[] }>();
|
||||
|
||||
const emit = defineEmits<{ preview: [index: number, key: string | number] }>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
function isViewable(index: number, key: string | number): boolean {
|
||||
const { mimeType } = binaryData[index][key];
|
||||
return ViewableMimeTypes.includes(mimeType);
|
||||
}
|
||||
|
||||
function isDownloadable(index: number, key: string | number): boolean {
|
||||
const { mimeType, fileName } = binaryData[index][key];
|
||||
return !!(mimeType && fileName);
|
||||
}
|
||||
|
||||
async function downloadBinaryData(index: number, key: string | number) {
|
||||
const { id, data, fileName, fileExtension, mimeType } = binaryData[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);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.component">
|
||||
<N8nText v-if="binaryData.length === 0" align="center" tag="div">
|
||||
{{ i18n.baseText('runData.noBinaryDataFound') }}
|
||||
</N8nText>
|
||||
<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="(data, 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="data.fileName">
|
||||
<div>
|
||||
<N8nText size="small" :bold="true"
|
||||
>{{ i18n.baseText('runData.fileName') }}:
|
||||
</N8nText>
|
||||
</div>
|
||||
<div :class="$style.binaryValue">{{ data.fileName }}</div>
|
||||
</div>
|
||||
<div v-if="data.directory">
|
||||
<div>
|
||||
<N8nText size="small" :bold="true"
|
||||
>{{ i18n.baseText('runData.directory') }}:
|
||||
</N8nText>
|
||||
</div>
|
||||
<div :class="$style.binaryValue">{{ data.directory }}</div>
|
||||
</div>
|
||||
<div v-if="data.fileExtension">
|
||||
<div>
|
||||
<N8nText size="small" :bold="true">
|
||||
{{ i18n.baseText('runData.fileExtension') }}:
|
||||
</N8nText>
|
||||
</div>
|
||||
<div :class="$style.binaryValue">{{ data.fileExtension }}</div>
|
||||
</div>
|
||||
<div v-if="data.mimeType">
|
||||
<div>
|
||||
<N8nText size="small" :bold="true">
|
||||
{{ i18n.baseText('runData.mimeType') }}:
|
||||
</N8nText>
|
||||
</div>
|
||||
<div :class="$style.binaryValue">{{ data.mimeType }}</div>
|
||||
</div>
|
||||
<div v-if="data.fileSize">
|
||||
<div>
|
||||
<N8nText size="small" :bold="true">
|
||||
{{ i18n.baseText('runData.fileSize') }}:
|
||||
</N8nText>
|
||||
</div>
|
||||
<div :class="$style.binaryValue">{{ data.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="emit('preview', 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>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.component {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0 var(--ndv-spacing) var(--spacing-3xl) var(--ndv-spacing);
|
||||
right: 0;
|
||||
overflow-y: auto;
|
||||
line-height: var(--font-line-height-xloose);
|
||||
word-break: normal;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.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(--ndv-spacing);
|
||||
margin-bottom: var(--ndv-spacing);
|
||||
border-radius: var(--border-radius-base);
|
||||
border: var(--border-base);
|
||||
padding: var(--ndv-spacing);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,6 @@ exports[`InputPanel > should render 1`] = `
|
||||
data-test-id="ndv-input-panel"
|
||||
data-v-2e5cd75c=""
|
||||
>
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="header"
|
||||
@@ -161,19 +160,24 @@ exports[`InputPanel > should render 1`] = `
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<div
|
||||
data-v-2e5cd75c=""
|
||||
>
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
|
||||
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div>
|
||||
<div
|
||||
class="dataContainer"
|
||||
data-test-id="ndv-data-container"
|
||||
data-v-2e5cd75c=""
|
||||
>
|
||||
<!--v-if-->
|
||||
<!---->
|
||||
</div>
|
||||
<!--v-if-->
|
||||
|
||||
@@ -75,6 +75,7 @@ function handleChangeDisplayMode(value: IRunDataDisplayMode) {
|
||||
v-if="runDataProps"
|
||||
v-bind="runDataProps"
|
||||
:key="`run-data${pipWindow ? '-pip' : ''}`"
|
||||
:class="$style.component"
|
||||
:workflow="logEntry.workflow"
|
||||
:workflow-execution="logEntry.execution"
|
||||
:too-much-data-title="locale.baseText('ndv.output.tooMuchData.title')"
|
||||
@@ -130,6 +131,10 @@ function handleChangeDisplayMode(value: IRunDataDisplayMode) {
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.component {
|
||||
--color-run-data-background: var(--color-background-light);
|
||||
}
|
||||
|
||||
.title {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
|
||||
Reference in New Issue
Block a user