diff --git a/package.json b/package.json index 0b16382..604867f 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "react-collapse": "^5.1.0", "react-color": "^2.19.3", "react-contexify": "^5.0.0", + "react-copy-to-clipboard": "^5.0.3", "react-dnd": "^14.0.2", "react-dnd-html5-backend": "^14.0.0", "react-dom": "^17.0.2", @@ -49,6 +50,7 @@ "react-router-dom": "^5.2.0", "react-scripts": "^4.0.3", "react-svg-pan-zoom": "^3.10.0", + "react-syntax-highlighter": "^15.4.3", "react-trafficlight": "^5.2.1", "superagent": "^6.1.0", "swagger-ui-react": "^3.48.0", diff --git a/src/common/table.js b/src/common/table.js index 614a49f..81ba4a8 100644 --- a/src/common/table.js +++ b/src/common/table.js @@ -243,6 +243,20 @@ class CustomTable extends Component { />); } + if (child.props.pythonResultsButton) { + cell.push( + child.props.onPythonResults(index)} + variant={'table-control-button'} + />); + } + if (child.props.downloadAllButton) { cell.push( . + ******************************************************************************/ + +import React from 'react'; +import { Button } from 'react-bootstrap'; +import Icon from '../common/icon'; +import Dialog from '../common/dialogs/dialog'; +import {CopyToClipboard} from 'react-copy-to-clipboard'; +import SyntaxHighlighter from 'react-syntax-highlighter'; +import { github } from 'react-syntax-highlighter/dist/esm/styles/hljs'; + + +class ResultPythonDialog extends React.Component { + villasDataProcessingUrl = 'https://pypi.org/project/villas-dataprocessing/'; + + constructor(props) { + super(props); + + this.state = {}; + } + + componentDidUpdate(prevProps) { + if (this.props.resultId !== prevProps.resultId) { + const result = this.props.results[this.props.resultId]; + const output = this.getJupyterNotebook(result); + const blob = new Blob([JSON.stringify(output)], { + 'type': 'application/x-ipynb+json' + }); + const url = URL.createObjectURL(blob); + + this.setState({ fileDownloadUrl: url }) + } + } + + downloadJupyterNotebook() { + const result = this.props.results[this.props.resultId]; + const output = this.getJupyterNotebook(result); + const blob = new Blob([JSON.stringify(output)], { + 'type': 'application/x-ipynb+json' + }); + var url = window.URL.createObjectURL(blob); + + var a = document.createElement('a'); + a.style = 'display: none'; + a.href = url; + a.download = `villas_web_result_${result.id}.ipynb`; + document.body.appendChild(a); + + a.click(); + + setTimeout(function(){ + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, 100); + } + + getPythonDependencies() { + return `pip install villas-dataprocessing` + } + + getPythonSnippet(result) { + let token = localStorage.getItem('token'); + + let files = []; + let hasCSV = false; + for (let file of this.props.files) { + if (result.resultFileIDs.includes(file.id)) { + files.push(file); + if (file.type == 'text/csv') + hasCSV = true; + } + } + + let code = ''; + + if (hasCSV) + code += 'import pandas\n'; + + code += `from villas.web.result import Result + +r = Result(${result.id}, '${token}') + +# print(r) # result details +# print(r.files) # list of files of this result set + +# f = r.files[0] # first file +# f = r.get_files_by_type('text/csv')[0] # first CSV file +# f = r.get_file_by_name('result.csv') # by filename`; + + for (let file of files) { + console.log(file); + code += `\n\nf${file.id} = r.get_file_by_name('${file.name}')`; + + switch (file.type) { + case 'application/zip': + code += `\nwith f${file.id}.open_zip('testdata.csv') as f: + data = pandas.read_csv(f)`; + break; + + case 'text/csv': + code += `\nwith f${file.id}.open() as f: + data = pandas.read_csv(f)`; + break; + + default: + code += `\nwith f${file.id}.open() as f: + data = f.read()`; + break; + } + + code += ` + print(data)`; + } + + return code; + } + + /* Generate random cell ids + * + * See: https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html + */ + getCellId() { + return Math.round(1000000 * Math.random()).toString(); + } + + getJupyterNotebook(result) { + let ipynb_cells = []; + let cells = [ + this.getPythonDependencies(), + this.getPythonSnippet(result) + ] + + for (let cell of cells) { + ipynb_cells.push({ + cell_type: 'code', + execution_count: null, + id: this.getCellId(), + metadata: {}, + outputs: [], + source: cell.split('\n') + }) + } + + return { + cells: ipynb_cells, + metadata: { + kernelspec: { + display_name: 'Python 3', + language: 'python', + name: 'python3' + }, + language_info: { + codemirror_mode: { + name: 'ipython', + version: 3 + }, + file_extension: '.py', + mimetype: 'text/x-python', + name: 'python', + nbconvert_exporter: 'python', + pygments_lexer: 'ipython3', + version: '3.9.5' + } + }, + nbformat: 4, + nbformat_minor: 5 + } + } + + render() { + let result = this.props.results[this.props.resultId]; + + if (!result) + return null; + + let code = this.getPythonSnippet(result); + return ( + this.props.onClose()} + valid={true} + size='lg' + blendOutCancel={true} + > +

Use the following Python code-snippet to fetch and load your results as a Pandas dataframe.

+ +

1) Please install the villas-controller Python package:

+ + {this.getPythonDependencies()} + + +

2a) Insert the following snippet your Python code:

+ + {code} + + + + + +

2b) Or alternatively, download the following generated Jupyter notebook to get started:

+ +
+ ); + } +} + +export default ResultPythonDialog; diff --git a/src/result/result-table.js b/src/result/result-table.js index f50707c..9a69b8c 100644 --- a/src/result/result-table.js +++ b/src/result/result-table.js @@ -26,6 +26,7 @@ import TableColumn from "../common/table-column"; import DeleteDialog from "../common/dialogs/delete-dialog"; import EditResultDialog from "./edit-result"; import ResultConfigDialog from "./result-configs-dialog"; +import ResultPythonDialog from "./result-python-dialog"; import NewDialog from "../common/dialogs/new-dialog"; class ResultTable extends Component { @@ -34,6 +35,7 @@ class ResultTable extends Component { super(); this.state = { + pythonResultsModal: false, editResultsModal: false, modalResultsData: {}, modalResultsIndex: 0, @@ -95,6 +97,10 @@ class ResultTable extends Component { this.setState({ editResultsModal: false }); } + closePythonResultsModal() { + this.setState({ pythonResultsModal: false }); + } + downloadResultData(param) { let toDownload = []; let zip = false; @@ -218,9 +224,11 @@ class ResultTable extends Component { width={200} align='right' editButton + pythonResultsButton downloadAllButton deleteButton - onEdit={index => this.setState({ editResultsModal: true, modalResultsIndex: index })} + onPythonResults={(index) => this.setState({ pythonResultsModal: true, modalResultsIndex: index })} + onEdit={(index) => this.setState({ editResultsModal: true, modalResultsIndex: index })} onDownloadAll={(index) => this.downloadResultData(this.props.results[index])} onDelete={(index) => this.setState({ deleteResultsModal: true, modalResultsData: this.props.results[index], modalResultsIndex: index })} locked={this.props.locked} @@ -248,6 +256,13 @@ class ResultTable extends Component { resultNo={this.state.modalResultConfigsIndex} onClose={this.closeResultConfigSnapshots.bind(this)} /> +