Add functionality to easily copy data and path of output data (#1260)

*  Add functionality to easily copy data and path of output data

*  Fix issues with copied path

* 👕 Fix lint issue

* ;bug: Fix issue that some paths were wrong

*  Final improvements
This commit is contained in:
Jan 2020-12-18 18:55:53 +01:00 committed by GitHub
parent b5d4391ace
commit bfb344a23c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 142 additions and 27 deletions

View file

@ -79,7 +79,7 @@
"uuid": "^8.1.0", "uuid": "^8.1.0",
"vue": "^2.6.9", "vue": "^2.6.9",
"vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0", "vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0",
"vue-json-tree": "^0.4.1", "vue-json-pretty": "^1.7.1",
"vue-prism-editor": "^0.3.0", "vue-prism-editor": "^0.3.0",
"vue-router": "^3.0.6", "vue-router": "^3.0.6",
"vue-template-compiler": "^2.5.17", "vue-template-compiler": "^2.5.17",

View file

@ -62,6 +62,21 @@
<el-radio-button label="Binary" v-if="binaryData.length !== 0"></el-radio-button> <el-radio-button label="Binary" v-if="binaryData.length !== 0"></el-radio-button>
</el-radio-group> </el-radio-group>
</div> </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>
<div class="data-display-content"> <div class="data-display-content">
<span v-if="node && workflowRunData !== null && workflowRunData.hasOwnProperty(node.name)"> <span v-if="node && workflowRunData !== null && workflowRunData.hasOwnProperty(node.name)">
@ -104,10 +119,19 @@
</tr> </tr>
</table> </table>
</div> </div>
<json-tree <vue-json-pretty
v-else-if="displayMode === 'JSON'" v-else-if="displayMode === 'JSON'"
:data="jsonData" :data="jsonData"
:level="10" :deep="10"
v-model="state.path"
:showLine="true"
:showLength="true"
selectableType="single"
path=""
:highlightSelectedNode="true"
:selectOnClickNode="true"
:custom-value-formatter="customLinkFormatter"
@click="dataItemClicked"
class="json-data" class="json-data"
/> />
</div> </div>
@ -171,8 +195,8 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
// @ts-ignore //@ts-ignore
import JsonTree from 'vue-json-tree'; import VueJsonPretty from 'vue-json-pretty';
import { import {
GenericValue, GenericValue,
IBinaryData, IBinaryData,
@ -200,13 +224,18 @@ import {
import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue'; import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
import { copyPaste } from '@/components/mixins/copyPaste';
import { genericHelpers } from '@/components/mixins/genericHelpers'; import { genericHelpers } from '@/components/mixins/genericHelpers';
import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { workflowRun } from '@/components/mixins/workflowRun'; import { workflowRun } from '@/components/mixins/workflowRun';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
// A path that does not exist so that nothing is selected by default
const deselectedPlaceholder = '_!^&*';
export default mixins( export default mixins(
copyPaste,
genericHelpers, genericHelpers,
nodeHelpers, nodeHelpers,
workflowRun, workflowRun,
@ -215,13 +244,18 @@ export default mixins(
name: 'RunData', name: 'RunData',
components: { components: {
BinaryDataDisplay, BinaryDataDisplay,
JsonTree, VueJsonPretty,
}, },
data () { data () {
return { return {
binaryDataPreviewActive: false, binaryDataPreviewActive: false,
dataSize: 0, dataSize: 0,
deselectedPlaceholder,
displayMode: 'Table', displayMode: 'Table',
state: {
value: '' as object | number | string,
path: deselectedPlaceholder,
},
runIndex: 0, runIndex: 0,
showData: false, showData: false,
outputIndex: 0, outputIndex: 0,
@ -380,22 +414,17 @@ export default mixins(
}, },
}, },
methods: { methods: {
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];
},
closeBinaryDataDisplay () { closeBinaryDataDisplay () {
this.binaryDataDisplayVisible = false; this.binaryDataDisplayVisible = false;
this.binaryDataDisplayData = null; this.binaryDataDisplayData = null;
}, },
customLinkFormatter (data: object | number | string, key: string, parent: object, defaultFormatted: () => string) {
if (typeof data === 'string' && data.startsWith('http://')) {
return `<a style="color:red;" href="${data}" target="_blank">"${data}"</a>`;
} else {
return defaultFormatted;
}
},
convertToJson (inputData: INodeExecutionData[]): IDataObject[] { convertToJson (inputData: INodeExecutionData[]): IDataObject[] {
const returnData: IDataObject[] = []; const returnData: IDataObject[] = [];
inputData.forEach((data) => { inputData.forEach((data) => {
@ -465,7 +494,9 @@ export default mixins(
this.$store.commit('setWorkflowExecutionData', null); this.$store.commit('setWorkflowExecutionData', null);
this.updateNodesExecutionIssues(); this.updateNodesExecutionIssues();
}, },
// displayBinaryData (binaryData: IBinaryData) { dataItemClicked (path: string, data: object | number | string) {
this.state.value = data;
},
displayBinaryData (index: number, key: string) { displayBinaryData (index: number, key: string) {
this.binaryDataDisplayVisible = true; this.binaryDataDisplayVisible = true;
@ -477,6 +508,85 @@ export default mixins(
key, 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 () { refreshDataSize () {
// Hide by default the data from being displayed // Hide by default the data from being displayed
this.showData = false; this.showData = false;
@ -610,15 +720,8 @@ export default mixins(
} }
.json-data { .json-data {
.json-tree { &.vjs-tree {
color: $--custom-input-font; color: $--custom-input-font;
.json-tree-value-number {
color: #b03030;
}
.json-tree-value-string {
color: #8aab1a;
}
} }
} }
@ -694,6 +797,16 @@ export default mixins(
padding-top: 10px; padding-top: 10px;
padding-left: 10px; padding-left: 10px;
.select-button {
height: 30px;
top: 50px;
right: 30px;
position: absolute;
text-align: right;
width: 200px;
z-index: 10;
}
.title-text { .title-text {
display: inline-block; display: inline-block;
line-height: 30px; line-height: 30px;

View file

@ -5,6 +5,7 @@ import Vue from 'vue';
import 'prismjs'; import 'prismjs';
import 'prismjs/themes/prism.css'; import 'prismjs/themes/prism.css';
import 'vue-prism-editor/dist/VuePrismEditor.css'; import 'vue-prism-editor/dist/VuePrismEditor.css';
import 'vue-json-pretty/lib/styles.css';
import Vue2TouchEvents from 'vue2-touch-events'; import Vue2TouchEvents from 'vue2-touch-events';
import * as ElementUI from 'element-ui'; import * as ElementUI from 'element-ui';

View file

@ -9,6 +9,7 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"strict": true, "strict": true,
"jsx": "preserve", "jsx": "preserve",
"skipLibCheck": true,
"importHelpers": true, "importHelpers": true,
"moduleResolution": "node", "moduleResolution": "node",
"esModuleInterop": true, "esModuleInterop": true,