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, IconFileExport } 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, formatDataGridRows } from 'utils/genericHelper' import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' 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, onUseTemplate, 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 [toolIcon, setToolIcon] = 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) => [ } label='Delete' onClick={deleteItem(params.id)} /> ] } ], [deleteItem] ) useEffect(() => { if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) else dispatch({ type: HIDE_CANVAS_DIALOG }) return () => dispatch({ type: HIDE_CANVAS_DIALOG }) }, [show, dispatch]) useEffect(() => { if (getSpecificToolApi.data) { setToolId(getSpecificToolApi.data.id) setToolName(getSpecificToolApi.data.name) setToolDesc(getSpecificToolApi.data.description) setToolSchema(formatDataGridRows(getSpecificToolApi.data.schema)) if (getSpecificToolApi.data.func) setToolFunc(getSpecificToolApi.data.func) else setToolFunc('') } }, [getSpecificToolApi.data]) useEffect(() => { if (dialogProps.type === 'EDIT' && dialogProps.data) { // When tool dialog is opened from Tools dashboard setToolId(dialogProps.data.id) setToolName(dialogProps.data.name) setToolDesc(dialogProps.data.description) setToolIcon(dialogProps.data.iconSrc) setToolSchema(formatDataGridRows(dialogProps.data.schema)) if (dialogProps.data.func) setToolFunc(dialogProps.data.func) else setToolFunc('') } else if (dialogProps.type === 'EDIT' && dialogProps.toolId) { // When tool dialog is opened from CustomTool node in canvas getSpecificToolApi.request(dialogProps.toolId) } else if (dialogProps.type === 'IMPORT' && dialogProps.data) { // When tool dialog is to import existing tool setToolName(dialogProps.data.name) setToolDesc(dialogProps.data.description) setToolIcon(dialogProps.data.iconSrc) setToolSchema(formatDataGridRows(dialogProps.data.schema)) if (dialogProps.data.func) setToolFunc(dialogProps.data.func) else setToolFunc('') } else if (dialogProps.type === 'TEMPLATE' && dialogProps.data) { // When tool dialog is a template setToolName(dialogProps.data.name) setToolDesc(dialogProps.data.description) setToolIcon(dialogProps.data.iconSrc) setToolSchema(formatDataGridRows(dialogProps.data.schema)) if (dialogProps.data.func) setToolFunc(dialogProps.data.func) else setToolFunc('') } else if (dialogProps.type === 'ADD') { // When tool dialog is to add a new tool setToolId('') setToolName('') setToolDesc('') setToolIcon('') setToolSchema([]) setToolFunc('') } // eslint-disable-next-line react-hooks/exhaustive-deps }, [dialogProps]) const useToolTemplate = () => { onUseTemplate(dialogProps.data) } const exportTool = async () => { try { const toolResp = await toolsApi.getSpecificTool(toolId) if (toolResp.data) { const toolData = toolResp.data delete toolData.id delete toolData.createdDate delete toolData.updatedDate let dataStr = JSON.stringify(toolData, null, 2) let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr) let exportFileDefaultName = `${toolName}-CustomTool.json` let linkElement = document.createElement('a') linkElement.setAttribute('href', dataUri) linkElement.setAttribute('download', exportFileDefaultName) linkElement.click() } } catch (error) { const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ message: `Failed to export Tool: ${errorData}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', persist: true, action: (key) => ( closeSnackbar(key)}> ) } }) onCancel() } } const addNewTool = async () => { try { const obj = { name: toolName, description: toolDesc, color: generateRandomGradient(), schema: JSON.stringify(toolSchema), func: toolFunc, iconSrc: toolIcon } const createResp = await toolsApi.createNewTool(obj) if (createResp.data) { enqueueSnackbar({ message: 'New Tool added', options: { key: new Date().getTime() + Math.random(), variant: 'success', action: (key) => ( closeSnackbar(key)}> ) } }) onConfirm(createResp.data.id) } } catch (error) { const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ message: `Failed to add new Tool: ${errorData}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', persist: true, action: (key) => ( closeSnackbar(key)}> ) } }) onCancel() } } const saveTool = async () => { try { const saveResp = await toolsApi.updateTool(toolId, { name: toolName, description: toolDesc, schema: JSON.stringify(toolSchema), func: toolFunc, iconSrc: toolIcon }) if (saveResp.data) { enqueueSnackbar({ message: 'Tool saved', options: { key: new Date().getTime() + Math.random(), variant: 'success', action: (key) => ( closeSnackbar(key)}> ) } }) onConfirm(saveResp.data.id) } } catch (error) { console.error(error) const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ message: `Failed to save Tool: ${errorData}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', persist: true, action: (key) => ( closeSnackbar(key)}> ) } }) onCancel() } } const deleteTool = async () => { const confirmPayload = { title: `Delete Tool`, description: `Delete tool ${toolName}?`, confirmButtonName: 'Delete', cancelButtonName: 'Cancel' } const isConfirmed = await confirm(confirmPayload) if (isConfirmed) { try { const delResp = await toolsApi.deleteTool(toolId) if (delResp.data) { enqueueSnackbar({ message: 'Tool deleted', options: { key: new Date().getTime() + Math.random(), variant: 'success', action: (key) => ( closeSnackbar(key)}> ) } }) onConfirm() } } catch (error) { const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ message: `Failed to delete Tool: ${errorData}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', persist: true, action: (key) => ( closeSnackbar(key)}> ) } }) onCancel() } } } const component = show ? ( {dialogProps.title} {dialogProps.type === 'EDIT' && ( exportTool()} startIcon={}> Export )} Tool Name * setToolName(e.target.value)} /> Tool description * setToolDesc(e.target.value)} /> Tool Icon Src setToolIcon(e.target.value)} /> Output Schema Javascript Function {dialogProps.type !== 'TEMPLATE' && ( setToolFunc(exampleAPIFunc)}> See Example )} {customization.isDarkMode ? ( setToolFunc(code)} style={{ fontSize: '0.875rem', minHeight: 'calc(100vh - 220px)', width: '100%', borderRadius: 5 }} /> ) : ( setToolFunc(code)} style={{ fontSize: '0.875rem', minHeight: 'calc(100vh - 220px)', width: '100%', border: `1px solid ${theme.palette.grey[300]}`, borderRadius: 5 }} /> )} {dialogProps.type === 'EDIT' && ( deleteTool()}> Delete )} {dialogProps.type === 'TEMPLATE' && ( Use Template )} {dialogProps.type !== 'TEMPLATE' && ( (dialogProps.type === 'ADD' || dialogProps.type === 'IMPORT' ? addNewTool() : saveTool())} > {dialogProps.confirmButtonName} )} ) : null return createPortal(component, portalElement) } ToolDialog.propTypes = { show: PropTypes.bool, dialogProps: PropTypes.object, onUseTemplate: PropTypes.func, onCancel: PropTypes.func, onConfirm: PropTypes.func } export default ToolDialog