add credentials

This commit is contained in:
Henry
2023-07-15 14:25:52 +01:00
parent aee0a51f73
commit 413d654493
37 changed files with 1858 additions and 126 deletions
+28
View File
@@ -0,0 +1,28 @@
import client from './client'
const getAllCredentials = () => client.get('/credentials')
const getCredentialsByName = (componentCredentialName) => client.get(`/credentials?credentialName=${componentCredentialName}`)
const getAllComponentsCredentials = () => client.get('/components-credentials')
const getSpecificCredential = (id) => client.get(`/credentials/${id}`)
const getSpecificComponentCredential = (name) => client.get(`/components-credentials/${name}`)
const createCredential = (body) => client.post(`/credentials`, body)
const updateCredential = (id, body) => client.put(`/credentials/${id}`, body)
const deleteCredential = (id) => client.delete(`/credentials/${id}`)
export default {
getAllCredentials,
getCredentialsByName,
getAllComponentsCredentials,
getSpecificCredential,
getSpecificComponentCredential,
createCredential,
updateCredential,
deleteCredential
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.2 KiB

+10 -2
View File
@@ -1,8 +1,8 @@
// assets
import { IconHierarchy, IconBuildingStore, IconKey, IconTool } from '@tabler/icons'
import { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock } from '@tabler/icons'
// constant
const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool }
const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock }
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
@@ -35,6 +35,14 @@ const dashboard = {
icon: icons.IconTool,
breadcrumbs: true
},
{
id: 'credentials',
title: 'Credentials',
type: 'item',
url: '/credentials',
icon: icons.IconLock,
breadcrumbs: true
},
{
id: 'apikey',
title: 'API Keys',
+8 -1
View File
@@ -13,9 +13,12 @@ const Marketplaces = Loadable(lazy(() => import('views/marketplaces')))
// apikey routing
const APIKey = Loadable(lazy(() => import('views/apikey')))
// apikey routing
// tools routing
const Tools = Loadable(lazy(() => import('views/tools')))
// credentials routing
const Credentials = Loadable(lazy(() => import('views/credentials')))
// ==============================|| MAIN ROUTING ||============================== //
const MainRoutes = {
@@ -41,6 +44,10 @@ const MainRoutes = {
{
path: '/tools',
element: <Tools />
},
{
path: '/credentials',
element: <Credentials />
}
]
}
+1
View File
@@ -5,3 +5,4 @@ export const appDrawerWidth = 320
export const maxScroll = 100000
export const baseURL = process.env.NODE_ENV === 'production' ? window.location.origin : window.location.origin.replace(':8080', ':3000')
export const uiBaseURL = window.location.origin
export const FLOWISE_CREDENTIAL_ID = 'FLOWISE_CREDENTIAL_ID'
@@ -1,13 +1,17 @@
import { useState, useEffect, Fragment } from 'react'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import axios from 'axios'
// Material
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
import { Popper, CircularProgress, TextField, Box, Typography } from '@mui/material'
import { styled } from '@mui/material/styles'
// API
import credentialsApi from 'api/credentials'
// const
import { baseURL } from 'store/constant'
const StyledPopper = styled(Popper)({
@@ -49,6 +53,7 @@ export const AsyncDropdown = ({
onSelect,
isCreateNewOption,
onCreateNew,
credentialNames = [],
disabled = false,
disableClearable = false
}) => {
@@ -62,11 +67,36 @@ export const AsyncDropdown = ({
const addNewOption = [{ label: '- Create New -', name: '-create-' }]
let [internalValue, setInternalValue] = useState(value ?? 'choose an option')
const fetchCredentialList = async () => {
try {
let names = ''
if (credentialNames.length > 1) {
names = credentialNames.join('&credentialName=')
} else {
names = credentialNames[0]
}
const resp = await credentialsApi.getCredentialsByName(names)
if (resp.data) {
const returnList = []
for (let i = 0; i < resp.data.length; i += 1) {
const data = {
label: resp.data[i].name,
name: resp.data[i].id
}
returnList.push(data)
}
return returnList
}
} catch (error) {
console.error(error)
}
}
useEffect(() => {
setLoading(true)
;(async () => {
const fetchData = async () => {
let response = await fetchList({ name, nodeData })
let response = credentialNames.length ? await fetchCredentialList() : await fetchList({ name, nodeData })
if (isCreateNewOption) setOptions([...response, ...addNewOption])
else setOptions([...response])
setLoading(false)
@@ -142,6 +172,7 @@ AsyncDropdown.propTypes = {
onSelect: PropTypes.func,
onCreateNew: PropTypes.func,
disabled: PropTypes.bool,
credentialNames: PropTypes.array,
disableClearable: PropTypes.bool,
isCreateNewOption: PropTypes.bool
}
+1 -1
View File
@@ -37,7 +37,7 @@ export const Input = ({ inputParam, value, onChange, disabled = false, showDialo
onChange(e.target.value)
}}
inputProps={{
step: 0.1,
step: inputParam.step ?? 0.1,
style: {
height: inputParam.rows ? '90px' : 'inherit'
}
+18 -1
View File
@@ -41,6 +41,7 @@ export const initNode = (nodeData, newNodeId) => {
const whitelistTypes = ['asyncOptions', 'options', 'string', 'number', 'boolean', 'password', 'json', 'code', 'date', 'file', 'folder']
// Inputs
for (let i = 0; i < incoming; i += 1) {
const newInput = {
...nodeData.inputs[i],
@@ -53,6 +54,16 @@ export const initNode = (nodeData, newNodeId) => {
}
}
// Credential
if (nodeData.credential) {
const newInput = {
...nodeData.credential,
id: `${newNodeId}-input-${nodeData.credential.name}-${nodeData.credential.type}`
}
inputParams.unshift(newInput)
}
// Outputs
const outputAnchors = []
for (let i = 0; i < outgoing; i += 1) {
if (nodeData.outputs && nodeData.outputs.length) {
@@ -129,6 +140,8 @@ export const initNode = (nodeData, newNodeId) => {
}
]
*/
// Inputs
if (nodeData.inputs) {
nodeData.inputAnchors = inputAnchors
nodeData.inputParams = inputParams
@@ -139,13 +152,17 @@ export const initNode = (nodeData, newNodeId) => {
nodeData.inputs = {}
}
// Outputs
if (nodeData.outputs) {
nodeData.outputs = initializeDefaultNodeData(outputAnchors)
} else {
nodeData.outputs = {}
}
nodeData.outputAnchors = outputAnchors
// Credential
if (nodeData.credential) nodeData.credential = ''
nodeData.id = newNodeId
return nodeData
@@ -0,0 +1,149 @@
import PropTypes from 'prop-types'
import { useRef, useState } from 'react'
// material-ui
import { IconButton } from '@mui/material'
import { IconEdit } from '@tabler/icons'
// project import
import { AsyncDropdown } from 'ui-component/dropdown/AsyncDropdown'
import AddEditCredentialDialog from 'views/credentials/AddEditCredentialDialog'
import CredentialListDialog from 'views/credentials/CredentialListDialog'
// API
import credentialsApi from 'api/credentials'
// ===========================|| CredentialInputHandler ||=========================== //
const CredentialInputHandler = ({ inputParam, data, onSelect, disabled = false }) => {
const ref = useRef(null)
const [credentialId, setCredentialId] = useState(data?.credential ?? '')
const [showCredentialListDialog, setShowCredentialListDialog] = useState(false)
const [credentialListDialogProps, setCredentialListDialogProps] = useState({})
const [showSpecificCredentialDialog, setShowSpecificCredentialDialog] = useState(false)
const [specificCredentialDialogProps, setSpecificCredentialDialogProps] = useState({})
const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString())
const editCredential = (credentialId) => {
const dialogProp = {
type: 'EDIT',
cancelButtonName: 'Cancel',
confirmButtonName: 'Save',
credentialId
}
setSpecificCredentialDialogProps(dialogProp)
setShowSpecificCredentialDialog(true)
}
const addAsyncOption = async () => {
try {
let names = ''
if (inputParam.credentialNames.length > 1) {
names = inputParam.credentialNames.join('&')
} else {
names = inputParam.credentialNames[0]
}
const componentCredentialsResp = await credentialsApi.getSpecificComponentCredential(names)
if (componentCredentialsResp.data) {
if (Array.isArray(componentCredentialsResp.data)) {
const dialogProp = {
title: 'Add New Credential',
componentsCredentials: componentCredentialsResp.data
}
setCredentialListDialogProps(dialogProp)
setShowCredentialListDialog(true)
} else {
const dialogProp = {
type: 'ADD',
cancelButtonName: 'Cancel',
confirmButtonName: 'Add',
credentialComponent: componentCredentialsResp.data
}
setSpecificCredentialDialogProps(dialogProp)
setShowSpecificCredentialDialog(true)
}
}
} catch (error) {
console.error(error)
}
}
const onConfirmAsyncOption = (selectedCredentialId = '') => {
setCredentialId(selectedCredentialId)
setReloadTimestamp(Date.now().toString())
setSpecificCredentialDialogProps({})
setShowSpecificCredentialDialog(false)
onSelect(selectedCredentialId)
}
const onCredentialSelected = (credentialComponent) => {
setShowCredentialListDialog(false)
const dialogProp = {
type: 'ADD',
cancelButtonName: 'Cancel',
confirmButtonName: 'Add',
credentialComponent
}
setSpecificCredentialDialogProps(dialogProp)
setShowSpecificCredentialDialog(true)
}
return (
<div ref={ref}>
{inputParam && (
<>
{inputParam.type === 'credential' && (
<>
<div style={{ marginTop: 10 }} />
<div key={reloadTimestamp} style={{ display: 'flex', flexDirection: 'row' }}>
<AsyncDropdown
disabled={disabled}
name={inputParam.name}
nodeData={data}
value={credentialId ?? 'choose an option'}
isCreateNewOption={true}
credentialNames={inputParam.credentialNames}
onSelect={(newValue) => {
setCredentialId(newValue)
onSelect(newValue)
}}
onCreateNew={() => addAsyncOption(inputParam.name)}
/>
{credentialId && (
<IconButton title='Edit' color='primary' size='small' onClick={() => editCredential(credentialId)}>
<IconEdit />
</IconButton>
)}
</div>
</>
)}
</>
)}
{showSpecificCredentialDialog && (
<AddEditCredentialDialog
show={showSpecificCredentialDialog}
dialogProps={specificCredentialDialogProps}
onCancel={() => setShowSpecificCredentialDialog(false)}
onConfirm={onConfirmAsyncOption}
></AddEditCredentialDialog>
)}
{showCredentialListDialog && (
<CredentialListDialog
show={showCredentialListDialog}
dialogProps={credentialListDialogProps}
onCancel={() => setShowCredentialListDialog(false)}
onCredentialSelected={onCredentialSelected}
></CredentialListDialog>
)}
</div>
)
}
CredentialInputHandler.propTypes = {
inputParam: PropTypes.object,
data: PropTypes.object,
onSelect: PropTypes.func,
disabled: PropTypes.bool
}
export default CredentialInputHandler
@@ -21,9 +21,14 @@ import { JsonEditorInput } from 'ui-component/json/JsonEditor'
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
import ToolDialog from 'views/tools/ToolDialog'
import FormatPromptValuesDialog from 'ui-component/dialog/FormatPromptValuesDialog'
import CredentialInputHandler from './CredentialInputHandler'
// utils
import { getInputVariables } from 'utils/genericHelper'
// const
import { FLOWISE_CREDENTIAL_ID } from 'store/constant'
const EDITABLE_TOOLS = ['selectedTool']
const CustomWidthTooltip = styled(({ className, ...props }) => <Tooltip {...props} classes={{ popper: className }} />)({
@@ -226,6 +231,17 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
<span style={{ color: 'rgb(116,66,16)', marginLeft: 10 }}>{inputParam.warning}</span>
</div>
)}
{inputParam.type === 'credential' && (
<CredentialInputHandler
disabled={disabled}
data={data}
inputParam={inputParam}
onSelect={(newValue) => {
data.credential = newValue
data.inputs[FLOWISE_CREDENTIAL_ID] = newValue // in case data.credential is not updated
}}
/>
)}
{inputParam.type === 'file' && (
<File
disabled={disabled}
+17 -9
View File
@@ -12,6 +12,7 @@ import {
enqueueSnackbar as enqueueSnackbarAction,
closeSnackbar as closeSnackbarAction
} from 'store/actions'
import { omit, cloneDeep } from 'lodash'
// material-ui
import { Toolbar, Box, AppBar, Button } from '@mui/material'
@@ -41,6 +42,9 @@ import { IconX } from '@tabler/icons'
import { getUniqueNodeId, initNode, getEdgeLabelName, rearrangeToolsOrdering } from 'utils/genericHelper'
import useNotifier from 'utils/useNotifier'
// const
import { FLOWISE_CREDENTIAL_ID } from 'store/constant'
const nodeTypes = { customNode: CanvasNode }
const edgeTypes = { buttonedge: ButtonEdge }
@@ -185,17 +189,21 @@ const Canvas = () => {
const handleSaveFlow = (chatflowName) => {
if (reactFlowInstance) {
setNodes((nds) =>
nds.map((node) => {
node.data = {
...node.data,
selected: false
}
return node
})
)
const nodes = reactFlowInstance.getNodes().map((node) => {
const nodeData = cloneDeep(node.data)
if (Object.prototype.hasOwnProperty.call(nodeData.inputs, FLOWISE_CREDENTIAL_ID)) {
nodeData.credential = nodeData.inputs[FLOWISE_CREDENTIAL_ID]
nodeData.inputs = omit(nodeData.inputs, [FLOWISE_CREDENTIAL_ID])
}
node.data = {
...nodeData,
selected: false
}
return node
})
const rfInstanceObject = reactFlowInstance.toObject()
rfInstanceObject.nodes = nodes
const flowData = JSON.stringify(rfInstanceObject)
if (!chatflow.id) {
@@ -0,0 +1,276 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
import parser from 'html-react-parser'
// Material
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Box, Stack, OutlinedInput, Typography } from '@mui/material'
// Project imports
import { StyledButton } from 'ui-component/button/StyledButton'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
import CredentialInputHandler from './CredentialInputHandler'
// Icons
import { IconX } from '@tabler/icons'
// API
import credentialsApi from 'api/credentials'
// Hooks
import useApi from 'hooks/useApi'
// utils
import useNotifier from 'utils/useNotifier'
// const
import { baseURL } from 'store/constant'
const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
// ==============================|| Snackbar ||============================== //
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const getSpecificCredentialApi = useApi(credentialsApi.getSpecificCredential)
const getSpecificComponentCredentialApi = useApi(credentialsApi.getSpecificComponentCredential)
const [credential, setCredential] = useState({})
const [name, setName] = useState('')
const [credentialData, setCredentialData] = useState({})
const [componentCredential, setComponentCredential] = useState({})
useEffect(() => {
if (getSpecificCredentialApi.data) {
setCredential(getSpecificCredentialApi.data)
if (getSpecificCredentialApi.data.name) {
setName(getSpecificCredentialApi.data.name)
}
if (getSpecificCredentialApi.data.plainDataObj) {
setCredentialData(getSpecificCredentialApi.data.plainDataObj)
}
getSpecificComponentCredentialApi.request(getSpecificCredentialApi.data.credentialName)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getSpecificCredentialApi.data])
useEffect(() => {
if (getSpecificComponentCredentialApi.data) {
setComponentCredential(getSpecificComponentCredentialApi.data)
}
}, [getSpecificComponentCredentialApi.data])
useEffect(() => {
if (dialogProps.type === 'EDIT' && dialogProps.data) {
// When credential dialog is opened from Credentials dashboard
getSpecificCredentialApi.request(dialogProps.data.id)
} else if (dialogProps.type === 'EDIT' && dialogProps.credentialId) {
// When credential dialog is opened from node in canvas
getSpecificCredentialApi.request(dialogProps.credentialId)
} else if (dialogProps.type === 'ADD' && dialogProps.credentialComponent) {
// When credential dialog is to add a new credential
setName('')
setCredential({})
setCredentialData({})
setComponentCredential(dialogProps.credentialComponent)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dialogProps])
const addNewCredential = async () => {
try {
const obj = {
name,
credentialName: componentCredential.name,
plainDataObj: credentialData
}
const createResp = await credentialsApi.createCredential(obj)
if (createResp.data) {
enqueueSnackbar({
message: 'New Credential added',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm(createResp.data.id)
}
} catch (error) {
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
message: `Failed to add new Credential: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onCancel()
}
}
const saveCredential = async () => {
try {
const saveResp = await credentialsApi.updateCredential(credential.id, {
name,
credentialName: componentCredential.name,
plainDataObj: credentialData
})
if (saveResp.data) {
enqueueSnackbar({
message: 'Credential saved',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm(saveResp.data.id)
}
} catch (error) {
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
message: `Failed to save Credential: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onCancel()
}
}
const component = show ? (
<Dialog
fullWidth
maxWidth='sm'
open={show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{componentCredential && componentCredential.label && (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<div
style={{
width: 50,
height: 50,
marginRight: 10,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 7,
borderRadius: '50%',
objectFit: 'contain'
}}
alt={componentCredential.name}
src={`${baseURL}/api/v1/components-credentials-icon/${componentCredential.name}`}
/>
</div>
{componentCredential.label}
</div>
)}
</DialogTitle>
<DialogContent>
{componentCredential && componentCredential.description && (
<Box sx={{ pl: 2, pr: 2 }}>
<div
style={{
display: 'flex',
flexDirection: 'row',
borderRadius: 10,
background: 'rgb(254,252,191)',
padding: 10,
marginTop: 10,
marginBottom: 10
}}
>
<span style={{ color: 'rgb(116,66,16)' }}>{parser(componentCredential.description)}</span>
</div>
</Box>
)}
{componentCredential && componentCredential.label && (
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Credential Name
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<OutlinedInput
id='credName'
type='string'
fullWidth
placeholder={componentCredential.label}
value={name}
name='name'
onChange={(e) => setName(e.target.value)}
/>
</Box>
)}
{componentCredential &&
componentCredential.inputs &&
componentCredential.inputs.map((inputParam, index) => (
<CredentialInputHandler key={index} inputParam={inputParam} data={credentialData} />
))}
</DialogContent>
<DialogActions>
<StyledButton
disabled={!name}
variant='contained'
onClick={() => (dialogProps.type === 'ADD' ? addNewCredential() : saveCredential())}
>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
<ConfirmDialog />
</Dialog>
) : null
return createPortal(component, portalElement)
}
AddEditCredentialDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}
export default AddEditCredentialDialog
@@ -0,0 +1,137 @@
import PropTypes from 'prop-types'
import { useRef, useState } from 'react'
import { useSelector } from 'react-redux'
// material-ui
import { Box, Typography, IconButton } from '@mui/material'
import { IconArrowsMaximize, IconAlertTriangle } from '@tabler/icons'
// project import
import { Dropdown } from 'ui-component/dropdown/Dropdown'
import { Input } from 'ui-component/input/Input'
import { SwitchInput } from 'ui-component/switch/Switch'
import { JsonEditorInput } from 'ui-component/json/JsonEditor'
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
// ===========================|| NodeInputHandler ||=========================== //
const CredentialInputHandler = ({ inputParam, data, disabled = false }) => {
const customization = useSelector((state) => state.customization)
const ref = useRef(null)
const [showExpandDialog, setShowExpandDialog] = useState(false)
const [expandDialogProps, setExpandDialogProps] = useState({})
const onExpandDialogClicked = (value, inputParam) => {
const dialogProp = {
value,
inputParam,
disabled,
confirmButtonName: 'Save',
cancelButtonName: 'Cancel'
}
setExpandDialogProps(dialogProp)
setShowExpandDialog(true)
}
const onExpandDialogSave = (newValue, inputParamName) => {
setShowExpandDialog(false)
data[inputParamName] = newValue
}
return (
<div ref={ref}>
{inputParam && (
<>
<Box sx={{ p: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
{inputParam.label}
{!inputParam.optional && <span style={{ color: 'red' }}>&nbsp;*</span>}
{inputParam.description && <TooltipWithParser style={{ marginLeft: 10 }} title={inputParam.description} />}
</Typography>
<div style={{ flexGrow: 1 }}></div>
{inputParam.type === 'string' && inputParam.rows && (
<IconButton
size='small'
sx={{
height: 25,
width: 25
}}
title='Expand'
color='primary'
onClick={() => onExpandDialogClicked(data[inputParam.name] ?? inputParam.default ?? '', inputParam)}
>
<IconArrowsMaximize />
</IconButton>
)}
</div>
{inputParam.warning && (
<div
style={{
display: 'flex',
flexDirection: 'row',
borderRadius: 10,
background: 'rgb(254,252,191)',
padding: 10,
marginTop: 10,
marginBottom: 10
}}
>
<IconAlertTriangle size={36} color='orange' />
<span style={{ color: 'rgb(116,66,16)', marginLeft: 10 }}>{inputParam.warning}</span>
</div>
)}
{inputParam.type === 'boolean' && (
<SwitchInput
disabled={disabled}
onChange={(newValue) => (data[inputParam.name] = newValue)}
value={data[inputParam.name] ?? inputParam.default ?? false}
/>
)}
{(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && (
<Input
key={data[inputParam.name]}
disabled={disabled}
inputParam={inputParam}
onChange={(newValue) => (data[inputParam.name] = newValue)}
value={data[inputParam.name] ?? inputParam.default ?? ''}
showDialog={showExpandDialog}
dialogProps={expandDialogProps}
onDialogCancel={() => setShowExpandDialog(false)}
onDialogConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)}
/>
)}
{inputParam.type === 'json' && (
<JsonEditorInput
disabled={disabled}
onChange={(newValue) => (data[inputParam.name] = newValue)}
value={data[inputParam.name] ?? inputParam.default ?? ''}
isDarkMode={customization.isDarkMode}
/>
)}
{inputParam.type === 'options' && (
<Dropdown
disabled={disabled}
name={inputParam.name}
options={inputParam.options}
onSelect={(newValue) => (data[inputParam.name] = newValue)}
value={data[inputParam.name] ?? inputParam.default ?? 'choose an option'}
/>
)}
</Box>
</>
)}
</div>
)
}
CredentialInputHandler.propTypes = {
inputAnchor: PropTypes.object,
inputParam: PropTypes.object,
data: PropTypes.object,
disabled: PropTypes.bool
}
export default CredentialInputHandler
@@ -0,0 +1,172 @@
import { useState, useEffect } from 'react'
import { createPortal } from 'react-dom'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import {
List,
ListItemButton,
ListItem,
ListItemAvatar,
ListItemText,
Dialog,
DialogContent,
DialogTitle,
Box,
OutlinedInput,
InputAdornment
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { IconSearch, IconX } from '@tabler/icons'
// const
import { baseURL } from 'store/constant'
const CredentialListDialog = ({ show, dialogProps, onCancel, onCredentialSelected }) => {
const portalElement = document.getElementById('portal')
const customization = useSelector((state) => state.customization)
const theme = useTheme()
const [searchValue, setSearchValue] = useState('')
const [componentsCredentials, setComponentsCredentials] = useState([])
const filterSearch = (value) => {
setSearchValue(value)
setTimeout(() => {
if (value) {
const searchData = dialogProps.componentsCredentials.filter((crd) => crd.name.toLowerCase().includes(value.toLowerCase()))
setComponentsCredentials(searchData)
} else if (value === '') {
setComponentsCredentials(dialogProps.componentsCredentials)
}
// scrollTop()
}, 500)
}
useEffect(() => {
if (show && dialogProps.componentsCredentials) {
setComponentsCredentials(dialogProps.componentsCredentials)
}
}, [show, dialogProps])
const component = show ? (
<Dialog
fullWidth
maxWidth='xs'
open={show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{dialogProps.title}
<Box sx={{ p: 2 }}>
<OutlinedInput
sx={{ width: '100%', pr: 2, pl: 2, my: 2 }}
id='input-search-credential'
value={searchValue}
onChange={(e) => filterSearch(e.target.value)}
placeholder='Search credential'
startAdornment={
<InputAdornment position='start'>
<IconSearch stroke={1.5} size='1rem' color={theme.palette.grey[500]} />
</InputAdornment>
}
endAdornment={
<InputAdornment
position='end'
sx={{
cursor: 'pointer',
color: theme.palette.grey[500],
'&:hover': {
color: theme.palette.grey[900]
}
}}
title='Clear Search'
>
<IconX
stroke={1.5}
size='1rem'
onClick={() => filterSearch('')}
style={{
cursor: 'pointer'
}}
/>
</InputAdornment>
}
aria-describedby='search-helper-text'
inputProps={{
'aria-label': 'weight'
}}
/>
</Box>
</DialogTitle>
<DialogContent>
<List
sx={{
width: '100%',
py: 0,
borderRadius: '10px',
[theme.breakpoints.down('md')]: {
maxWidth: 370
},
'& .MuiListItemSecondaryAction-root': {
top: 22
},
'& .MuiDivider-root': {
my: 0
},
'& .list-container': {
pl: 7
}
}}
>
{[...componentsCredentials].map((componentCredential) => (
<div key={componentCredential.name}>
<ListItemButton
onClick={() => onCredentialSelected(componentCredential)}
sx={{ p: 0, borderRadius: `${customization.borderRadius}px` }}
>
<ListItem alignItems='center'>
<ListItemAvatar>
<div
style={{
width: 50,
height: 50,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 7,
borderRadius: '50%',
objectFit: 'contain'
}}
alt={componentCredential.name}
src={`${baseURL}/api/v1/components-credentials-icon/${componentCredential.name}`}
/>
</div>
</ListItemAvatar>
<ListItemText sx={{ ml: 1 }} primary={componentCredential.label} />
</ListItem>
</ListItemButton>
</div>
))}
</List>
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
CredentialListDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onCredentialSelected: PropTypes.func
}
export default CredentialListDialog
+274
View File
@@ -0,0 +1,274 @@
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
import moment from 'moment'
// material-ui
import { Button, Box, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
import MainCard from 'ui-component/cards/MainCard'
import { StyledButton } from 'ui-component/button/StyledButton'
import CredentialListDialog from './CredentialListDialog'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
import AddEditCredentialDialog from './AddEditCredentialDialog'
// API
import credentialsApi from 'api/credentials'
// Hooks
import useApi from 'hooks/useApi'
import useConfirm from 'hooks/useConfirm'
// utils
import useNotifier from 'utils/useNotifier'
// Icons
import { IconTrash, IconEdit, IconX, IconPlus } from '@tabler/icons'
import CredentialEmptySVG from 'assets/images/credential_empty.svg'
// const
import { baseURL } from 'store/constant'
// ==============================|| Credentials ||============================== //
const Credentials = () => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const dispatch = useDispatch()
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [showCredentialListDialog, setShowCredentialListDialog] = useState(false)
const [credentialListDialogProps, setCredentialListDialogProps] = useState({})
const [showSpecificCredentialDialog, setShowSpecificCredentialDialog] = useState(false)
const [specificCredentialDialogProps, setSpecificCredentialDialogProps] = useState({})
const [credentials, setCredentials] = useState([])
const [componentsCredentials, setComponentsCredentials] = useState([])
const { confirm } = useConfirm()
const getAllCredentialsApi = useApi(credentialsApi.getAllCredentials)
const getAllComponentsCredentialsApi = useApi(credentialsApi.getAllComponentsCredentials)
const listCredential = () => {
const dialogProp = {
title: 'Add New Credential',
componentsCredentials
}
setCredentialListDialogProps(dialogProp)
setShowCredentialListDialog(true)
}
const addNew = (credentialComponent) => {
const dialogProp = {
type: 'ADD',
cancelButtonName: 'Cancel',
confirmButtonName: 'Add',
credentialComponent
}
setSpecificCredentialDialogProps(dialogProp)
setShowSpecificCredentialDialog(true)
}
const edit = (credential) => {
const dialogProp = {
type: 'EDIT',
cancelButtonName: 'Cancel',
confirmButtonName: 'Save',
data: credential
}
setSpecificCredentialDialogProps(dialogProp)
setShowSpecificCredentialDialog(true)
}
const deleteCredential = async (credential) => {
const confirmPayload = {
title: `Delete`,
description: `Delete credential ${credential.name}?`,
confirmButtonName: 'Delete',
cancelButtonName: 'Cancel'
}
const isConfirmed = await confirm(confirmPayload)
if (isConfirmed) {
try {
const deleteResp = await credentialsApi.deleteCredential(credential.id)
if (deleteResp.data) {
enqueueSnackbar({
message: 'Credential deleted',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm()
}
} catch (error) {
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
message: `Failed to delete Credential: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onCancel()
}
}
}
const onCredentialSelected = (credentialComponent) => {
setShowCredentialListDialog(false)
addNew(credentialComponent)
}
const onConfirm = () => {
setShowCredentialListDialog(false)
setShowSpecificCredentialDialog(false)
getAllCredentialsApi.request()
}
useEffect(() => {
getAllCredentialsApi.request()
getAllComponentsCredentialsApi.request()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getAllCredentialsApi.data) {
setCredentials(getAllCredentialsApi.data)
}
}, [getAllCredentialsApi.data])
useEffect(() => {
if (getAllComponentsCredentialsApi.data) {
setComponentsCredentials(getAllComponentsCredentialsApi.data)
}
}, [getAllComponentsCredentialsApi.data])
return (
<>
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
<Stack flexDirection='row'>
<h1>Credentials&nbsp;</h1>
<Box sx={{ flexGrow: 1 }} />
<StyledButton
variant='contained'
sx={{ color: 'white', mr: 1, height: 37 }}
onClick={listCredential}
startIcon={<IconPlus />}
>
Add Credential
</StyledButton>
</Stack>
{credentials.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={CredentialEmptySVG}
alt='CredentialEmptySVG'
/>
</Box>
<div>No Credentials Yet</div>
</Stack>
)}
{credentials.length > 0 && (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Last Updated</TableCell>
<TableCell>Created</TableCell>
<TableCell> </TableCell>
<TableCell> </TableCell>
</TableRow>
</TableHead>
<TableBody>
{credentials.map((credential, index) => (
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component='th' scope='row'>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<div
style={{
width: 25,
height: 25,
marginRight: 10,
borderRadius: '50%'
}}
>
<img
style={{
width: '100%',
height: '100%',
borderRadius: '50%',
objectFit: 'contain'
}}
alt={credential.credentialName}
src={`${baseURL}/api/v1/components-credentials-icon/${credential.credentialName}`}
/>
</div>
{credential.name}
</div>
</TableCell>
<TableCell>{moment(credential.updatedDate).format('DD-MMM-YY')}</TableCell>
<TableCell>{moment(credential.createdDate).format('DD-MMM-YY')}</TableCell>
<TableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(credential)}>
<IconEdit />
</IconButton>
</TableCell>
<TableCell>
<IconButton title='Delete' color='error' onClick={() => deleteCredential(credential)}>
<IconTrash />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</MainCard>
<CredentialListDialog
show={showCredentialListDialog}
dialogProps={credentialListDialogProps}
onCancel={() => setShowCredentialListDialog(false)}
onCredentialSelected={onCredentialSelected}
></CredentialListDialog>
<AddEditCredentialDialog
show={showSpecificCredentialDialog}
dialogProps={specificCredentialDialogProps}
onCancel={() => setShowSpecificCredentialDialog(false)}
onConfirm={onConfirm}
></AddEditCredentialDialog>
<ConfirmDialog />
</>
)
}
export default Credentials