Add marketplaces

This commit is contained in:
Henry
2023-04-10 17:47:25 +01:00
parent 58e06718d1
commit c576ea5b67
24 changed files with 2665 additions and 15 deletions
+7
View File
@@ -0,0 +1,7 @@
import client from './client'
const getAllMarketplaces = () => client.get('/marketplaces')
export default {
getAllMarketplaces
}
+10 -2
View File
@@ -1,8 +1,8 @@
// assets
import { IconHierarchy, IconKey, IconBook, IconListCheck } from '@tabler/icons'
import { IconHierarchy, IconBuildingStore } from '@tabler/icons'
// constant
const icons = { IconHierarchy, IconKey, IconBook, IconListCheck }
const icons = { IconHierarchy, IconBuildingStore }
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
@@ -18,6 +18,14 @@ const dashboard = {
url: '/chatflows',
icon: icons.IconHierarchy,
breadcrumbs: true
},
{
id: 'marketplaces',
title: 'Marketplaces',
type: 'item',
url: '/marketplaces',
icon: icons.IconBuildingStore,
breadcrumbs: true
}
]
}
+5
View File
@@ -6,6 +6,7 @@ import MinimalLayout from 'layout/MinimalLayout'
// canvas routing
const Canvas = Loadable(lazy(() => import('views/canvas')))
const MarketplaceCanvas = Loadable(lazy(() => import('views/marketplaces/MarketplaceCanvas')))
// ==============================|| CANVAS ROUTING ||============================== //
@@ -20,6 +21,10 @@ const CanvasRoutes = {
{
path: '/canvas/:id',
element: <Canvas />
},
{
path: '/marketplace/:id',
element: <MarketplaceCanvas />
}
]
}
+7
View File
@@ -7,6 +7,9 @@ import Loadable from 'ui-component/loading/Loadable'
// chatflows routing
const Chatflows = Loadable(lazy(() => import('views/chatflows')))
// marketplaces routing
const Marketplaces = Loadable(lazy(() => import('views/marketplaces')))
// ==============================|| MAIN ROUTING ||============================== //
const MainRoutes = {
@@ -20,6 +23,10 @@ const MainRoutes = {
{
path: '/chatflows',
element: <Chatflows />
},
{
path: '/marketplaces',
element: <Marketplaces />
}
]
}
@@ -45,9 +45,10 @@ const ItemCard = ({ isLoading, data, images, onClick }) => {
<CardWrapper border={false} content={false} onClick={onClick}>
<Box sx={{ p: 2.25 }}>
<Grid container direction='column'>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<div>
<Typography sx={{ fontSize: '1.5rem', fontWeight: 500 }}>{data.name}</Typography>
</div>
{data.description && <span style={{ marginTop: 10 }}>{data.description}</span>}
<Grid sx={{ mt: 1, mb: 1 }} container direction='row'>
{data.deployed && (
<Grid item>
@@ -18,7 +18,7 @@ const StyledPopper = styled(Popper)({
}
})
export const Dropdown = ({ name, value, options, onSelect }) => {
export const Dropdown = ({ name, value, options, onSelect, disabled = false }) => {
const customization = useSelector((state) => state.customization)
const findMatchingOptions = (options = [], value) => options.find((option) => option.name === value)
const getDefaultOptionValue = () => ''
@@ -28,6 +28,7 @@ export const Dropdown = ({ name, value, options, onSelect }) => {
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
<Autocomplete
id={name}
disabled={disabled}
size='small'
options={options || []}
value={findMatchingOptions(options, internalValue) || getDefaultOptionValue()}
@@ -57,5 +58,6 @@ Dropdown.propTypes = {
name: PropTypes.string,
value: PropTypes.string,
options: PropTypes.array,
onSelect: PropTypes.func
onSelect: PropTypes.func,
disabled: PropTypes.bool
}
+11 -3
View File
@@ -5,7 +5,7 @@ import { FormControl, Button } from '@mui/material'
import { IconUpload } from '@tabler/icons'
import { getFileName } from 'utils/genericHelper'
export const File = ({ value, fileType, onChange }) => {
export const File = ({ value, fileType, onChange, disabled = false }) => {
const theme = useTheme()
const [myValue, setMyValue] = useState(value ?? '')
@@ -42,7 +42,14 @@ export const File = ({ value, fileType, onChange }) => {
>
{myValue ? getFileName(myValue) : 'Choose a file to upload'}
</span>
<Button variant='outlined' component='label' fullWidth startIcon={<IconUpload />} sx={{ marginRight: '1rem' }}>
<Button
disabled={disabled}
variant='outlined'
component='label'
fullWidth
startIcon={<IconUpload />}
sx={{ marginRight: '1rem' }}
>
{'Upload File'}
<input type='file' accept={fileType} hidden onChange={(e) => handleFileUpload(e)} />
</Button>
@@ -53,5 +60,6 @@ export const File = ({ value, fileType, onChange }) => {
File.propTypes = {
value: PropTypes.string,
fileType: PropTypes.string,
onChange: PropTypes.func
onChange: PropTypes.func,
disabled: PropTypes.bool
}
+4 -2
View File
@@ -2,13 +2,14 @@ import { useState } from 'react'
import PropTypes from 'prop-types'
import { FormControl, OutlinedInput } from '@mui/material'
export const Input = ({ inputParam, value, onChange }) => {
export const Input = ({ inputParam, value, onChange, disabled = false }) => {
const [myValue, setMyValue] = useState(value ?? '')
return (
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
<OutlinedInput
id={inputParam.name}
size='small'
disabled={disabled}
type={inputParam.type === 'string' ? 'text' : inputParam.type}
placeholder={inputParam.placeholder}
multiline={!!inputParam.rows}
@@ -28,5 +29,6 @@ export const Input = ({ inputParam, value, onChange }) => {
Input.propTypes = {
inputParam: PropTypes.object,
value: PropTypes.string,
onChange: PropTypes.func
onChange: PropTypes.func,
disabled: PropTypes.bool
}
+3 -1
View File
@@ -203,12 +203,14 @@ export const generateExportFlowData = (flowData) => {
selected: false
}
// Remove password
// Remove password, file & folder
if (node.data.inputs && Object.keys(node.data.inputs).length) {
const nodeDataInputs = {}
for (const input in node.data.inputs) {
const inputParam = node.data.inputParams.find((inp) => inp.name === input)
if (inputParam && inputParam.type === 'password') continue
if (inputParam && inputParam.type === 'file') continue
if (inputParam && inputParam.type === 'folder') continue
nodeDataInputs[input] = node.data.inputs[input]
}
newNodeData.inputs = nodeDataInputs
@@ -14,7 +14,7 @@ import { isValidConnection } from 'utils/genericHelper'
// ===========================|| NodeInputHandler ||=========================== //
const NodeInputHandler = ({ inputAnchor, inputParam, data }) => {
const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false }) => {
const theme = useTheme()
const ref = useRef(null)
const updateNodeInternals = useUpdateNodeInternals()
@@ -76,6 +76,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data }) => {
</Typography>
{inputParam.type === 'file' && (
<File
disabled={disabled}
fileType={inputParam.fileType || '*'}
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'Choose a file to upload'}
@@ -83,6 +84,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data }) => {
)}
{(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && (
<Input
disabled={disabled}
inputParam={inputParam}
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
@@ -90,6 +92,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data }) => {
)}
{inputParam.type === 'options' && (
<Dropdown
disabled={disabled}
name={inputParam.name}
options={inputParam.options}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
@@ -106,7 +109,8 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data }) => {
NodeInputHandler.propTypes = {
inputAnchor: PropTypes.object,
inputParam: PropTypes.object,
data: PropTypes.object
data: PropTypes.object,
disabled: PropTypes.bool
}
export default NodeInputHandler
+13 -1
View File
@@ -3,7 +3,7 @@ import ReactFlow, { addEdge, Controls, Background, useNodesState, useEdgesState
import 'reactflow/dist/style.css'
import { useDispatch, useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import { useNavigate, useLocation } from 'react-router-dom'
import { usePrompt } from '../../utils/usePrompt'
import {
REMOVE_DIRTY,
@@ -50,6 +50,9 @@ const Canvas = () => {
const theme = useTheme()
const navigate = useNavigate()
const { state } = useLocation()
const templateFlowData = state ? state.templateFlowData : ''
const URLpath = document.location.pathname.toString().split('/')
const chatflowId = URLpath[URLpath.length - 1] === 'canvas' ? '' : URLpath[URLpath.length - 1]
@@ -59,6 +62,7 @@ const Canvas = () => {
const canvas = useSelector((state) => state.canvas)
const [canvasDataStore, setCanvasDataStore] = useState(canvas)
const [chatflow, setChatflow] = useState(null)
const { reactFlowInstance, setReactFlowInstance } = useContext(flowContext)
// ==============================|| Snackbar ||============================== //
@@ -437,6 +441,14 @@ const Canvas = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (templateFlowData && templateFlowData.includes('"nodes":[') && templateFlowData.includes('],"edges":[')) {
handleLoadFlow(templateFlowData)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [templateFlowData])
usePrompt('You have unsaved changes! Do you want to navigate away?', canvasDataStore.isDirty)
return (
+4 -1
View File
@@ -22,6 +22,9 @@ import useApi from 'hooks/useApi'
// const
import { baseURL } from 'store/constant'
// icons
import { IconPlus } from '@tabler/icons'
// ==============================|| CHATFLOWS ||============================== //
const Chatflows = () => {
@@ -83,7 +86,7 @@ const Chatflows = () => {
<Grid sx={{ mb: 1.25 }} container direction='row'>
<Box sx={{ flexGrow: 1 }} />
<Grid item>
<StyledButton variant='contained' sx={{ color: 'white' }} onClick={addNew}>
<StyledButton variant='contained' sx={{ color: 'white' }} onClick={addNew} startIcon={<IconPlus />}>
Add New
</StyledButton>
</Grid>
@@ -0,0 +1,105 @@
import { useEffect, useRef } from 'react'
import ReactFlow, { Controls, Background, useNodesState, useEdgesState } from 'reactflow'
import 'reactflow/dist/style.css'
import 'views/canvas/index.css'
import { useLocation, useNavigate } from 'react-router-dom'
// material-ui
import { Toolbar, Box, AppBar } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
import MarketplaceCanvasNode from './MarketplaceCanvasNode'
import MarketplaceCanvasHeader from './MarketplaceCanvasHeader'
const nodeTypes = { customNode: MarketplaceCanvasNode }
const edgeTypes = { buttonedge: '' }
// ==============================|| CANVAS ||============================== //
const MarketplaceCanvas = () => {
const theme = useTheme()
const navigate = useNavigate()
const { state } = useLocation()
const { flowData, name } = state
// ==============================|| ReactFlow ||============================== //
const [nodes, setNodes, onNodesChange] = useNodesState()
const [edges, setEdges, onEdgesChange] = useEdgesState()
const reactFlowWrapper = useRef(null)
// ==============================|| useEffect ||============================== //
useEffect(() => {
if (flowData) {
const initialFlow = JSON.parse(flowData)
setNodes(initialFlow.nodes || [])
setEdges(initialFlow.edges || [])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [flowData])
const onChatflowCopy = (flowData) => {
//navigator.clipboard.writeText(JSON.stringify(flowData))
const templateFlowData = JSON.stringify(flowData)
navigate(`/canvas`, { state: { templateFlowData } })
}
return (
<>
<Box>
<AppBar
enableColorOnDark
position='fixed'
color='inherit'
elevation={1}
sx={{
bgcolor: theme.palette.background.default
}}
>
<Toolbar>
<MarketplaceCanvasHeader
flowName={name}
flowData={JSON.parse(flowData)}
onChatflowCopy={(flowData) => onChatflowCopy(flowData)}
/>
</Toolbar>
</AppBar>
<Box sx={{ pt: '70px', height: '100vh', width: '100%' }}>
<div className='reactflow-parent-wrapper'>
<div className='reactflow-wrapper' ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodesDraggable={false}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
fitView
>
<Controls
style={{
display: 'flex',
flexDirection: 'row',
left: '50%',
transform: 'translate(-50%, -50%)'
}}
/>
<Background color='#aaa' gap={16} />
</ReactFlow>
</div>
</div>
</Box>
</Box>
</>
)
}
export default MarketplaceCanvas
@@ -0,0 +1,76 @@
import PropTypes from 'prop-types'
import { useNavigate } from 'react-router-dom'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Avatar, Box, ButtonBase, Typography, Stack } from '@mui/material'
import { StyledButton } from 'ui-component/button/StyledButton'
// icons
import { IconCopy, IconChevronLeft } from '@tabler/icons'
// ==============================|| CANVAS HEADER ||============================== //
const MarketplaceCanvasHeader = ({ flowName, flowData, onChatflowCopy }) => {
const theme = useTheme()
const navigate = useNavigate()
return (
<>
<Box>
<ButtonBase title='Back' sx={{ borderRadius: '50%' }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.secondary.light,
color: theme.palette.secondary.dark,
'&:hover': {
background: theme.palette.secondary.dark,
color: theme.palette.secondary.light
}
}}
color='inherit'
onClick={() => navigate(-1)}
>
<IconChevronLeft stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
</Box>
<Box sx={{ flexGrow: 1 }}>
<Stack flexDirection='row'>
<Typography
sx={{
fontSize: '1.5rem',
fontWeight: 600,
ml: 2
}}
>
{flowName}
</Typography>
</Stack>
</Box>
<Box>
<StyledButton
color='secondary'
variant='contained'
title='Use Chatflow'
onClick={() => onChatflowCopy(flowData)}
startIcon={<IconCopy />}
>
Use Template
</StyledButton>
</Box>
</>
)
}
MarketplaceCanvasHeader.propTypes = {
flowName: PropTypes.string,
flowData: PropTypes.object,
onChatflowCopy: PropTypes.func
}
export default MarketplaceCanvasHeader
@@ -0,0 +1,123 @@
import PropTypes from 'prop-types'
// material-ui
import { styled, useTheme } from '@mui/material/styles'
import { Box, Typography, Divider } from '@mui/material'
// project imports
import MainCard from 'ui-component/cards/MainCard'
import NodeInputHandler from 'views/canvas/NodeInputHandler'
import NodeOutputHandler from 'views/canvas/NodeOutputHandler'
// const
import { baseURL } from 'store/constant'
const CardWrapper = styled(MainCard)(({ theme }) => ({
background: theme.palette.card.main,
color: theme.darkTextPrimary,
border: 'solid 1px',
borderColor: theme.palette.primary[200] + 75,
width: '300px',
height: 'auto',
padding: '10px',
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
'&:hover': {
borderColor: theme.palette.primary.main
}
}))
// ===========================|| CANVAS NODE ||=========================== //
const MarketplaceCanvasNode = ({ data }) => {
const theme = useTheme()
return (
<>
<CardWrapper
content={false}
sx={{
padding: 0,
borderColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary
}}
border={false}
>
<Box>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Box style={{ width: 50, marginRight: 10, padding: 5 }}>
<div
style={{
...theme.typography.commonAvatar,
...theme.typography.largeAvatar,
borderRadius: '50%',
backgroundColor: 'white',
cursor: 'grab'
}}
>
<img
style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }}
src={`${baseURL}/api/v1/node-icon/${data.name}`}
alt='Notification'
/>
</div>
</Box>
<Box>
<Typography
sx={{
fontSize: '1rem',
fontWeight: 500
}}
>
{data.label}
</Typography>
</Box>
</div>
{(data.inputAnchors.length > 0 || data.inputParams.length > 0) && (
<>
<Divider />
<Box sx={{ background: theme.palette.asyncSelect.main, p: 1 }}>
<Typography
sx={{
fontWeight: 500,
textAlign: 'center'
}}
>
Inputs
</Typography>
</Box>
<Divider />
</>
)}
{data.inputAnchors.map((inputAnchor, index) => (
<NodeInputHandler key={index} inputAnchor={inputAnchor} data={data} />
))}
{data.inputParams.map((inputParam, index) => (
<NodeInputHandler disabled={true} key={index} inputParam={inputParam} data={data} />
))}
<Divider />
<Box sx={{ background: theme.palette.asyncSelect.main, p: 1 }}>
<Typography
sx={{
fontWeight: 500,
textAlign: 'center'
}}
>
Output
</Typography>
</Box>
<Divider />
{data.outputAnchors.map((outputAnchor, index) => (
<NodeOutputHandler key={index} outputAnchor={outputAnchor} data={data} />
))}
</Box>
</CardWrapper>
</>
)
}
MarketplaceCanvasNode.propTypes = {
data: PropTypes.object
}
export default MarketplaceCanvasNode
+101
View File
@@ -0,0 +1,101 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
// material-ui
import { Grid, Box, Stack } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
import MainCard from 'ui-component/cards/MainCard'
import ItemCard from 'ui-component/cards/ItemCard'
import { gridSpacing } from 'store/constant'
import WorkflowEmptySVG from 'assets/images/workflow_empty.svg'
// API
import marketplacesApi from 'api/marketplaces'
// Hooks
import useApi from 'hooks/useApi'
// const
import { baseURL } from 'store/constant'
// ==============================|| Marketplace ||============================== //
const Marketplace = () => {
const navigate = useNavigate()
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const [isLoading, setLoading] = useState(true)
const [images, setImages] = useState({})
const getAllMarketplacesApi = useApi(marketplacesApi.getAllMarketplaces)
const goToCanvas = (selectedChatflow) => {
navigate(`/marketplace/${selectedChatflow.id}`, { state: selectedChatflow })
}
useEffect(() => {
getAllMarketplacesApi.request()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
setLoading(getAllMarketplacesApi.loading)
}, [getAllMarketplacesApi.loading])
useEffect(() => {
if (getAllMarketplacesApi.data) {
try {
const chatflows = getAllMarketplacesApi.data
const images = {}
for (let i = 0; i < chatflows.length; i += 1) {
const flowDataStr = chatflows[i].flowData
const flowData = JSON.parse(flowDataStr)
const nodes = flowData.nodes || []
images[chatflows[i].id] = []
for (let j = 0; j < nodes.length; j += 1) {
const imageSrc = `${baseURL}/api/v1/node-icon/${nodes[j].data.name}`
if (!images[chatflows[i].id].includes(imageSrc)) {
images[chatflows[i].id].push(imageSrc)
}
}
}
setImages(images)
} catch (e) {
console.error(e)
}
}
}, [getAllMarketplacesApi.data])
return (
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
<Stack flexDirection='row'>
<h1>Marketplace</h1>
</Stack>
<Grid container spacing={gridSpacing}>
{!isLoading &&
getAllMarketplacesApi.data &&
getAllMarketplacesApi.data.map((data, index) => (
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
</Grid>
))}
</Grid>
{!isLoading && (!getAllMarketplacesApi.data || getAllMarketplacesApi.data.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} src={WorkflowEmptySVG} alt='WorkflowEmptySVG' />
</Box>
<div>No Marketplace Yet</div>
</Stack>
)}
</MainCard>
)
}
export default Marketplace