mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-16 09:34:07 -08:00
05eec87d1d
* clean up dropdown * clean up focusoncreate * ⚡ Ignore mistaken ID in POST /workflows * ⚡ Fix undefined tag ID in PATCH /workflows * ⚡ Shorten response for POST /tags * remove scss mixins * clean up imports * ⚡ Implement validation with class-validator * address ivan's comments * implement modals * Fix lint issues * fix disabling shortcuts * fix focus issues * fix focus issues * fix focus issues with modal * fix linting issues * use dispatch * use constants for modal keys * fix focus * fix lint issues * remove unused prop * add modal root * fix lint issues * remove unused methods * fix shortcut * remove max width * ⚡ Fix duplicate entry error for pg and MySQL * update rename messaging * update order of buttons * fix firefox overflow on windows * fix dropdown height * 🔨 refactor tag crud controllers * 🧹 remove unused imports * use variable for number of items * fix dropdown spacing * ⚡ Restore type to fix build * ⚡ Fix post-refactor PATCH /workflows/:id * ⚡ Fix PATCH /workflows/:id for zero tags * ⚡ Fix usage count becoming stringified * address max's comments * fix filter spacing * fix blur bug * address most of ivan's comments * address tags type concern * remove defaults * ⚡ return tag id as string * 🔨 add hooks to tag CUD operations * 🏎 simplify timestamp pruning * remove blur event * fix onblur bug * ⚡ Fix fs import to fix build * address max's comments * implement responsive tag container * fix lint issues * Set default dates in entities * 👕 Fix lint in migrations * update tag limits * address ivan's comments * remove rename, refactor header, implement new designs for save, remove responsive tag container * update styling * update styling * implement responsive tag container * implement header tags edit * implement header tags edit * fix lint issues * implement expandable input * minor fixes * minor fixes * use variable * rename save as * duplicate fixes * ⚡ Implement unique workflow names * ⚡ Create /workflows/new endpoint * minor edit fixes * lint fixes * style fixes * hook up saving name * hook up tags * clean up impl * fix dirty state bug * update limit * update notification messages * on click outside * fix minor bug with count * lint fixes * ⚡ Add query string params to /workflows/new * handle minor edge cases * handle minor edge cases * handle minor bugs; fix firefox dropdown issue * Fix min width * apply tags only after api success * remove count fix * 🚧 Adjust to new qs requirements * clean up workflow tags impl, fix tags delete bug * fix minor issue * fix minor spacing issue * disable wrap for ops * fix viewport root; save on click in dropdown * save button loading when saving name/tags * implement max width on tags container * implement cleaner create experience * disable edit while updating * codacy hex color * refactor tags container * fix clickability * fix workflow open and count * clean up structure * fix up lint issues * ⚡ Create migrations for unique workflow names * fix button size * increase workflow name limit for larger screen * tslint fixes * disable responsiveness for workflow modal * rename event * change min width for tags * clean up pr * ⚡ Adjust quotes in MySQL migration * ⚡ Adjust quotes in Postgres migration * address max's comments on styles * remove success toasts * add hover mode to name * minor fixes * refactor name preview * fix name input not to jiggle * finish up name input * Fix up add tags * clean up param * clean up scss * fix resizing name * fix resizing name * fix resize bug * clean up edit spacing * ignore on esc * fix input bug * focus input on clear * build * fix up add tags clickablity * remove scrollbars * move into folders * clean up multiple patch req * remove padding top from edit * update tags on enter * build * rollout blur on enter behavior * rollout esc behavior * fix tags bug when duplicating tags * move key to reload tags * update header spacing * build * update hex case * refactor workflow title * remove unusued prop * keep focus on error, fix bug on error * Fix bug with name / tags toggle on error * impl creating new workflow name * ⚡ Refactor endpoint per new guidelines * support naming endpoint * ⚡ Refactor to support numeric suffixes * 👕 Lint migrations for unique workflow names * ⚡ Add migrations set default dates to indexes * fix connection push bug * ⚡ Lowercase default workflow name * ⚡ Add prefixes to set default dates migration * ⚡ Fix indentation on default dates migrations * ⚡ Add temp ts-ignore for unrelated change * ⚡ Adjust default dates migration for MySQL Remove change to data column in credentials_entity, already covered by Omar's migration. Also, fix quotes from table prefix addition. * ⚡ Adjust quotes in dates migration for PG * fix safari color bug * fix count bug * fix scroll bugs in dropdown * expand filter size * apply box-sizing to main header * update workflow names in executions to be wrapped by quotes * fix bug where key is same in dropdown * fix firefox bug * move up push connection session * 🔨 Remove mistakenly added nullable property * 🔥 Remove unneeded index drop-create (PG) * 🔥 Remove unneeded table copying * ⚡ Merge dates migration with tags migration * 🔨 Refactor endpoint and make wf name env * dropdown colors in firefox * update colors to use variables * update thumb color * change error message * remove 100 char maximum * fix bug with saving tags dropdowns multiple times * update error message when no name * ⚡ Update name missing toast message * ⚡ Update workflow already exists message * disable saving for executions * fix bug causing modal to close * make tags in workflow open clickable * increase workflow limit to 3 * remove success notifications * update header spacing * escape tag names * update tag and table colors * remove tags from export * build * clean up push connection dependencies * address ben's comments * revert tags optional interface * address comments * update duplicate message * build * fix eol * add one more eol * ⚡ Update comment * add hover style for workflow open, fix up font weight Co-authored-by: Mutasem <mutdmour@gmail.com> Co-authored-by: Iván Ovejero <ivov.src@gmail.com> Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>
844 lines
22 KiB
Vue
844 lines
22 KiB
Vue
<template>
|
|
<div class="run-data-view" v-loading="workflowRunning">
|
|
<BinaryDataDisplay :windowVisible="binaryDataDisplayVisible" :displayData="binaryDataDisplayData" @close="closeBinaryDataDisplay"/>
|
|
|
|
<el-button
|
|
v-if="node && !isReadOnly"
|
|
:disabled="workflowRunning"
|
|
@click.stop="runWorkflow(node.name, 'RunData.ExecuteNodeButton')"
|
|
class="execute-node-button"
|
|
:title="`Executes this ${node.name} node after executing any previous nodes that have not yet returned data`"
|
|
>
|
|
<div class="run-icon-button">
|
|
<font-awesome-icon v-if="!workflowRunning" icon="play-circle"/>
|
|
<font-awesome-icon v-else icon="spinner" spin />
|
|
</div>
|
|
|
|
Execute Node
|
|
</el-button>
|
|
|
|
<div class="header">
|
|
<div class="title-text">
|
|
<strong v-if="dataCount < maxDisplayItems">
|
|
Items: {{ dataCount }}
|
|
</strong>
|
|
<strong v-else>Items:
|
|
<el-select v-model="maxDisplayItems" @click.stop>
|
|
<el-option v-for="option in maxDisplayItemsOptions" :label="option" :value="option" :key="option" />
|
|
</el-select> /
|
|
{{ dataCount }}
|
|
</strong>
|
|
|
|
<el-popover
|
|
v-if="runMetadata"
|
|
placement="right"
|
|
width="400"
|
|
trigger="hover"
|
|
>
|
|
<strong>Start Time:</strong> {{runMetadata.startTime}}<br/>
|
|
<strong>Execution Time:</strong> {{runMetadata.executionTime}} ms
|
|
<font-awesome-icon icon="info-circle" class="primary-color" slot="reference" />
|
|
</el-popover>
|
|
<span v-if="maxOutputIndex > 0">
|
|
| Output:
|
|
<el-select v-model="outputIndex" @click.stop>
|
|
<el-option v-for="option in (maxOutputIndex + 1)" :label="getOutputName(option-1)" :value="option -1" :key="option">
|
|
</el-option>
|
|
</el-select>
|
|
</span>
|
|
<span v-if="maxRunIndex > 0">
|
|
| Data of Execution:
|
|
<el-select v-model="runIndex" @click.stop>
|
|
<el-option v-for="option in (maxRunIndex + 1)" :label="option + '/' + (maxRunIndex+1)" :value="option-1" :key="option">
|
|
</el-option>
|
|
</el-select>
|
|
|
|
</span>
|
|
</div>
|
|
<div v-if="node && workflowRunData !== null && workflowRunData.hasOwnProperty(node.name) && !workflowRunData[node.name][runIndex].error" class="title-data-display-selector" @click.stop>
|
|
<el-radio-group v-model="displayMode" size="mini">
|
|
<el-radio-button label="JSON" :disabled="showData === false"></el-radio-button>
|
|
<el-radio-button label="Table"></el-radio-button>
|
|
<el-radio-button label="Binary" v-if="binaryData.length !== 0"></el-radio-button>
|
|
</el-radio-group>
|
|
</div>
|
|
<div class="select-button" v-if="displayMode === 'JSON' && state.path !== deselectedPlaceholder">
|
|
<el-dropdown trigger="click" @command="handleCopyClick">
|
|
<span class="el-dropdown-link">
|
|
<el-button class="retry-button" circle type="text" size="small" title="Copy">
|
|
<font-awesome-icon icon="copy" />
|
|
</el-button>
|
|
</span>
|
|
<el-dropdown-menu slot="dropdown">
|
|
<el-dropdown-item :command="{command: 'itemPath'}">Copy Item Path</el-dropdown-item>
|
|
<el-dropdown-item :command="{command: 'parameterPath'}">Copy Parameter Path</el-dropdown-item>
|
|
<el-dropdown-item :command="{command: 'value'}">Copy Value</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</el-dropdown>
|
|
|
|
</div>
|
|
</div>
|
|
<div class="data-display-content">
|
|
<span v-if="node && workflowRunData !== null && workflowRunData.hasOwnProperty(node.name)">
|
|
<div v-if="workflowRunData[node.name][runIndex].error" class="error-display">
|
|
<NodeErrorView :error="workflowRunData[node.name][runIndex].error" />
|
|
</div>
|
|
<span v-else>
|
|
<div v-if="showData === false" class="to-much-data">
|
|
<h3>
|
|
Node returned a large amount of data
|
|
</h3>
|
|
|
|
<div class="text">
|
|
The node contains {{parseInt(dataSize/1024).toLocaleString()}} KB of data.<br />
|
|
Displaying it could cause problems!<br />
|
|
<br />
|
|
If you do decide to display it, avoid the JSON view!
|
|
</div>
|
|
|
|
<el-button size="small" @click="displayMode = 'Table';showData = true;">
|
|
<font-awesome-icon icon="eye"/>
|
|
Display Data Anyway
|
|
</el-button>
|
|
</div>
|
|
<div v-else-if="['JSON', 'Table'].includes(displayMode)">
|
|
<div v-if="jsonData.length === 0" class="no-data">
|
|
No text data found
|
|
</div>
|
|
<div v-else-if="displayMode === 'Table'">
|
|
<div v-if="tableData !== null && tableData.columns.length === 0" class="no-data">
|
|
Entries exist but they do not contain any JSON data.
|
|
</div>
|
|
<table v-else-if="tableData !== null">
|
|
<tr>
|
|
<th v-for="column in tableData.columns" :key="column">{{column}}</th>
|
|
</tr>
|
|
<tr v-for="(row, index1) in tableData.data" :key="index1">
|
|
<td v-for="(data, index2) in row" :key="index2">{{ [null, undefined].includes(data) ? ' ' : data }}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
<vue-json-pretty
|
|
v-else-if="displayMode === 'JSON'"
|
|
:data="jsonData"
|
|
:deep="10"
|
|
v-model="state.path"
|
|
:showLine="true"
|
|
:showLength="true"
|
|
selectableType="single"
|
|
path=""
|
|
:highlightSelectedNode="true"
|
|
:selectOnClickNode="true"
|
|
@click="dataItemClicked"
|
|
class="json-data"
|
|
/>
|
|
</div>
|
|
<div v-else-if="displayMode === 'Binary'">
|
|
<div v-if="binaryData.length === 0" class="no-data">
|
|
No binary data found
|
|
</div>
|
|
|
|
<div v-else>
|
|
<div v-for="(binaryDataEntry, index) in binaryData" :key="index">
|
|
<div class="binary-data-row-index">
|
|
<div class="binary-data-cell-index">
|
|
{{index + 1}}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="binary-data-row">
|
|
<div class="binary-data-cell" v-for="(binaryData, key) in binaryDataEntry" :key="index + '_' + key">
|
|
<div class="binary-data-information">
|
|
<div class="binary-data-cell-name">
|
|
{{key}}
|
|
</div>
|
|
<div v-if="binaryData.fileName">
|
|
<div class="label">File Name: </div>
|
|
<div class="value">{{binaryData.fileName}}</div>
|
|
</div>
|
|
<div v-if="binaryData.directory">
|
|
<div class="label">Directory: </div>
|
|
<div class="value">{{binaryData.directory}}</div>
|
|
</div>
|
|
<div v-if="binaryData.fileExtension">
|
|
<div class="label">File Extension:</div>
|
|
<div class="value">{{binaryData.fileExtension}}</div>
|
|
</div>
|
|
<div v-if="binaryData.mimeType">
|
|
<div class="label">Mime Type: </div>
|
|
<div class="value">{{binaryData.mimeType}}</div>
|
|
</div>
|
|
|
|
<!-- <el-button @click="displayBinaryData(binaryData)"> -->
|
|
<div class="binary-data-show-data-button-wrapper">
|
|
<el-button size="mini" class="binary-data-show-data-button" @click="displayBinaryData(index, key)">
|
|
Show Binary Data
|
|
</el-button>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</span>
|
|
</span>
|
|
<div v-else class="message">
|
|
<div>
|
|
<strong>No data</strong><br />
|
|
<br />
|
|
Data returned by this node will display here<br />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import Vue from 'vue';
|
|
//@ts-ignore
|
|
import VueJsonPretty from 'vue-json-pretty';
|
|
import {
|
|
GenericValue,
|
|
IBinaryData,
|
|
IBinaryKeyData,
|
|
IDataObject,
|
|
INodeExecutionData,
|
|
IRun,
|
|
IRunData,
|
|
IRunExecutionData,
|
|
ITaskData,
|
|
ITaskDataConnections,
|
|
} from 'n8n-workflow';
|
|
|
|
import {
|
|
IBinaryDisplayData,
|
|
IExecutionResponse,
|
|
INodeUi,
|
|
ITableData,
|
|
} from '@/Interface';
|
|
|
|
import {
|
|
MAX_DISPLAY_DATA_SIZE,
|
|
MAX_DISPLAY_ITEMS_AUTO_ALL,
|
|
} from '@/constants';
|
|
|
|
import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
|
|
import NodeErrorView from '@/components/Error/NodeViewError.vue';
|
|
|
|
import { copyPaste } from '@/components/mixins/copyPaste';
|
|
import { externalHooks } from "@/components/mixins/externalHooks";
|
|
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
|
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
|
import { workflowRun } from '@/components/mixins/workflowRun';
|
|
|
|
import mixins from 'vue-typed-mixins';
|
|
|
|
// A path that does not exist so that nothing is selected by default
|
|
const deselectedPlaceholder = '_!^&*';
|
|
|
|
export default mixins(
|
|
copyPaste,
|
|
externalHooks,
|
|
genericHelpers,
|
|
nodeHelpers,
|
|
workflowRun,
|
|
)
|
|
.extend({
|
|
name: 'RunData',
|
|
components: {
|
|
BinaryDataDisplay,
|
|
NodeErrorView,
|
|
VueJsonPretty,
|
|
},
|
|
data () {
|
|
return {
|
|
binaryDataPreviewActive: false,
|
|
dataSize: 0,
|
|
deselectedPlaceholder,
|
|
displayMode: 'Table',
|
|
state: {
|
|
value: '' as object | number | string,
|
|
path: deselectedPlaceholder,
|
|
},
|
|
runIndex: 0,
|
|
showData: false,
|
|
outputIndex: 0,
|
|
maxDisplayItems: 25 as number | null,
|
|
binaryDataDisplayVisible: false,
|
|
binaryDataDisplayData: null as IBinaryDisplayData | null,
|
|
|
|
MAX_DISPLAY_DATA_SIZE,
|
|
MAX_DISPLAY_ITEMS_AUTO_ALL,
|
|
};
|
|
},
|
|
computed: {
|
|
workflowRunning (): boolean {
|
|
return this.$store.getters.isActionActive('workflowRunning');
|
|
},
|
|
workflowExecution (): IExecutionResponse | null {
|
|
return this.$store.getters.getWorkflowExecution;
|
|
},
|
|
workflowRunData (): IRunData | null {
|
|
if (this.workflowExecution === null) {
|
|
return null;
|
|
}
|
|
const executionData: IRunExecutionData = this.workflowExecution.data;
|
|
return executionData.resultData.runData;
|
|
},
|
|
maxDisplayItemsOptions (): number[] {
|
|
const options = [25, 50, 100, 250, 500, 1000].filter(option => option <= this.dataCount);
|
|
if (!options.includes(this.dataCount)) {
|
|
options.push(this.dataCount);
|
|
}
|
|
return options;
|
|
},
|
|
node (): INodeUi | null {
|
|
return this.$store.getters.activeNode;
|
|
},
|
|
runMetadata () {
|
|
if (!this.node || this.workflowExecution === null) {
|
|
return null;
|
|
}
|
|
|
|
const runData = this.workflowRunData;
|
|
|
|
if (runData === null || !runData.hasOwnProperty(this.node.name)) {
|
|
return null;
|
|
}
|
|
|
|
if (runData[this.node.name].length <= this.runIndex) {
|
|
return null;
|
|
}
|
|
|
|
const taskData: ITaskData = runData[this.node.name][this.runIndex];
|
|
return {
|
|
executionTime: taskData.executionTime,
|
|
startTime: new Date(taskData.startTime).toLocaleString(),
|
|
};
|
|
},
|
|
dataCount (): 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 <= this.runIndex) {
|
|
return 0;
|
|
}
|
|
|
|
if (runData[this.node.name][this.runIndex].hasOwnProperty('error')) {
|
|
return 1;
|
|
}
|
|
|
|
if (!runData[this.node.name][this.runIndex].hasOwnProperty('data') ||
|
|
runData[this.node.name][this.runIndex].data === undefined
|
|
) {
|
|
return 0;
|
|
}
|
|
|
|
const inputData = this.getMainInputData(runData[this.node.name][this.runIndex].data!, this.outputIndex);
|
|
|
|
return inputData.length;
|
|
},
|
|
maxOutputIndex (): 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 < this.runIndex) {
|
|
return 0;
|
|
}
|
|
|
|
if (runData[this.node.name][this.runIndex].data === undefined ||
|
|
runData[this.node.name][this.runIndex].data!.main === undefined
|
|
) {
|
|
return 0;
|
|
}
|
|
|
|
return runData[this.node.name][this.runIndex].data!.main.length - 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;
|
|
},
|
|
jsonData (): IDataObject[] {
|
|
let inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex);
|
|
if (inputData.length === 0 || !Array.isArray(inputData)) {
|
|
return [];
|
|
}
|
|
|
|
if (this.maxDisplayItems !== null) {
|
|
inputData = inputData.slice(0, this.maxDisplayItems);
|
|
}
|
|
|
|
return this.convertToJson(inputData);
|
|
},
|
|
tableData (): ITableData | undefined {
|
|
let inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex);
|
|
if (inputData.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
if (this.maxDisplayItems !== null) {
|
|
inputData = inputData.slice(0,this.maxDisplayItems);
|
|
}
|
|
|
|
return this.convertToTable(inputData);
|
|
},
|
|
binaryData (): IBinaryKeyData[] {
|
|
if (this.node === null) {
|
|
return [];
|
|
}
|
|
|
|
return this.getBinaryData(this.workflowRunData, this.node.name, this.runIndex, this.outputIndex);
|
|
},
|
|
},
|
|
methods: {
|
|
closeBinaryDataDisplay () {
|
|
this.binaryDataDisplayVisible = false;
|
|
this.binaryDataDisplayData = null;
|
|
},
|
|
convertToJson (inputData: INodeExecutionData[]): IDataObject[] {
|
|
const returnData: IDataObject[] = [];
|
|
inputData.forEach((data) => {
|
|
if (!data.hasOwnProperty('json')) {
|
|
return;
|
|
}
|
|
returnData.push(data.json);
|
|
});
|
|
|
|
return returnData;
|
|
},
|
|
convertToTable (inputData: INodeExecutionData[]): ITableData | undefined {
|
|
const tableData: GenericValue[][] = [];
|
|
const tableColumns: string[] = [];
|
|
let leftEntryColumns: string[], entryRows: GenericValue[];
|
|
// Go over all entries
|
|
let entry: IDataObject;
|
|
inputData.forEach((data) => {
|
|
if (!data.hasOwnProperty('json')) {
|
|
return;
|
|
}
|
|
entry = data.json;
|
|
|
|
// Go over all keys of entry
|
|
entryRows = [];
|
|
leftEntryColumns = Object.keys(entry);
|
|
|
|
// Go over all the already existing column-keys
|
|
tableColumns.forEach((key) => {
|
|
if (entry.hasOwnProperty(key)) {
|
|
// Entry does have key so add its value
|
|
entryRows.push(entry[key]);
|
|
// Remove key so that we know that it got added
|
|
leftEntryColumns.splice(leftEntryColumns.indexOf(key), 1);
|
|
} else {
|
|
// Entry does not have key so add null
|
|
entryRows.push(null);
|
|
}
|
|
});
|
|
|
|
// Go over all the columns the entry has but did not exist yet
|
|
leftEntryColumns.forEach((key) => {
|
|
// Add the key for all runs in the future
|
|
tableColumns.push(key);
|
|
// Add the value
|
|
entryRows.push(entry[key]);
|
|
});
|
|
|
|
// Add the data of the entry
|
|
tableData.push(entryRows);
|
|
});
|
|
|
|
// Make sure that all entry-rows have the same length
|
|
tableData.forEach((entryRows) => {
|
|
if (tableColumns.length > entryRows.length) {
|
|
// Has to less entries so add the missing ones
|
|
entryRows.push.apply(entryRows, new Array(tableColumns.length - entryRows.length));
|
|
}
|
|
});
|
|
|
|
return {
|
|
columns: tableColumns,
|
|
data: tableData,
|
|
};
|
|
},
|
|
clearExecutionData () {
|
|
this.$store.commit('setWorkflowExecutionData', null);
|
|
this.updateNodesExecutionIssues();
|
|
},
|
|
dataItemClicked (path: string, data: object | number | string) {
|
|
this.state.value = data;
|
|
},
|
|
displayBinaryData (index: number, key: string) {
|
|
this.binaryDataDisplayVisible = true;
|
|
|
|
this.binaryDataDisplayData = {
|
|
node: this.node!.name,
|
|
runIndex: this.runIndex,
|
|
outputIndex: this.outputIndex,
|
|
index,
|
|
key,
|
|
};
|
|
},
|
|
getOutputName (outputIndex: number) {
|
|
if (this.node === null) {
|
|
return outputIndex + 1;
|
|
}
|
|
|
|
const nodeType = this.$store.getters.nodeType(this.node.type);
|
|
if (!nodeType.hasOwnProperty('outputNames') || nodeType.outputNames.length <= outputIndex) {
|
|
return outputIndex + 1;
|
|
}
|
|
|
|
return nodeType.outputNames[outputIndex];
|
|
},
|
|
convertPath (path: string): string {
|
|
// TODO: That can for sure be done fancier but for now it works
|
|
const placeholder = '*___~#^#~___*';
|
|
let inBrackets = path.match(/\[(.*?)\]/g);
|
|
|
|
if (inBrackets === null) {
|
|
inBrackets = [];
|
|
} else {
|
|
inBrackets = inBrackets.map(item => item.slice(1, -1)).map(item => {
|
|
if (item.startsWith('"') && item.endsWith('"')) {
|
|
return item.slice(1, -1);
|
|
}
|
|
return item;
|
|
});
|
|
}
|
|
const withoutBrackets = path.replace(/\[(.*?)\]/g, placeholder);
|
|
const pathParts = withoutBrackets.split('.');
|
|
const allParts = [] as string[];
|
|
pathParts.forEach(part => {
|
|
let index = part.indexOf(placeholder);
|
|
while(index !== -1) {
|
|
if (index === 0) {
|
|
allParts.push(inBrackets!.shift() as string);
|
|
part = part.substr(placeholder.length);
|
|
} else {
|
|
allParts.push(part.substr(0, index));
|
|
part = part.substr(index);
|
|
}
|
|
index = part.indexOf(placeholder);
|
|
}
|
|
if (part !== '') {
|
|
allParts.push(part);
|
|
}
|
|
});
|
|
|
|
return '["' + allParts.join('"]["') + '"]';
|
|
},
|
|
handleCopyClick (commandData: { command: string }) {
|
|
const newPath = this.convertPath(this.state.path);
|
|
|
|
let value: string;
|
|
if (commandData.command === 'value') {
|
|
if (typeof this.state.value === 'object') {
|
|
value = JSON.stringify(this.state.value, null, 2);
|
|
} else {
|
|
value = this.state.value.toString();
|
|
}
|
|
} else {
|
|
let startPath = '';
|
|
let path = '';
|
|
if (commandData.command === 'itemPath') {
|
|
const pathParts = newPath.split(']');
|
|
const index = pathParts[0].slice(1);
|
|
path = pathParts.slice(1).join(']');
|
|
startPath = `$item(${index}).$node["${this.node!.name}"].json`;
|
|
} else if (commandData.command === 'parameterPath') {
|
|
path = newPath.split(']').slice(1).join(']');
|
|
startPath = `$node["${this.node!.name}"].json`;
|
|
}
|
|
if (!path.startsWith('[') && !path.startsWith('.') && path) {
|
|
path += '.';
|
|
}
|
|
value = `{{ ${startPath + path} }}`;
|
|
}
|
|
|
|
this.copyToClipboard(value);
|
|
},
|
|
refreshDataSize () {
|
|
// Hide by default the data from being displayed
|
|
this.showData = false;
|
|
|
|
// Check how much data there is to display
|
|
const inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex);
|
|
|
|
const jsonItems = inputData.slice(0, this.maxDisplayItems || inputData.length).map(item => item.json);
|
|
|
|
this.dataSize = JSON.stringify(jsonItems).length;
|
|
|
|
if (this.dataSize < this.MAX_DISPLAY_DATA_SIZE) {
|
|
// Data is reasonable small (< 200kb) so display it directly
|
|
this.showData = true;
|
|
}
|
|
},
|
|
},
|
|
watch: {
|
|
node (newNode, oldNode) {
|
|
// Reset the selected output index every time another node gets selected
|
|
this.outputIndex = 0;
|
|
this.maxDisplayItems = 25;
|
|
this.refreshDataSize();
|
|
if (this.displayMode === 'Binary') {
|
|
this.closeBinaryDataDisplay();
|
|
if (this.binaryData.length === 0) {
|
|
this.displayMode = 'Table';
|
|
}
|
|
}
|
|
},
|
|
jsonData () {
|
|
this.refreshDataSize();
|
|
},
|
|
displayMode (newValue, oldValue) {
|
|
this.closeBinaryDataDisplay();
|
|
this.$externalHooks().run('runData.displayModeChanged', { newValue, oldValue });
|
|
},
|
|
maxRunIndex () {
|
|
this.runIndex = Math.min(this.runIndex, this.maxRunIndex);
|
|
},
|
|
},
|
|
mounted () {
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
|
|
.run-data-view {
|
|
position: relative;
|
|
bottom: 0;
|
|
left: 0;
|
|
margin-left: 350px;
|
|
width: calc(100% - 350px);
|
|
height: 100%;
|
|
z-index: 100;
|
|
color: #555;
|
|
font-size: 14px;
|
|
background-color: #f9f9f9;
|
|
|
|
.data-display-content {
|
|
position: absolute;
|
|
bottom: 0;
|
|
top: 50px;
|
|
left: 0;
|
|
right: 0;
|
|
overflow-y: auto;
|
|
|
|
.binary-data-row {
|
|
display: inline-flex;
|
|
padding: 0.5em 1em;
|
|
|
|
.binary-data-cell {
|
|
display: inline-block;
|
|
width: 300px;
|
|
overflow: hidden;
|
|
background-color: #fff;
|
|
margin-right: 1em;
|
|
border-radius: 3px;
|
|
-webkit-box-shadow: 0px 0px 12px 0px rgba(0,0,0,0.05);
|
|
-moz-box-shadow: 0px 0px 12px 0px rgba(0,0,0,0.05);
|
|
box-shadow: 0px 0px 12px 0px rgba(0,0,0,0.05);
|
|
|
|
.binary-data-information {
|
|
margin: 1em;
|
|
|
|
.binary-data-cell-name {
|
|
color: $--color-primary;
|
|
font-weight: 600;
|
|
font-size: 1.2em;
|
|
padding-bottom: 0.5em;
|
|
margin-bottom: 0.5em;
|
|
border-bottom: 1px solid #ccc;
|
|
}
|
|
|
|
.binary-data-show-data-button-wrapper {
|
|
margin-top: 1.5em;
|
|
text-align: center;
|
|
width: 100%;
|
|
|
|
.binary-data-show-data-button {
|
|
width: 130px;
|
|
}
|
|
}
|
|
|
|
.label {
|
|
padding-top: 0.5em;
|
|
font-weight: bold;
|
|
}
|
|
.value {
|
|
white-space: initial;
|
|
word-wrap: break-word;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.binary-data-row-index {
|
|
display: block;
|
|
padding: 1em 1em 0.25em 1em;
|
|
|
|
.binary-data-cell-index {
|
|
display: inline-block;
|
|
width: 30px;
|
|
height: 30px;
|
|
line-height: 30px;
|
|
border-radius: 5px;
|
|
text-align: center;
|
|
padding: 0 0.1em;
|
|
background-color: $--custom-header-background;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
}
|
|
}
|
|
|
|
.json-data {
|
|
&.vjs-tree {
|
|
color: $--custom-input-font;
|
|
}
|
|
}
|
|
|
|
.error-display,
|
|
.json-data,
|
|
.message,
|
|
.no-data {
|
|
margin: 1em;
|
|
}
|
|
|
|
.to-much-data {
|
|
margin: 1em;
|
|
text-align: center;
|
|
|
|
.text {
|
|
margin-bottom: 1em;
|
|
}
|
|
}
|
|
|
|
table {
|
|
border-collapse: collapse;
|
|
text-align: left;
|
|
width: calc(100% - 1px);
|
|
border-left: 25px solid #00000000;
|
|
border-right: 25px solid #00000000;
|
|
|
|
th {
|
|
background-color: $--custom-table-background-main;
|
|
color: #fff;
|
|
padding: 12px;
|
|
}
|
|
td {
|
|
padding: 12px;
|
|
}
|
|
tr:nth-child(even) {
|
|
background: #fff;;
|
|
}
|
|
tr:nth-child(odd) {
|
|
background: $--custom-table-background-stripe-color;
|
|
}
|
|
}
|
|
}
|
|
|
|
.execute-node-button {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
height: 30px;
|
|
width: 140px;
|
|
padding: 7px;
|
|
border-radius: 13px;
|
|
color: $--color-primary;
|
|
border: 1px solid $--color-primary;
|
|
background-color: #fff;
|
|
}
|
|
.execute-node-button:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.run-icon-button {
|
|
display: inline-block;
|
|
width: 20px;
|
|
}
|
|
|
|
.header {
|
|
padding-top: 10px;
|
|
padding-left: 10px;
|
|
|
|
.select-button {
|
|
height: 30px;
|
|
top: 50px;
|
|
right: 30px;
|
|
position: absolute;
|
|
text-align: right;
|
|
width: 200px;
|
|
z-index: 10;
|
|
}
|
|
|
|
.title-text {
|
|
display: inline-block;
|
|
line-height: 30px;
|
|
}
|
|
|
|
.title-data-display-selector {
|
|
position: absolute;
|
|
left: calc(50% - 105px);
|
|
width: 210px;
|
|
display: inline-block;
|
|
line-height: 30px;
|
|
text-align: center;
|
|
|
|
.entry.active {
|
|
font-weight: bold;
|
|
}
|
|
}
|
|
|
|
.el-select {
|
|
width: 80px;
|
|
z-index: 1;
|
|
|
|
.el-input__suffix-inner {
|
|
// TODO: Not sure why I have to do that. Invesigate when I have some time
|
|
position: absolute;
|
|
top: -5px;
|
|
right: 0;
|
|
}
|
|
|
|
input.el-input__inner {
|
|
border: 1px solid $--color-primary;
|
|
height: 25px;
|
|
line-height: 25px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
</style>
|