{
+ const loadMethod = nodeData.inputParams.find((param) => param.name === name)?.loadMethod
+ const username = localStorage.getItem('username')
+ const password = localStorage.getItem('password')
+
+ let lists = await axios
+ .post(
+ `${baseURL}/api/v1/node-load-method/${nodeData.name}`,
+ { ...nodeData, loadMethod },
+ { auth: username && password ? { username, password } : undefined }
+ )
+ .then(async function (response) {
+ return response.data
+ })
+ .catch(function (error) {
+ console.error(error)
+ })
+ return lists
+}
+
+export const AsyncDropdown = ({
+ name,
+ nodeData,
+ value,
+ onSelect,
+ isCreateNewOption,
+ onCreateNew,
+ disabled = false,
+ disableClearable = false
+}) => {
+ const customization = useSelector((state) => state.customization)
+
+ const [open, setOpen] = useState(false)
+ const [options, setOptions] = useState([])
+ const [loading, setLoading] = useState(false)
+ const findMatchingOptions = (options = [], value) => options.find((option) => option.name === value)
+ const getDefaultOptionValue = () => ''
+ const addNewOption = [{ label: '- Create New -', name: '-create-' }]
+ let [internalValue, setInternalValue] = useState(value ?? 'choose an option')
+
+ useEffect(() => {
+ setLoading(true)
+ ;(async () => {
+ const fetchData = async () => {
+ let response = await fetchList({ name, nodeData })
+ if (isCreateNewOption) setOptions([...response, ...addNewOption])
+ else setOptions([...response])
+ setLoading(false)
+ }
+ fetchData()
+ })()
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ return (
+ <>
+
{
+ setOpen(true)
+ }}
+ onClose={() => {
+ setOpen(false)
+ }}
+ options={options}
+ value={findMatchingOptions(options, internalValue) || getDefaultOptionValue()}
+ onChange={(e, selection) => {
+ const value = selection ? selection.name : ''
+ if (isCreateNewOption && value === '-create-') {
+ onCreateNew()
+ } else {
+ setInternalValue(value)
+ onSelect(value)
+ }
+ }}
+ PopperComponent={StyledPopper}
+ loading={loading}
+ renderInput={(params) => (
+
+ {loading ? : null}
+ {params.InputProps.endAdornment}
+
+ )
+ }}
+ />
+ )}
+ renderOption={(props, option) => (
+
+
+ {option.label}
+ {option.description && (
+ {option.description}
+ )}
+
+
+ )}
+ />
+ >
+ )
+}
+
+AsyncDropdown.propTypes = {
+ name: PropTypes.string,
+ nodeData: PropTypes.object,
+ value: PropTypes.string,
+ onSelect: PropTypes.func,
+ onCreateNew: PropTypes.func,
+ disabled: PropTypes.bool,
+ disableClearable: PropTypes.bool,
+ isCreateNewOption: PropTypes.bool
+}
diff --git a/packages/ui/src/ui-component/editor/DarkCodeEditor.js b/packages/ui/src/ui-component/editor/DarkCodeEditor.js
index 3925f4a6..bf0719dd 100644
--- a/packages/ui/src/ui-component/editor/DarkCodeEditor.js
+++ b/packages/ui/src/ui-component/editor/DarkCodeEditor.js
@@ -21,6 +21,7 @@ export const DarkCodeEditor = ({ value, placeholder, disabled = false, type, sty
onValueChange={onValueChange}
onMouseUp={onMouseUp}
onBlur={onBlur}
+ tabSize={4}
style={{
...style,
background: theme.palette.codeEditor.main
diff --git a/packages/ui/src/ui-component/editor/LightCodeEditor.js b/packages/ui/src/ui-component/editor/LightCodeEditor.js
index 86f7057d..14dcbf29 100644
--- a/packages/ui/src/ui-component/editor/LightCodeEditor.js
+++ b/packages/ui/src/ui-component/editor/LightCodeEditor.js
@@ -21,6 +21,7 @@ export const LightCodeEditor = ({ value, placeholder, disabled = false, type, st
onValueChange={onValueChange}
onMouseUp={onMouseUp}
onBlur={onBlur}
+ tabSize={4}
style={{
...style,
background: theme.palette.card.main
diff --git a/packages/ui/src/ui-component/grid/Grid.js b/packages/ui/src/ui-component/grid/Grid.js
new file mode 100644
index 00000000..2049f56c
--- /dev/null
+++ b/packages/ui/src/ui-component/grid/Grid.js
@@ -0,0 +1,37 @@
+import PropTypes from 'prop-types'
+import { DataGrid } from '@mui/x-data-grid'
+import { IconPlus } from '@tabler/icons'
+import { Button } from '@mui/material'
+
+export const Grid = ({ columns, rows, style, onRowUpdate, addNewRow }) => {
+ const handleProcessRowUpdate = (newRow) => {
+ onRowUpdate(newRow)
+ return newRow
+ }
+
+ return (
+ <>
+ }>
+ Add Item
+
+ {rows && columns && (
+
+ console.error(error)}
+ rows={rows}
+ columns={columns}
+ />
+
+ )}
+ >
+ )
+}
+
+Grid.propTypes = {
+ rows: PropTypes.array,
+ columns: PropTypes.array,
+ style: PropTypes.any,
+ addNewRow: PropTypes.func,
+ onRowUpdate: PropTypes.func
+}
diff --git a/packages/ui/src/utils/genericHelper.js b/packages/ui/src/utils/genericHelper.js
index fac83225..03f891ec 100644
--- a/packages/ui/src/utils/genericHelper.js
+++ b/packages/ui/src/utils/genericHelper.js
@@ -39,7 +39,7 @@ export const initNode = (nodeData, newNodeId) => {
const incoming = nodeData.inputs ? nodeData.inputs.length : 0
const outgoing = 1
- const whitelistTypes = ['options', 'string', 'number', 'boolean', 'password', 'json', 'code', 'date', 'file', 'folder']
+ const whitelistTypes = ['asyncOptions', 'options', 'string', 'number', 'boolean', 'password', 'json', 'code', 'date', 'file', 'folder']
for (let i = 0; i < incoming; i += 1) {
const newInput = {
@@ -334,3 +334,22 @@ export const throttle = (func, limit) => {
}
}
}
+
+export const generateRandomGradient = () => {
+ function randomColor() {
+ var color = 'rgb('
+ for (var i = 0; i < 3; i++) {
+ var random = Math.floor(Math.random() * 256)
+ color += random
+ if (i < 2) {
+ color += ','
+ }
+ }
+ color += ')'
+ return color
+ }
+
+ var gradient = 'linear-gradient(' + randomColor() + ', ' + randomColor() + ')'
+
+ return gradient
+}
diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js
index d58f7a66..31a8a37d 100644
--- a/packages/ui/src/views/canvas/NodeInputHandler.js
+++ b/packages/ui/src/views/canvas/NodeInputHandler.js
@@ -7,10 +7,11 @@ import { useSelector } from 'react-redux'
import { useTheme, styled } from '@mui/material/styles'
import { Box, Typography, Tooltip, IconButton } from '@mui/material'
import { tooltipClasses } from '@mui/material/Tooltip'
-import { IconArrowsMaximize } from '@tabler/icons'
+import { IconArrowsMaximize, IconEdit } from '@tabler/icons'
// project import
import { Dropdown } from 'ui-component/dropdown/Dropdown'
+import { AsyncDropdown } from 'ui-component/dropdown/AsyncDropdown'
import { Input } from 'ui-component/input/Input'
import { File } from 'ui-component/file/File'
import { SwitchInput } from 'ui-component/switch/Switch'
@@ -18,6 +19,9 @@ import { flowContext } from 'store/context/ReactFlowContext'
import { isValidConnection, getAvailableNodesForVariable } from 'utils/genericHelper'
import { JsonEditorInput } from 'ui-component/json/JsonEditor'
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
+import ToolDialog from 'views/tools/ToolDialog'
+
+const EDITABLE_TOOLS = ['selectedTool']
const CustomWidthTooltip = styled(({ className, ...props }) => )({
[`& .${tooltipClasses.tooltip}`]: {
@@ -36,6 +40,9 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
const [position, setPosition] = useState(0)
const [showExpandDialog, setShowExpandDialog] = useState(false)
const [expandDialogProps, setExpandDialogProps] = useState({})
+ const [showAsyncOptionDialog, setAsyncOptionEditDialog] = useState('')
+ const [asyncOptionEditDialogProps, setAsyncOptionEditDialogProps] = useState({})
+ const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString())
const onExpandDialogClicked = (value, inputParam) => {
const dialogProp = {
@@ -61,6 +68,42 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
data.inputs[inputParamName] = newValue
}
+ const editAsyncOption = (inputParamName, inputValue) => {
+ if (inputParamName === 'selectedTool') {
+ setAsyncOptionEditDialogProps({
+ title: 'Edit Tool',
+ type: 'EDIT',
+ cancelButtonName: 'Cancel',
+ confirmButtonName: 'Save',
+ toolId: inputValue
+ })
+ }
+ setAsyncOptionEditDialog(inputParamName)
+ }
+
+ const addAsyncOption = (inputParamName) => {
+ if (inputParamName === 'selectedTool') {
+ setAsyncOptionEditDialogProps({
+ title: 'Add New Tool',
+ type: 'ADD',
+ cancelButtonName: 'Cancel',
+ confirmButtonName: 'Add'
+ })
+ }
+ setAsyncOptionEditDialog(inputParamName)
+ }
+
+ const onConfirmAsyncOption = (selectedOptionId = '') => {
+ if (!selectedOptionId) {
+ data.inputs[showAsyncOptionDialog] = ''
+ } else {
+ data.inputs[showAsyncOptionDialog] = selectedOptionId
+ setReloadTimestamp(Date.now().toString())
+ }
+ setAsyncOptionEditDialogProps({})
+ setAsyncOptionEditDialog('')
+ }
+
useEffect(() => {
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2)
@@ -186,12 +229,44 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
name={inputParam.name}
options={inputParam.options}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
- value={data.inputs[inputParam.name] ?? inputParam.default ?? 'chose an option'}
+ value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
/>
)}
+ {inputParam.type === 'asyncOptions' && (
+ <>
+ {data.inputParams.length === 1 && }
+
+
(data.inputs[inputParam.name] = newValue)}
+ onCreateNew={() => addAsyncOption(inputParam.name)}
+ />
+ {EDITABLE_TOOLS.includes(inputParam.name) && data.inputs[inputParam.name] && (
+ editAsyncOption(inputParam.name, data.inputs[inputParam.name])}
+ >
+
+
+ )}
+
+ >
+ )}
>
)}
+ setAsyncOptionEditDialog('')}
+ onConfirm={onConfirmAsyncOption}
+ >
)
}
diff --git a/packages/ui/src/views/tools/ToolDialog.js b/packages/ui/src/views/tools/ToolDialog.js
new file mode 100644
index 00000000..bd5af355
--- /dev/null
+++ b/packages/ui/src/views/tools/ToolDialog.js
@@ -0,0 +1,448 @@
+import { createPortal } from 'react-dom'
+import PropTypes from 'prop-types'
+import { useState, useEffect, useCallback, useMemo } from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
+import { cloneDeep } from 'lodash'
+
+import { Box, Typography, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, OutlinedInput } from '@mui/material'
+import { StyledButton } from 'ui-component/button/StyledButton'
+import { Grid } from 'ui-component/grid/Grid'
+import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
+import { GridActionsCellItem } from '@mui/x-data-grid'
+import DeleteIcon from '@mui/icons-material/Delete'
+import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
+import { DarkCodeEditor } from 'ui-component/editor/DarkCodeEditor'
+import { LightCodeEditor } from 'ui-component/editor/LightCodeEditor'
+import { useTheme } from '@mui/material/styles'
+
+// Icons
+import { IconX } from '@tabler/icons'
+
+// API
+import toolsApi from 'api/tools'
+
+// Hooks
+import useConfirm from 'hooks/useConfirm'
+import useApi from 'hooks/useApi'
+
+// utils
+import useNotifier from 'utils/useNotifier'
+import { generateRandomGradient } from 'utils/genericHelper'
+
+const exampleAPIFunc = `/*
+* You can use any libraries imported in Flowise
+* You can use properties specified in Output Schema as variables. Ex: Property = userid, Variable = $userid
+* Must return a string value at the end of function
+*/
+
+const fetch = require('node-fetch');
+const url = 'https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t_weather=true';
+const options = {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+};
+try {
+ const response = await fetch(url, options);
+ const text = await response.text();
+ return text;
+} catch (error) {
+ console.error(error);
+ return '';
+}`
+
+const ToolDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
+ const portalElement = document.getElementById('portal')
+ const theme = useTheme()
+
+ const customization = useSelector((state) => state.customization)
+ const dispatch = useDispatch()
+
+ // ==============================|| Snackbar ||============================== //
+
+ useNotifier()
+ const { confirm } = useConfirm()
+
+ const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
+ const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
+
+ const getSpecificToolApi = useApi(toolsApi.getSpecificTool)
+
+ const [toolId, setToolId] = useState('')
+ const [toolName, setToolName] = useState('')
+ const [toolDesc, setToolDesc] = useState('')
+ const [toolSchema, setToolSchema] = useState([])
+ const [toolFunc, setToolFunc] = useState('')
+
+ const deleteItem = useCallback(
+ (id) => () => {
+ setTimeout(() => {
+ setToolSchema((prevRows) => prevRows.filter((row) => row.id !== id))
+ })
+ },
+ []
+ )
+
+ const addNewRow = () => {
+ setTimeout(() => {
+ setToolSchema((prevRows) => {
+ let allRows = [...cloneDeep(prevRows)]
+ const lastRowId = allRows.length ? allRows[allRows.length - 1].id + 1 : 1
+ allRows.push({
+ id: lastRowId,
+ property: '',
+ description: '',
+ type: '',
+ required: false
+ })
+ return allRows
+ })
+ })
+ }
+
+ const onRowUpdate = (newRow) => {
+ setTimeout(() => {
+ setToolSchema((prevRows) => {
+ let allRows = [...cloneDeep(prevRows)]
+ const indexToUpdate = allRows.findIndex((row) => row.id === newRow.id)
+ if (indexToUpdate >= 0) {
+ allRows[indexToUpdate] = { ...newRow }
+ }
+ return allRows
+ })
+ })
+ }
+
+ const columns = useMemo(
+ () => [
+ { field: 'property', headerName: 'Property', editable: true, flex: 1 },
+ {
+ field: 'type',
+ headerName: 'Type',
+ type: 'singleSelect',
+ valueOptions: ['string', 'number', 'boolean', 'date'],
+ editable: true,
+ width: 120
+ },
+ { field: 'description', headerName: 'Description', editable: true, flex: 1 },
+ { field: 'required', headerName: 'Required', type: 'boolean', editable: true, width: 80 },
+ {
+ field: 'actions',
+ type: 'actions',
+ width: 80,
+ getActions: (params) => [
+