diff --git a/packages/ui/src/ui-component/cards/ItemCard.jsx b/packages/ui/src/ui-component/cards/ItemCard.jsx index fa113ee22a1..c40746e1e70 100644 --- a/packages/ui/src/ui-component/cards/ItemCard.jsx +++ b/packages/ui/src/ui-component/cards/ItemCard.jsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from 'react' import PropTypes from 'prop-types' import { useSelector } from 'react-redux' @@ -29,9 +30,36 @@ const CardWrapper = styled(MainCard)(({ theme }) => ({ whiteSpace: 'pre-line' })) +const IconAvatar = ({ iconSrc, fallbackIconSrc }) => { + const [imgSrc, setImgSrc] = useState(iconSrc || fallbackIconSrc) + + useEffect(() => { + setImgSrc(iconSrc || fallbackIconSrc) + }, [iconSrc, fallbackIconSrc]) + + if (!imgSrc) return null + + return ( + item-icon setImgSrc(fallbackIconSrc || '')} + style={{ + width: 35, + height: 35, + display: 'flex', + flexShrink: 0, + marginRight: 10, + borderRadius: '50%', + objectFit: 'contain' + }} + /> + ) +} + // ===========================|| CONTRACT CARD ||=========================== // -const ItemCard = ({ data, images, icons, scheduleStatus, onClick }) => { +const ItemCard = ({ data, images, icons, onClick, fallbackIconSrc }) => { const theme = useTheme() const customization = useSelector((state) => state.customization) @@ -49,23 +77,27 @@ const ItemCard = ({ data, images, icons, scheduleStatus, onClick }) => { overflow: 'hidden' }} > - {data.iconSrc && ( -
+ {fallbackIconSrc ? ( + + ) : ( + data.iconSrc && ( +
+ ) )} - {!data.iconSrc && data.color && ( + {!fallbackIconSrc && !data.iconSrc && data.color && (
({ borderColor: theme.palette.grey[900] + 25, @@ -35,6 +37,31 @@ const StyledTableRow = styled(TableRow)(() => ({ } })) +const ToolIconAvatar = ({ iconSrc }) => { + const [imgSrc, setImgSrc] = useState(iconSrc || toolSVG) + + useEffect(() => { + setImgSrc(iconSrc || toolSVG) + }, [iconSrc]) + + return ( + tool-icon setImgSrc(toolSVG)} + style={{ + width: 35, + height: 35, + display: 'flex', + flexShrink: 0, + marginRight: 10, + borderRadius: '50%', + objectFit: 'contain' + }} + /> + ) +} + export const ToolsTable = ({ data, isLoading, onSelect }) => { const theme = useTheme() const customization = useSelector((state) => state.customization) @@ -90,20 +117,7 @@ export const ToolsTable = ({ data, isLoading, onSelect }) => { {data?.map((row, index) => ( -
+ { + const trimmedIconSource = iconSource?.trim() + if (!trimmedIconSource) return '' + + try { + const parsedUrl = new URL(trimmedIconSource) + if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { + return 'Tool Icon Source must be a valid HTTP/HTTPS URL' + } + return '' + } catch { + return 'Tool Icon Source must be a valid HTTP/HTTPS URL' + } + } + + const validateIconAndNotify = () => { + const iconError = validateToolIconUrl(toolIcon) + setToolIconError(iconError) + + if (iconError) { + enqueueSnackbar({ + message: iconError, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + action: (key) => ( + + ) + } + }) + return false + } + + return true + } + const deleteItem = useCallback( (id) => () => { setTimeout(() => { @@ -177,6 +216,8 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set setToolId(getSpecificToolApi.data.id) setToolName(getSpecificToolApi.data.name) setToolDesc(getSpecificToolApi.data.description) + setToolIcon(getSpecificToolApi.data.iconSrc || '') + setToolIconError('') setToolSchema(formatDataGridRows(getSpecificToolApi.data.schema)) if (getSpecificToolApi.data.func) setToolFunc(getSpecificToolApi.data.func) else setToolFunc('') @@ -196,7 +237,8 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set setToolId(dialogProps.data.id) setToolName(dialogProps.data.name) setToolDesc(dialogProps.data.description) - setToolIcon(dialogProps.data.iconSrc) + setToolIcon(dialogProps.data.iconSrc || '') + setToolIconError('') setToolSchema(formatDataGridRows(dialogProps.data.schema)) if (dialogProps.data.func) setToolFunc(dialogProps.data.func) else setToolFunc('') @@ -207,7 +249,8 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set // When tool dialog is to import existing tool setToolName(dialogProps.data.name) setToolDesc(dialogProps.data.description) - setToolIcon(dialogProps.data.iconSrc) + setToolIcon(dialogProps.data.iconSrc || '') + setToolIconError('') setToolSchema(formatDataGridRows(dialogProps.data.schema)) if (dialogProps.data.func) setToolFunc(dialogProps.data.func) else setToolFunc('') @@ -215,7 +258,8 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set // When tool dialog is a template setToolName(dialogProps.data.name) setToolDesc(dialogProps.data.description) - setToolIcon(dialogProps.data.iconSrc) + setToolIcon(dialogProps.data.iconSrc || '') + setToolIconError('') setToolSchema(formatDataGridRows(dialogProps.data.schema)) if (dialogProps.data.func) setToolFunc(dialogProps.data.func) else setToolFunc('') @@ -225,6 +269,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set setToolName('') setToolDesc('') setToolIcon('') + setToolIconError('') setToolSchema([]) setToolFunc('') } @@ -277,6 +322,8 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set } const addNewTool = async () => { + if (!validateIconAndNotify()) return + try { const obj = { name: toolName, @@ -323,6 +370,8 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set } const saveTool = async () => { + if (!validateIconAndNotify()) return + try { const saveResp = await toolsApi.updateTool(toolId, { name: toolName, @@ -513,8 +562,23 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set placeholder='https://raw.githubusercontent.com/gilbarbara/logos/main/logos/airtable.svg' value={toolIcon} name='toolIcon' - onChange={(e) => setToolIcon(e.target.value)} + error={!!toolIconError} + onBlur={() => setToolIconError(validateToolIconUrl(toolIcon))} + onChange={(e) => { + const iconSource = e.target.value + setToolIcon(iconSource) + setToolIconError(validateToolIconUrl(iconSource)) + }} /> + {toolIconError ? ( + + {toolIconError} + + ) : ( + + Optional. Leave empty to use the default tool icon. + + )} @@ -583,7 +647,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set {dialogProps.type !== 'TEMPLATE' && ( (dialogProps.type === 'ADD' || dialogProps.type === 'IMPORT' ? addNewTool() : saveTool())} > diff --git a/packages/ui/src/views/tools/index.jsx b/packages/ui/src/views/tools/index.jsx index 0c15ee2b474..bc8427fcf9c 100644 --- a/packages/ui/src/views/tools/index.jsx +++ b/packages/ui/src/views/tools/index.jsx @@ -1,7 +1,7 @@ import { useEffect, useState, useRef } from 'react' // material-ui -import { Box, Stack, ButtonGroup, Skeleton, ToggleButtonGroup, ToggleButton, Tabs, Tab } from '@mui/material' +import { Box, Stack, ButtonGroup, Skeleton, ToggleButtonGroup, ToggleButton } from '@mui/material' import { useTheme } from '@mui/material/styles' // project imports @@ -29,6 +29,7 @@ import { gridSpacing } from '@/store/constant' // icons import { IconPlus, IconFileUpload, IconLayoutGrid, IconList } from '@tabler/icons-react' import ToolEmptySVG from '@/assets/images/tools_empty.svg' +import toolSVG from '@/assets/images/tool.svg' // ==============================|| TOOLS ||============================== // @@ -413,14 +414,108 @@ const Tools = () => { borderColor: 'divider' }} > - setTabValue(newValue)} aria-label='tools tabs'> - - - - {tabValue === 0 ? renderCustomToolsToolbar() : renderMcpServersToolbar()} + + + + + + + + + + inputRef.current.click()} + startIcon={} + sx={{ borderRadius: 2, height: 40 }} + > + Load + + handleFileUpload(e)} + /> + + + } + sx={{ borderRadius: 2, height: 40 }} + > + Create + + - {tabValue === 0 && renderCustomToolsTab()} - {tabValue === 1 && renderMcpServersTab()} + {isLoading && ( + + + + + + )} + {!isLoading && total > 0 && ( + <> + {!view || view === 'card' ? ( + + {getAllToolsApi.data?.data?.filter(filterTools).map((data, index) => ( + edit(data)} fallbackIconSrc={toolSVG} /> + ))} + + ) : ( + + )} + {/* Pagination and Page Size Controls */} + + + )} + {!isLoading && total === 0 && ( + + + ToolEmptySVG + +
No Tools Created Yet
+
+ )} )}