diff --git a/package.json b/package.json
index b666cf9b..ca505d2a 100644
--- a/package.json
+++ b/package.json
@@ -26,12 +26,12 @@
"gaugeJS": "^1.3.7",
"handlebars": "^4.7.7",
"isomorphic-form-data": "^2.0.0",
+ "json-edit-react": "^1.29.0",
"jsonwebtoken": "^8.5.1",
"jszip": "^3.9.1",
"lodash": "^4.17.21",
"moment": "^2.29.3",
"moment-duration-format": "^2.3.2",
- "multiselect-react-dropdown": "^2.0.21",
"prop-types": "^15.8.1",
"rc-slider": "^10.0.0",
"react": "^17.0.2",
@@ -39,12 +39,10 @@
"react-collapse": "^5.1.1",
"react-color": "^2.19.3",
"react-contexify": "^5.0.0",
- "react-copy-to-clipboard": "^5.1.0",
"react-datetime-picker": "^3.5.0",
"react-dnd": "^14.0.5",
"react-dnd-html5-backend": "^14.1.0",
"react-dom": "^17.0.2",
- "react-json-view": "^1.21.3",
"react-notification-system": "^0.4.0",
"react-redux": "^7.2.8",
"react-rnd": "^10.3.7",
diff --git a/src/common/multiselect.jsx b/src/common/multiselect.jsx
new file mode 100644
index 00000000..b3127f28
--- /dev/null
+++ b/src/common/multiselect.jsx
@@ -0,0 +1,79 @@
+/**
+ * This file is part of VILLASweb.
+ *
+ * VILLASweb is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * VILLASweb is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with VILLASweb. If not, see .
+ ******************************************************************************/
+
+import { useEffect, useState } from "react";
+import { Dropdown, Form } from "react-bootstrap";
+
+//a dropdown with a checklist
+//each item has to be an object and have parameters id and name
+const MultiselectDropdown = ({ items, checkedInitialyIDs, onUpdate }) => {
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ //push items that are to be checked upon component mount
+ useEffect(() => {
+ if (checkedInitialyIDs.length > 0) {
+ const initialItems = items.filter((item) =>
+ checkedInitialyIDs.includes(item.id)
+ );
+ setSelectedItems(initialItems);
+ }
+ }, [checkedInitialyIDs]);
+
+ const handleItemCheck = (item) => {
+ let updatedList;
+
+ if (selectedItems.some((i) => i.id === item.id)) {
+ updatedList = selectedItems.filter((i) => i.id !== item.id);
+ } else {
+ updatedList = [...selectedItems, item];
+ }
+
+ setSelectedItems(updatedList);
+ onUpdate(updatedList, item);
+ };
+
+ return (
+
+
+ {selectedItems.length > 0
+ ? selectedItems.map((i) => (
+ {i.name}
+ ))
+ : "Select file(s)..."}
+
+
+
+ {items.map((item) => (
+ i.id == item.id)}
+ onChange={() => handleItemCheck(item)}
+ style={{
+ marginLeft: "0.5em",
+ marginRight: "0.5em",
+ paddingTop: "1em",
+ }}
+ />
+ ))}
+
+
+ );
+};
+
+export default MultiselectDropdown;
diff --git a/src/common/parameters-editor.js b/src/common/parameters-editor.js
index aca6b052..872a5436 100644
--- a/src/common/parameters-editor.js
+++ b/src/common/parameters-editor.js
@@ -15,64 +15,53 @@
* along with VILLASweb. If not, see .
******************************************************************************/
-import React from 'react';
-import PropTypes from 'prop-types';
-import JsonView from 'react-json-view';
+import React from "react";
+import PropTypes from "prop-types";
+import { JsonEditor } from "json-edit-react";
class ParametersEditor extends React.Component {
- onAdd = event => {
- if (this.props.onChange != null) {
- this.props.onChange(JSON.parse(JSON.stringify(event.updated_src)));
- }
- }
+ handleJsonUpdate = ({ newData }) => {
+ this.props.onChange(JSON.parse(JSON.stringify(newData)));
+ };
- onEdit = event => {
- if (this.props.onChange != null) {
- this.props.onChange(JSON.parse(JSON.stringify(event.updated_src)));
- }
- }
+ render() {
+ const containerStyle = {
+ width: "100%",
+ minHeight: "100px",
+ padding: "5px",
+ border: "1px solid lightgray",
+ display: "flex",
+ };
- onDelete = event => {
- if (this.props.onChange != null) {
- this.props.onChange(JSON.parse(JSON.stringify(event.updated_src)));
- }
- }
-
- render() {
- const containerStyle = {
- minHeight: '100px',
-
- paddingTop: '5px',
- paddingBottom: '5px',
- paddingLeft: '8px',
-
- border: '1px solid lightgray'
- };
-
- return
-
-
;
- }
+ return (
+
+ );
+ }
}
ParametersEditor.propTypes = {
- content: PropTypes.object,
- onChange: PropTypes.func,
- disabled: PropTypes.bool
+ content: PropTypes.object,
+ onChange: PropTypes.func,
+ disabled: PropTypes.bool,
};
ParametersEditor.defaultProps = {
- content: {},
- disabled: false
+ content: {},
+ disabled: false,
};
export default ParametersEditor;
diff --git a/src/common/rawDataTable.js b/src/common/rawDataTable.js
index 87ff5bab..92ebcd18 100644
--- a/src/common/rawDataTable.js
+++ b/src/common/rawDataTable.js
@@ -1,23 +1,23 @@
import { isJSON } from "../utils/isJson";
-import ReactJson from "react-json-view";
+import { JsonEditor } from "json-edit-react";
const RawDataTable = (props) => {
- if(props.rawData !== null && isJSON(props.rawData)){
- return (
-
- )
- } else {
- return (
- No valid JSON raw data available.
- )
- }
-}
+ if (props.rawData !== null && isJSON(props.rawData)) {
+ return (
+
+ );
+ } else {
+ return No valid JSON raw data available.
;
+ }
+};
-export default RawDataTable;
\ No newline at end of file
+export default RawDataTable;
diff --git a/src/pages/infrastructure/ic-params-table.js b/src/pages/infrastructure/ic-params-table.js
index a134c5b2..50ff8532 100644
--- a/src/pages/infrastructure/ic-params-table.js
+++ b/src/pages/infrastructure/ic-params-table.js
@@ -1,74 +1,81 @@
-import { Table, } from 'react-bootstrap';
-import moment from 'moment';
-import { isJSON } from '../../utils/isJson';
-import ReactJson from 'react-json-view';
+import { Table } from "react-bootstrap";
+import moment from "moment";
+import { isJSON } from "../../utils/isJson";
+import { JsonEditor } from "json-edit-react";
const ICParamsTable = (props) => {
- const ic = props.ic;
+ const ic = props.ic;
- return(
-
-
- Property Value
-
-
-
- Name
- {ic.name}
-
-
- Description
- {ic.description}
-
-
- UUID {ic.uuid}
-
-
- State
- {ic.state}
-
-
- Category
- {ic.category}
-
-
- Type
- {ic.type}
-
-
- Uptime
- {moment.duration(ic.uptime, "seconds").humanize()}
-
-
- Location
- {ic.location}
-
-
- Websocket URL
- {ic.websocketurl}
-
-
- API URL
- {ic.apiurl}
-
-
- Start parameter schema
-
- {isJSON(ic.startparameterschema) ?
- : No Start parameter schema JSON available.
}
-
-
-
-
- )
-}
+ return (
+
+
+
+ Property
+ Value
+
+
+
+
+ Name
+ {ic.name}
+
+
+ Description
+ {ic.description}
+
+
+ UUID
+ {ic.uuid}
+
+
+ State
+ {ic.state}
+
+
+ Category
+ {ic.category}
+
+
+ Type
+ {ic.type}
+
+
+ Uptime
+ {moment.duration(ic.uptime, "seconds").humanize()}
+
+
+ Location
+ {ic.location}
+
+
+ Websocket URL
+ {ic.websocketurl}
+
+
+ API URL
+ {ic.apiurl}
+
+
+ Start parameter schema
+
+ {isJSON(ic.startparameterschema) ? (
+
+ ) : (
+ No Start parameter schema JSON available.
+ )}
+
+
+
+
+ );
+};
-export default ICParamsTable;
\ No newline at end of file
+export default ICParamsTable;
diff --git a/src/pages/scenarios/dialogs/edit-config.js b/src/pages/scenarios/dialogs/edit-config.js
index e2d73a6b..a4ba089f 100644
--- a/src/pages/scenarios/dialogs/edit-config.js
+++ b/src/pages/scenarios/dialogs/edit-config.js
@@ -15,14 +15,13 @@
* along with VILLASweb. If not, see .
******************************************************************************/
-import React from 'react';
+import React from "react";
import Form from "@rjsf/core";
-import { Form as BForm } from 'react-bootstrap';
-import { Multiselect } from 'multiselect-react-dropdown'
-import Dialog from '../../../common/dialogs/dialog';
-import ParametersEditor from '../../../common/parameters-editor';
-
+import { Form as BForm } from "react-bootstrap";
+import MultiselectDropdown from "../../../common/multiselect";
+import Dialog from "../../../common/dialogs/dialog";
+import ParametersEditor from "../../../common/parameters-editor";
class EditConfigDialog extends React.Component {
valid = false;
@@ -30,12 +29,12 @@ class EditConfigDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
- name: '',
- icID: '',
+ name: "",
+ icID: "",
startParameters: {},
formData: {},
startparamTemplate: null,
- selectedFiles: [] // list of selected files {name, id}, this is not the fileIDs list of the config!
+ selectedFiles: [], // list of selected files {name, id}, this is not the fileIDs list of the config!
};
}
@@ -43,52 +42,58 @@ class EditConfigDialog extends React.Component {
if (canceled === false) {
if (this.valid) {
let data = JSON.parse(JSON.stringify(this.props.config));
- if (this.state.name !== '' && this.props.config.name !== this.state.name) {
+ if (
+ this.state.name !== "" &&
+ this.props.config.name !== this.state.name
+ ) {
data.name = this.state.name;
}
- if (this.state.icID !== '' && this.props.config.icID !== parseInt(this.state.icID)) {
+ if (
+ this.state.icID !== "" &&
+ this.props.config.icID !== parseInt(this.state.icID)
+ ) {
data.icID = parseInt(this.state.icID, 10);
}
- if (Object.keys(this.state.startParameters).length === 0 && this.state.startParameters.constructor === Object &&
- JSON.stringify(this.props.config.startParameters) !== JSON.stringify(this.state.startParameters)) {
+ if (
+ Object.keys(this.state.startParameters).length !== 0 &&
+ this.state.startParameters.constructor === Object &&
+ JSON.stringify(this.props.config.startParameters) !==
+ JSON.stringify(this.state.startParameters)
+ ) {
data.startParameters = this.state.startParameters;
}
- let IDs = []
+ let IDs = [];
for (let e of this.state.selectedFiles) {
- IDs.push(e.id)
- }
- if (this.props.config.fileIDs !== null && this.props.config.fileIDs !== undefined) {
- if (JSON.stringify(IDs) !== JSON.stringify(this.props.config.fileIDs)) {
- data.fileIDs = IDs;
- }
- }
- else {
- data.fileIDs = IDs
+ IDs.push(e.id);
}
+ data.fileIDs = IDs;
//forward modified config to callback function
- this.props.onClose(data)
+ this.props.onClose(data);
}
} else {
this.props.onClose();
}
- this.setState({ startparamTemplate: null })
- this.valid = false
+ this.setState({ startparamTemplate: null });
+ this.valid = false;
}
handleChange(e) {
this.setState({ [e.target.id]: e.target.value });
- this.valid = this.isValid()
+ this.valid = this.isValid();
}
changeIC(id) {
let schema = null;
if (this.props.ics) {
- let currentIC = this.props.ics.find(ic => ic.id === parseInt(id, 10));
+ let currentIC = this.props.ics.find((ic) => ic.id === parseInt(id, 10));
if (currentIC) {
- if (currentIC.startparameterschema !== null && currentIC.startparameterschema.hasOwnProperty('type')) {
+ if (
+ currentIC.startparameterschema !== null &&
+ currentIC.startparameterschema.hasOwnProperty("type")
+ ) {
schema = currentIC.startparameterschema;
}
}
@@ -99,40 +104,44 @@ class EditConfigDialog extends React.Component {
startparamTemplate: schema,
});
- this.valid = this.isValid()
+ this.valid = this.isValid();
}
handleParameterChange(data) {
if (data) {
this.setState({ startParameters: data });
}
- this.valid = this.isValid()
+ this.valid = this.isValid();
}
onFileChange(selectedList, changedItem) {
this.setState({
- selectedFiles: selectedList
- })
- this.valid = this.isValid()
+ selectedFiles: selectedList,
+ });
+ this.valid = this.isValid();
}
-
isValid() {
// input is valid if at least one element has changed from its initial value
- return this.state.name !== ''
- || this.state.icID !== ''
- || Object.keys(this.state.startParameters).length === 0 && this.state.startParameters.constructor === Object
+ return (
+ this.state.name !== "" ||
+ this.state.icID !== "" ||
+ (Object.keys(this.state.startParameters).length === 0 &&
+ this.state.startParameters.constructor === Object)
+ );
}
resetState() {
-
// determine list of selected files incl id and filename
- let selectedFiles = []
- if (this.props.config.fileIDs !== null && this.props.config.fileIDs !== undefined) {
+ let selectedFiles = [];
+ if (
+ this.props.config.fileIDs !== null &&
+ this.props.config.fileIDs !== undefined
+ ) {
for (let selectedFileID of this.props.config.fileIDs) {
for (let file of this.props.files) {
if (file.id === selectedFileID) {
- selectedFiles.push({ name: file.name, id: file.id })
+ selectedFiles.push({ name: file.name, id: file.id });
}
}
}
@@ -140,9 +149,14 @@ class EditConfigDialog extends React.Component {
let schema = null;
if (this.props.ics && this.props.config.icID) {
- let currentIC = this.props.ics.find(ic => ic.id === parseInt(this.props.config.icID, 10));
+ let currentIC = this.props.ics.find(
+ (ic) => ic.id === parseInt(this.props.config.icID, 10)
+ );
if (currentIC) {
- if (currentIC.startparameterschema !== null && currentIC.startparameterschema.hasOwnProperty('type')) {
+ if (
+ currentIC.startparameterschema !== null &&
+ currentIC.startparameterschema.hasOwnProperty("type")
+ ) {
schema = currentIC.startparameterschema;
}
}
@@ -157,21 +171,21 @@ class EditConfigDialog extends React.Component {
});
}
- handleFormChange({formData}) {
- this.setState({formData: formData, startParameters: formData})
- this.valid = this.isValid()
+ handleFormChange({ formData }) {
+ this.setState({ formData: formData, startParameters: formData });
+ this.valid = this.isValid();
}
render() {
- const ICOptions = this.props.ics.map(s =>
- {s.name}
- );
+ const ICOptions = this.props.ics.map((s) => (
+
+ {s.name}
+
+ ));
let configFileOptions = [];
for (let file of this.props.files) {
- configFileOptions.push(
- { name: file.name, id: file.id }
- );
+ configFileOptions.push({ name: file.name, id: file.id });
}
return (
@@ -184,7 +198,7 @@ class EditConfigDialog extends React.Component {
valid={this.valid}
>
-
+
Name
-
- Infrastructure Component
+
+ Infrastructure Component
this.changeIC(e.target.value)}
>
@@ -207,35 +221,39 @@ class EditConfigDialog extends React.Component {
- this.onFileChange(selectedList, selectedItem)}
- onRemove={(selectedList, removedItem) => this.onFileChange(selectedList, removedItem)}
- displayValue={'name'}
- placeholder={'Select file(s)...'}
+
+ this.onFileChange(selectedItems, item)
+ }
/>
-
- Start Parameters
+
+
+ Start Parameters
+
- {!this.state.startparamTemplate ?
+ {!this.state.startparamTemplate ? (
this.handleParameterChange(data)}
- />
- : <>>}
+ content={this.state.startParameters}
+ onChange={(data) => this.handleParameterChange(data)}
+ />
+ ) : (
+ <>>
+ )}
- {this.state.startparamTemplate ?
-
diff --git a/src/pages/scenarios/dialogs/result-python-dialog.js b/src/pages/scenarios/dialogs/result-python-dialog.js
index f86b53a0..3619f097 100644
--- a/src/pages/scenarios/dialogs/result-python-dialog.js
+++ b/src/pages/scenarios/dialogs/result-python-dialog.js
@@ -15,16 +15,15 @@
* along with VILLASweb. If not, see .
******************************************************************************/
-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';
+import React from "react";
+import { Button } from "react-bootstrap";
+import Icon from "../../../common/icon";
+import Dialog from "../../../common/dialogs/dialog";
+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/';
+ villasDataProcessingUrl = "https://pypi.org/project/villas-dataprocessing/";
constructor(props) {
super(props);
@@ -38,11 +37,11 @@ class ResultPythonDialog extends React.Component {
if (result) {
const output = this.getJupyterNotebook(result);
const blob = new Blob([JSON.stringify(output)], {
- 'type': 'application/x-ipynb+json'
+ type: "application/x-ipynb+json",
});
const url = URL.createObjectURL(blob);
- this.setState({ fileDownloadUrl: url })
+ this.setState({ fileDownloadUrl: url });
}
}
}
@@ -51,26 +50,26 @@ class ResultPythonDialog extends React.Component {
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'
+ type: "application/x-ipynb+json",
});
var url = window.URL.createObjectURL(blob);
- var a = document.createElement('a');
- a.style = 'display: none';
+ 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(){
+ setTimeout(function () {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
}
getPythonDependencies(notebook) {
- let code = '';
+ let code = "";
if (notebook)
code += `import sys
!{sys.executable} -m `;
@@ -81,7 +80,7 @@ class ResultPythonDialog extends React.Component {
}
getPythonSnippets(notebook, result) {
- let token = localStorage.getItem('token');
+ let token = localStorage.getItem("token");
let files = [];
for (let file of this.props.files) {
@@ -95,17 +94,18 @@ class ResultPythonDialog extends React.Component {
let code_snippets = [];
/* Imports */
- let code_imports = '';
- if (notebook)
- code_imports += 'from IPython.display import display\n'
+ let code_imports = "";
+ if (notebook) code_imports += "from IPython.display import display\n";
- code_imports += `from villas.web.result import Result\n`
- code_imports += `from pprint import pprint`
+ code_imports += `from villas.web.result import Result\n`;
+ code_imports += `from pprint import pprint`;
- code_snippets.push(code_imports)
+ code_snippets.push(code_imports);
/* Result object */
- code_snippets.push(`r = Result(${result.id}, '${token}', endpoint='https://slew.k8s.eonerc.rwth-aachen.de')`);
+ code_snippets.push(
+ `r = Result(${result.id}, '${token}', endpoint='https://slew.k8s.eonerc.rwth-aachen.de')`
+ );
/* Examples */
code_snippets.push(`# Get result metadata
@@ -139,22 +139,22 @@ f${file.id} = r.get_file_by_name('${file.name}')`;
display(f${file.id})\n`;
switch (file.type) {
- case 'application/zip':
+ case "application/zip":
code += `\n# Open a file within the zipped results
with f${file.id}.open_zip('file_in_zip.csv') as f:
f${file.id} = pandas.read_csv(f)`;
break;
- case 'text/csv':
- case 'application/vnd.ms-excel':
- case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
- case 'application/x-hdf5':
- case 'application/x-matlab-data':
+ case "text/csv":
+ case "application/vnd.ms-excel":
+ case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
+ case "application/x-hdf5":
+ case "application/x-matlab-data":
code += `\n# Load tables as Pandas dataframe
f${file.id} = f${file.id}.load()`;
break;
- case 'application/json':
+ case "application/json":
code += `\n# Load JSON file as Python dictionary
f${file.id} = f${file.id}.load()`;
break;
@@ -176,107 +176,115 @@ f${file.id} = f${file.id}.load()`;
* See: https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html
*/
getCellId() {
- var result = [];
- var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ var result = [];
+ var characters =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var charactersLength = characters.length;
- for ( var i = 0; i < 8; i++ )
- result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));
+ for (var i = 0; i < 8; i++)
+ result.push(
+ characters.charAt(Math.floor(Math.random() * charactersLength))
+ );
- return result.join('');
+ return result.join("");
}
getJupyterNotebook(result) {
let ipynb_cells = [];
- let cells = [ this.getPythonDependencies(true) ];
+ let cells = [this.getPythonDependencies(true)];
cells = cells.concat(this.getPythonSnippets(true, result));
for (let cell of cells) {
- let lines = cell.split('\n');
+ let lines = cell.split("\n");
- for (let i = 0; i < lines.length -1; i++)
- lines[i] += '\n'
+ for (let i = 0; i < lines.length - 1; i++) lines[i] += "\n";
ipynb_cells.push({
- cell_type: 'code',
+ cell_type: "code",
execution_count: null,
id: this.getCellId(),
metadata: {},
outputs: [],
- source: lines
- })
+ source: lines,
+ });
}
return {
cells: ipynb_cells,
metadata: {
kernelspec: {
- display_name: 'Python 3',
- language: 'python',
- name: 'python3'
+ display_name: "Python 3",
+ language: "python",
+ name: "python3",
},
language_info: {
codemirror_mode: {
- name: 'ipython',
- version: 3
+ name: "ipython",
+ version: 3,
},
- file_extension: '.py',
- mimetype: 'text/x-python',
- name: 'python',
- nbconvert_exporter: 'python',
- pygments_lexer: 'ipython3',
- version: '3.9.5'
- }
+ file_extension: ".py",
+ mimetype: "text/x-python",
+ name: "python",
+ nbconvert_exporter: "python",
+ pygments_lexer: "ipython3",
+ version: "3.9.5",
+ },
},
nbformat: 4,
- nbformat_minor: 5
- }
+ nbformat_minor: 5,
+ };
}
render() {
let result = this.props.results[this.props.resultId];
- if (!result)
- return null;
+ if (!result) return null;
let snippets = this.getPythonSnippets(true, result);
- let code = snippets.join('\n\n');
+ let code = snippets.join("\n\n");
return (
this.props.onClose()}
valid={true}
- size='lg'
+ 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:
-
+
+ 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(false)}
- 2a) Insert the following snippet your Python code:
-
+
+ 2a) Insert the following snippet your Python code:
+
+
{code}
-
-
-
- Copy to Clipboard
-
-
- 2b) Or alternatively, download the following generated Jupyter notebook to get started:
+ navigator.clipboard.writeText(code)}>
+
+ Copy to Clipboard
+
+
+
+ 2b) Or alternatively, download the following generated Jupyter
+ notebook to get started:
+
-
- Download Jupyter Notebook
+
+ Download Jupyter Notebook
);