mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +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">
|
<style lang="scss">
|
||||||
.binary-data-window {
|
.binary-data-window {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50px;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100% - 50px);
|
height: 100%;
|
||||||
background-color: var(--color-run-data-background);
|
background-color: var(--color-run-data-background);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -111,7 +111,7 @@ function closeWindow() {
|
|||||||
.binary-data-window-wrapper {
|
.binary-data-window-wrapper {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
height: calc(100% - 50px);
|
height: 100%;
|
||||||
|
|
||||||
.el-row,
|
.el-row,
|
||||||
.el-col {
|
.el-col {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ViewableMimeTypes } from '@n8n/api-types';
|
|
||||||
import { useStorage } from '@/composables/useStorage';
|
import { useStorage } from '@/composables/useStorage';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import NodeSettingsHint from '@/components/NodeSettingsHint.vue';
|
import NodeSettingsHint from '@/components/NodeSettingsHint.vue';
|
||||||
@@ -99,6 +98,7 @@ import RunDataPaginationBar from '@/components/RunDataPaginationBar.vue';
|
|||||||
import { parseAiContent } from '@/utils/aiUtils';
|
import { parseAiContent } from '@/utils/aiUtils';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
import { I18nT } from 'vue-i18n';
|
import { I18nT } from 'vue-i18n';
|
||||||
|
import RunDataBinary from '@/components/RunDataBinary.vue';
|
||||||
|
|
||||||
const LazyRunDataTable = defineAsyncComponent(
|
const LazyRunDataTable = defineAsyncComponent(
|
||||||
async () => await import('@/components/RunDataTable.vue'),
|
async () => await import('@/components/RunDataTable.vue'),
|
||||||
@@ -220,7 +220,6 @@ const dataSize = ref(0);
|
|||||||
const showData = ref(false);
|
const showData = ref(false);
|
||||||
const userEnabledShowData = ref(false);
|
const userEnabledShowData = ref(false);
|
||||||
const outputIndex = ref(0);
|
const outputIndex = ref(0);
|
||||||
const binaryDataDisplayVisible = ref(false);
|
|
||||||
const binaryDataDisplayData = ref<IBinaryData | null>(null);
|
const binaryDataDisplayData = ref<IBinaryData | null>(null);
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const pageSize = ref(10);
|
const pageSize = ref(10);
|
||||||
@@ -636,6 +635,10 @@ const hasParsedAiContent = computed(() =>
|
|||||||
parsedAiContent.value.some((prr) => prr.parsedContent?.parsed),
|
parsedAiContent.value.some((prr) => prr.parsedContent?.parsed),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const binaryDataDisplayVisible = computed(
|
||||||
|
() => binaryDataDisplayData.value !== null && props.displayMode === 'binary',
|
||||||
|
);
|
||||||
|
|
||||||
function setInputBranchIndex(value: number) {
|
function setInputBranchIndex(value: number) {
|
||||||
if (props.paneType === 'input') {
|
if (props.paneType === 'input') {
|
||||||
outputIndex.value = value;
|
outputIndex.value = value;
|
||||||
@@ -1238,35 +1241,10 @@ function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeBinaryDataDisplay() {
|
function closeBinaryDataDisplay() {
|
||||||
binaryDataDisplayVisible.value = false;
|
|
||||||
binaryDataDisplayData.value = null;
|
binaryDataDisplayData.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isViewable(index: number, key: string | number): boolean {
|
function downloadJsonData() {
|
||||||
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() {
|
|
||||||
const fileName = (node.value?.name ?? '').replace(/[^\w\d]/g, '_');
|
const fileName = (node.value?.name ?? '').replace(/[^\w\d]/g, '_');
|
||||||
const blob = new Blob([JSON.stringify(rawInputData.value, null, 2)], {
|
const blob = new Blob([JSON.stringify(rawInputData.value, null, 2)], {
|
||||||
type: 'application/json',
|
type: 'application/json',
|
||||||
@@ -1277,7 +1255,6 @@ async function downloadJsonData() {
|
|||||||
|
|
||||||
function displayBinaryData(index: number, key: string | number) {
|
function displayBinaryData(index: number, key: string | number) {
|
||||||
const { data, mimeType } = binaryData.value[index][key];
|
const { data, mimeType } = binaryData.value[index][key];
|
||||||
binaryDataDisplayVisible.value = true;
|
|
||||||
|
|
||||||
binaryDataDisplayData.value = {
|
binaryDataDisplayData.value = {
|
||||||
node: node.value?.name,
|
node: node.value?.name,
|
||||||
@@ -1413,13 +1390,6 @@ defineExpose({ enterEditMode });
|
|||||||
</template>
|
</template>
|
||||||
</N8nCallout>
|
</N8nCallout>
|
||||||
|
|
||||||
<BinaryDataDisplay
|
|
||||||
v-if="binaryDataDisplayData"
|
|
||||||
:window-visible="binaryDataDisplayVisible"
|
|
||||||
:display-data="binaryDataDisplayData"
|
|
||||||
@close="closeBinaryDataDisplay"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<div :class="$style.title">
|
<div :class="$style.title">
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
@@ -1513,126 +1483,135 @@ defineExpose({ enterEditMode });
|
|||||||
<RunDataItemCount v-if="props.compact" v-bind="itemsCountProps" />
|
<RunDataItemCount v-if="props.compact" v-bind="itemsCountProps" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="inputSelectLocation === 'header'" :class="$style.inputSelect">
|
<div v-show="!binaryDataDisplayVisible">
|
||||||
<slot name="input-select"></slot>
|
<div v-if="inputSelectLocation === 'header'" :class="$style.inputSelect">
|
||||||
</div>
|
<slot name="input-select"></slot>
|
||||||
|
|
||||||
<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>
|
</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">
|
<N8nSelect
|
||||||
<N8nCallout theme="info" data-test-id="run-data-callout">
|
:model-value="runIndex"
|
||||||
<slot name="callout-message">
|
:class="$style.runSelectorSelect"
|
||||||
<N8nText v-n8n-html="props.calloutMessage" size="small"></N8nText>
|
size="small"
|
||||||
</slot>
|
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>
|
</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
|
<div
|
||||||
v-if="maxOutputIndex > 0 && branches.length > 1 && !displaysMultipleNodes"
|
v-if="maxOutputIndex > 0 && branches.length > 1 && !displaysMultipleNodes"
|
||||||
:class="$style.outputs"
|
:class="$style.outputs"
|
||||||
data-test-id="branches"
|
data-test-id="branches"
|
||||||
>
|
>
|
||||||
<slot v-if="inputSelectLocation === 'outputs'" name="input-select"></slot>
|
<slot v-if="inputSelectLocation === 'outputs'" name="input-select"></slot>
|
||||||
<ViewSubExecution
|
<ViewSubExecution
|
||||||
v-if="activeTaskMetadata && !(paneType === 'input' && hasInputOverwrite)"
|
v-if="activeTaskMetadata && !(paneType === 'input' && hasInputOverwrite)"
|
||||||
:task-metadata="activeTaskMetadata"
|
:task-metadata="activeTaskMetadata"
|
||||||
:display-mode="displayMode"
|
:display-mode="displayMode"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div :class="$style.tabs">
|
<div :class="$style.tabs">
|
||||||
<N8nTabs
|
<N8nTabs
|
||||||
size="small"
|
size="small"
|
||||||
:model-value="currentOutputIndex"
|
:model-value="currentOutputIndex"
|
||||||
:options="branches"
|
:options="branches"
|
||||||
@update:model-value="onBranchChange"
|
@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>
|
</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">
|
<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
|
<div
|
||||||
v-if="isExecuting && !isWaitNodeWaiting"
|
v-if="isExecuting && !isWaitNodeWaiting"
|
||||||
:class="[$style.center, $style.executingMessage]"
|
:class="[$style.center, $style.executingMessage]"
|
||||||
@@ -1912,91 +1891,12 @@ defineExpose({ enterEditMode });
|
|||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
<div v-else-if="displayMode === 'binary' && binaryData.length === 0" :class="$style.center">
|
<RunDataBinary
|
||||||
<N8nText align="center" tag="div">{{ i18n.baseText('runData.noBinaryDataFound') }}</N8nText>
|
v-else-if="displayMode === 'binary'"
|
||||||
</div>
|
: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">
|
<div v-else-if="!hasNodeRun" :class="$style.center">
|
||||||
<slot name="node-not-run"></slot>
|
<slot name="node-not-run"></slot>
|
||||||
</div>
|
</div>
|
||||||
@@ -2187,67 +2087,6 @@ defineExpose({ enterEditMode });
|
|||||||
margin-left: auto;
|
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 {
|
.displayModes {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
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-test-id="ndv-input-panel"
|
||||||
data-v-2e5cd75c=""
|
data-v-2e5cd75c=""
|
||||||
>
|
>
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<div
|
<div
|
||||||
class="header"
|
class="header"
|
||||||
@@ -161,19 +160,24 @@ exports[`InputPanel > should render 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<div
|
||||||
<!--v-if-->
|
data-v-2e5cd75c=""
|
||||||
<!--v-if-->
|
>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="dataContainer"
|
class="dataContainer"
|
||||||
data-test-id="ndv-data-container"
|
data-test-id="ndv-data-container"
|
||||||
data-v-2e5cd75c=""
|
data-v-2e5cd75c=""
|
||||||
>
|
>
|
||||||
|
<!--v-if-->
|
||||||
<!---->
|
<!---->
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ function handleChangeDisplayMode(value: IRunDataDisplayMode) {
|
|||||||
v-if="runDataProps"
|
v-if="runDataProps"
|
||||||
v-bind="runDataProps"
|
v-bind="runDataProps"
|
||||||
:key="`run-data${pipWindow ? '-pip' : ''}`"
|
:key="`run-data${pipWindow ? '-pip' : ''}`"
|
||||||
|
:class="$style.component"
|
||||||
:workflow="logEntry.workflow"
|
:workflow="logEntry.workflow"
|
||||||
:workflow-execution="logEntry.execution"
|
:workflow-execution="logEntry.execution"
|
||||||
:too-much-data-title="locale.baseText('ndv.output.tooMuchData.title')"
|
:too-much-data-title="locale.baseText('ndv.output.tooMuchData.title')"
|
||||||
@@ -130,6 +131,10 @@ function handleChangeDisplayMode(value: IRunDataDisplayMode) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
.component {
|
||||||
|
--color-run-data-background: var(--color-background-light);
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 3px;
|
letter-spacing: 3px;
|
||||||
|
|||||||
Reference in New Issue
Block a user