add API authorization
This commit is contained in:
parent
419d9634b4
commit
2a98c9510f
|
|
@ -9,6 +9,7 @@ export interface IChatFlow {
|
|||
id: string
|
||||
name: string
|
||||
flowData: string
|
||||
apikeyid: string
|
||||
deployed: boolean
|
||||
updatedDate: Date
|
||||
createdDate: Date
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ export class ChatFlow implements IChatFlow {
|
|||
@Column()
|
||||
flowData: string
|
||||
|
||||
@Column({ nullable: true })
|
||||
apikeyid: string
|
||||
|
||||
@Column()
|
||||
deployed: boolean
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,12 @@ import {
|
|||
getEndingNode,
|
||||
constructGraphs,
|
||||
resolveVariables,
|
||||
isStartNodeDependOnInput
|
||||
isStartNodeDependOnInput,
|
||||
getAPIKeys,
|
||||
addAPIKey,
|
||||
updateAPIKey,
|
||||
deleteAPIKey,
|
||||
compareKeys
|
||||
} from './utils'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { getDataSource } from './DataSource'
|
||||
|
|
@ -42,6 +47,9 @@ export class App {
|
|||
await this.nodesPool.initialize()
|
||||
|
||||
this.chatflowPool = new ChatflowPool()
|
||||
|
||||
// Initialize API keys
|
||||
await getAPIKeys()
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('❌[server]: Error during Data Source initialization:', err)
|
||||
|
|
@ -195,93 +203,14 @@ export class App {
|
|||
// Prediction
|
||||
// ----------------------------------------
|
||||
|
||||
// Send input message and get prediction result
|
||||
// Send input message and get prediction result (External)
|
||||
this.app.post('/api/v1/prediction/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const chatflowid = req.params.id
|
||||
const incomingInput: IncomingInput = req.body
|
||||
await this.processPrediction(req, res)
|
||||
})
|
||||
|
||||
let nodeToExecuteData: INodeData
|
||||
|
||||
/* Check if:
|
||||
* - Node Data already exists in pool
|
||||
* - Still in sync (i.e the flow has not been modified since)
|
||||
* - Flow doesn't start with nodes that depend on incomingInput.question
|
||||
***/
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(this.chatflowPool.activeChatflows, chatflowid) &&
|
||||
this.chatflowPool.activeChatflows[chatflowid].inSync &&
|
||||
!isStartNodeDependOnInput(this.chatflowPool.activeChatflows[chatflowid].startingNodes)
|
||||
) {
|
||||
nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData
|
||||
} else {
|
||||
/*** Get chatflows and prepare data ***/
|
||||
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
|
||||
id: chatflowid
|
||||
})
|
||||
if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`)
|
||||
|
||||
const flowData = chatflow.flowData
|
||||
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
|
||||
const nodes = parsedFlowData.nodes
|
||||
const edges = parsedFlowData.edges
|
||||
|
||||
/*** Get Ending Node with Directed Graph ***/
|
||||
const { graph, nodeDependencies } = constructGraphs(nodes, edges)
|
||||
const directedGraph = graph
|
||||
const endingNodeId = getEndingNode(nodeDependencies, directedGraph)
|
||||
if (!endingNodeId) return res.status(500).send(`Ending node must be either a Chain or Agent`)
|
||||
|
||||
const endingNodeData = nodes.find((nd) => nd.id === endingNodeId)?.data
|
||||
if (!endingNodeData) return res.status(500).send(`Ending node must be either a Chain or Agent`)
|
||||
|
||||
if (
|
||||
endingNodeData.outputs &&
|
||||
Object.keys(endingNodeData.outputs).length &&
|
||||
!Object.values(endingNodeData.outputs).includes(endingNodeData.name)
|
||||
) {
|
||||
return res
|
||||
.status(500)
|
||||
.send(
|
||||
`Output of ${endingNodeData.label} (${endingNodeData.id}) must be ${endingNodeData.label}, can't be an Output Prediction`
|
||||
)
|
||||
}
|
||||
|
||||
/*** Get Starting Nodes with Non-Directed Graph ***/
|
||||
const constructedObj = constructGraphs(nodes, edges, true)
|
||||
const nonDirectedGraph = constructedObj.graph
|
||||
const { startingNodeIds, depthQueue } = getStartingNodes(nonDirectedGraph, endingNodeId)
|
||||
|
||||
/*** BFS to traverse from Starting Nodes to Ending Node ***/
|
||||
const reactFlowNodes = await buildLangchain(
|
||||
startingNodeIds,
|
||||
nodes,
|
||||
graph,
|
||||
depthQueue,
|
||||
this.nodesPool.componentNodes,
|
||||
incomingInput.question
|
||||
)
|
||||
|
||||
const nodeToExecute = reactFlowNodes.find((node: IReactFlowNode) => node.id === endingNodeId)
|
||||
if (!nodeToExecute) return res.status(404).send(`Node ${endingNodeId} not found`)
|
||||
|
||||
const reactFlowNodeData: INodeData = resolveVariables(nodeToExecute.data, reactFlowNodes, incomingInput.question)
|
||||
nodeToExecuteData = reactFlowNodeData
|
||||
|
||||
const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id))
|
||||
this.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes)
|
||||
}
|
||||
|
||||
const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string
|
||||
const nodeModule = await import(nodeInstanceFilePath)
|
||||
const nodeInstance = new nodeModule.nodeClass()
|
||||
|
||||
const result = await nodeInstance.run(nodeToExecuteData, incomingInput.question, { chatHistory: incomingInput.history })
|
||||
|
||||
return res.json(result)
|
||||
} catch (e: any) {
|
||||
return res.status(500).send(e.message)
|
||||
}
|
||||
// Send input message and get prediction result (Internal)
|
||||
this.app.post('/api/v1/internal-prediction/:id', async (req: Request, res: Response) => {
|
||||
await this.processPrediction(req, res, true)
|
||||
})
|
||||
|
||||
// ----------------------------------------
|
||||
|
|
@ -308,6 +237,34 @@ export class App {
|
|||
return res.json(templates)
|
||||
})
|
||||
|
||||
// ----------------------------------------
|
||||
// API Keys
|
||||
// ----------------------------------------
|
||||
|
||||
// Get api keys
|
||||
this.app.get('/api/v1/apikey', async (req: Request, res: Response) => {
|
||||
const keys = await getAPIKeys()
|
||||
return res.json(keys)
|
||||
})
|
||||
|
||||
// Add new api key
|
||||
this.app.post('/api/v1/apikey', async (req: Request, res: Response) => {
|
||||
const keys = await addAPIKey(req.body.keyName)
|
||||
return res.json(keys)
|
||||
})
|
||||
|
||||
// Update api key
|
||||
this.app.put('/api/v1/apikey/:id', async (req: Request, res: Response) => {
|
||||
const keys = await updateAPIKey(req.params.id, req.body.keyName)
|
||||
return res.json(keys)
|
||||
})
|
||||
|
||||
// Delete new api key
|
||||
this.app.delete('/api/v1/apikey/:id', async (req: Request, res: Response) => {
|
||||
const keys = await deleteAPIKey(req.params.id)
|
||||
return res.json(keys)
|
||||
})
|
||||
|
||||
// ----------------------------------------
|
||||
// Serve UI static
|
||||
// ----------------------------------------
|
||||
|
|
@ -324,6 +281,109 @@ export class App {
|
|||
})
|
||||
}
|
||||
|
||||
async processPrediction(req: Request, res: Response, isInternal = false) {
|
||||
try {
|
||||
const chatflowid = req.params.id
|
||||
const incomingInput: IncomingInput = req.body
|
||||
|
||||
let nodeToExecuteData: INodeData
|
||||
|
||||
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
|
||||
id: chatflowid
|
||||
})
|
||||
if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`)
|
||||
|
||||
if (!isInternal) {
|
||||
const chatFlowApiKeyId = chatflow.apikeyid
|
||||
const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? ''
|
||||
|
||||
if (chatFlowApiKeyId && !authorizationHeader) return res.status(401).send(`Unauthorized`)
|
||||
|
||||
const suppliedKey = authorizationHeader.split(`Bearer `).pop()
|
||||
if (chatFlowApiKeyId && suppliedKey) {
|
||||
const keys = await getAPIKeys()
|
||||
const apiSecret = keys.find((key) => key.id === chatFlowApiKeyId)?.apiSecret
|
||||
if (!compareKeys(apiSecret, suppliedKey)) return res.status(401).send(`Unauthorized`)
|
||||
}
|
||||
}
|
||||
|
||||
/* Check if:
|
||||
* - Node Data already exists in pool
|
||||
* - Still in sync (i.e the flow has not been modified since)
|
||||
* - Flow doesn't start with nodes that depend on incomingInput.question
|
||||
***/
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(this.chatflowPool.activeChatflows, chatflowid) &&
|
||||
this.chatflowPool.activeChatflows[chatflowid].inSync &&
|
||||
!isStartNodeDependOnInput(this.chatflowPool.activeChatflows[chatflowid].startingNodes)
|
||||
) {
|
||||
nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData
|
||||
} else {
|
||||
/*** Get chatflows and prepare data ***/
|
||||
|
||||
const flowData = chatflow.flowData
|
||||
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
|
||||
const nodes = parsedFlowData.nodes
|
||||
const edges = parsedFlowData.edges
|
||||
|
||||
/*** Get Ending Node with Directed Graph ***/
|
||||
const { graph, nodeDependencies } = constructGraphs(nodes, edges)
|
||||
const directedGraph = graph
|
||||
const endingNodeId = getEndingNode(nodeDependencies, directedGraph)
|
||||
if (!endingNodeId) return res.status(500).send(`Ending node must be either a Chain or Agent`)
|
||||
|
||||
const endingNodeData = nodes.find((nd) => nd.id === endingNodeId)?.data
|
||||
if (!endingNodeData) return res.status(500).send(`Ending node must be either a Chain or Agent`)
|
||||
|
||||
if (
|
||||
endingNodeData.outputs &&
|
||||
Object.keys(endingNodeData.outputs).length &&
|
||||
!Object.values(endingNodeData.outputs).includes(endingNodeData.name)
|
||||
) {
|
||||
return res
|
||||
.status(500)
|
||||
.send(
|
||||
`Output of ${endingNodeData.label} (${endingNodeData.id}) must be ${endingNodeData.label}, can't be an Output Prediction`
|
||||
)
|
||||
}
|
||||
|
||||
/*** Get Starting Nodes with Non-Directed Graph ***/
|
||||
const constructedObj = constructGraphs(nodes, edges, true)
|
||||
const nonDirectedGraph = constructedObj.graph
|
||||
const { startingNodeIds, depthQueue } = getStartingNodes(nonDirectedGraph, endingNodeId)
|
||||
|
||||
/*** BFS to traverse from Starting Nodes to Ending Node ***/
|
||||
const reactFlowNodes = await buildLangchain(
|
||||
startingNodeIds,
|
||||
nodes,
|
||||
graph,
|
||||
depthQueue,
|
||||
this.nodesPool.componentNodes,
|
||||
incomingInput.question
|
||||
)
|
||||
|
||||
const nodeToExecute = reactFlowNodes.find((node: IReactFlowNode) => node.id === endingNodeId)
|
||||
if (!nodeToExecute) return res.status(404).send(`Node ${endingNodeId} not found`)
|
||||
|
||||
const reactFlowNodeData: INodeData = resolveVariables(nodeToExecute.data, reactFlowNodes, incomingInput.question)
|
||||
nodeToExecuteData = reactFlowNodeData
|
||||
|
||||
const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id))
|
||||
this.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes)
|
||||
}
|
||||
|
||||
const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string
|
||||
const nodeModule = await import(nodeInstanceFilePath)
|
||||
const nodeInstance = new nodeModule.nodeClass()
|
||||
|
||||
const result = await nodeInstance.run(nodeToExecuteData, incomingInput.question, { chatHistory: incomingInput.history })
|
||||
|
||||
return res.json(result)
|
||||
} catch (e: any) {
|
||||
return res.status(500).send(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
async stopApp() {
|
||||
try {
|
||||
const removePromises: any[] = []
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import moment from 'moment'
|
||||
import {
|
||||
IComponentNodes,
|
||||
IDepthQueue,
|
||||
|
|
@ -14,6 +15,7 @@ import {
|
|||
} from '../Interface'
|
||||
import { cloneDeep, get } from 'lodash'
|
||||
import { ICommonObject, getInputVariables } from 'flowise-components'
|
||||
import { scryptSync, randomBytes, timingSafeEqual } from 'crypto'
|
||||
|
||||
const QUESTION_VAR_PREFIX = 'question'
|
||||
|
||||
|
|
@ -362,3 +364,119 @@ export const isStartNodeDependOnInput = (startingNodes: IReactFlowNode[]): boole
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the api key path
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getAPIKeyPath = (): string => {
|
||||
return path.join(__dirname, '..', '..', 'api.json')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the api key
|
||||
* @returns {string}
|
||||
*/
|
||||
export const generateAPIKey = (): string => {
|
||||
const buffer = randomBytes(32)
|
||||
return buffer.toString('base64')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the secret key
|
||||
* @param {string} apiKey
|
||||
* @returns {string}
|
||||
*/
|
||||
export const generateSecretHash = (apiKey: string): string => {
|
||||
const salt = randomBytes(8).toString('hex')
|
||||
const buffer = scryptSync(apiKey, salt, 64) as Buffer
|
||||
return `${buffer.toString('hex')}.${salt}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify valid keys
|
||||
* @param {string} storedKey
|
||||
* @param {string} suppliedKey
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const compareKeys = (storedKey: string, suppliedKey: string): boolean => {
|
||||
const [hashedPassword, salt] = storedKey.split('.')
|
||||
const buffer = scryptSync(suppliedKey, salt, 64) as Buffer
|
||||
return timingSafeEqual(Buffer.from(hashedPassword, 'hex'), buffer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API keys
|
||||
* @returns {Promise<ICommonObject[]>}
|
||||
*/
|
||||
export const getAPIKeys = async (): Promise<ICommonObject[]> => {
|
||||
try {
|
||||
const content = await fs.promises.readFile(getAPIKeyPath(), 'utf8')
|
||||
return JSON.parse(content)
|
||||
} catch (error) {
|
||||
const keyName = 'DefaultKey'
|
||||
const apiKey = generateAPIKey()
|
||||
const apiSecret = generateSecretHash(apiKey)
|
||||
const content = [
|
||||
{
|
||||
keyName,
|
||||
apiKey,
|
||||
apiSecret,
|
||||
createdAt: moment().format('DD-MMM-YY'),
|
||||
id: randomBytes(16).toString('hex')
|
||||
}
|
||||
]
|
||||
await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(content), 'utf8')
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new API key
|
||||
* @param {string} keyName
|
||||
* @returns {Promise<ICommonObject[]>}
|
||||
*/
|
||||
export const addAPIKey = async (keyName: string): Promise<ICommonObject[]> => {
|
||||
const existingAPIKeys = await getAPIKeys()
|
||||
const apiKey = generateAPIKey()
|
||||
const apiSecret = generateSecretHash(apiKey)
|
||||
const content = [
|
||||
...existingAPIKeys,
|
||||
{
|
||||
keyName,
|
||||
apiKey,
|
||||
apiSecret,
|
||||
createdAt: moment().format('DD-MMM-YY'),
|
||||
id: randomBytes(16).toString('hex')
|
||||
}
|
||||
]
|
||||
await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(content), 'utf8')
|
||||
return content
|
||||
}
|
||||
|
||||
/**
|
||||
* Update existing API key
|
||||
* @param {string} keyIdToUpdate
|
||||
* @param {string} newKeyName
|
||||
* @returns {Promise<ICommonObject[]>}
|
||||
*/
|
||||
export const updateAPIKey = async (keyIdToUpdate: string, newKeyName: string): Promise<ICommonObject[]> => {
|
||||
const existingAPIKeys = await getAPIKeys()
|
||||
const keyIndex = existingAPIKeys.findIndex((key) => key.id === keyIdToUpdate)
|
||||
if (keyIndex < 0) return []
|
||||
existingAPIKeys[keyIndex].keyName = newKeyName
|
||||
await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(existingAPIKeys), 'utf8')
|
||||
return existingAPIKeys
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete API key
|
||||
* @param {string} keyIdToDelete
|
||||
* @returns {Promise<ICommonObject[]>}
|
||||
*/
|
||||
export const deleteAPIKey = async (keyIdToDelete: string): Promise<ICommonObject[]> => {
|
||||
const existingAPIKeys = await getAPIKeys()
|
||||
const result = existingAPIKeys.filter((key) => key.id !== keyIdToDelete)
|
||||
await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(result), 'utf8')
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import client from './client'
|
||||
|
||||
const getAllAPIKeys = () => client.get('/apikey')
|
||||
|
||||
const createNewAPI = (body) => client.post(`/apikey`, body)
|
||||
|
||||
const updateAPI = (id, body) => client.put(`/apikey/${id}`, body)
|
||||
|
||||
const deleteAPI = (id) => client.delete(`/apikey/${id}`)
|
||||
|
||||
export default {
|
||||
getAllAPIKeys,
|
||||
createNewAPI,
|
||||
updateAPI,
|
||||
deleteAPI
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import client from './client'
|
||||
|
||||
const sendMessageAndGetPrediction = (id, input) => client.post(`/prediction/${id}`, input)
|
||||
const sendMessageAndGetPrediction = (id, input) => client.post(`/internal-prediction/${id}`, input)
|
||||
|
||||
export default {
|
||||
sendMessageAndGetPrediction
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// assets
|
||||
import { IconHierarchy, IconBuildingStore } from '@tabler/icons'
|
||||
import { IconHierarchy, IconBuildingStore, IconKey } from '@tabler/icons'
|
||||
|
||||
// constant
|
||||
const icons = { IconHierarchy, IconBuildingStore }
|
||||
const icons = { IconHierarchy, IconBuildingStore, IconKey }
|
||||
|
||||
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
||||
|
||||
|
|
@ -26,6 +26,14 @@ const dashboard = {
|
|||
url: '/marketplaces',
|
||||
icon: icons.IconBuildingStore,
|
||||
breadcrumbs: true
|
||||
},
|
||||
{
|
||||
id: 'apikey',
|
||||
title: 'API Keys',
|
||||
type: 'item',
|
||||
url: '/apikey',
|
||||
icon: icons.IconKey,
|
||||
breadcrumbs: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ const Chatflows = Loadable(lazy(() => import('views/chatflows')))
|
|||
// marketplaces routing
|
||||
const Marketplaces = Loadable(lazy(() => import('views/marketplaces')))
|
||||
|
||||
// apikey routing
|
||||
const APIKey = Loadable(lazy(() => import('views/apikey')))
|
||||
|
||||
// ==============================|| MAIN ROUTING ||============================== //
|
||||
|
||||
const MainRoutes = {
|
||||
|
|
@ -27,6 +30,10 @@ const MainRoutes = {
|
|||
{
|
||||
path: '/marketplaces',
|
||||
element: <Marketplaces />
|
||||
},
|
||||
{
|
||||
path: '/apikey',
|
||||
element: <APIKey />
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,31 @@
|
|||
import { createPortal } from 'react-dom'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Tabs, Tab, Dialog, DialogContent, DialogTitle, Box } from '@mui/material'
|
||||
import { CopyBlock, atomOneDark } from 'react-code-blocks'
|
||||
|
||||
// Project import
|
||||
import { Dropdown } from 'ui-component/dropdown/Dropdown'
|
||||
|
||||
// Const
|
||||
import { baseURL } from 'store/constant'
|
||||
import { SET_CHATFLOW } from 'store/actions'
|
||||
|
||||
// Images
|
||||
import pythonSVG from 'assets/images/python.svg'
|
||||
import javascriptSVG from 'assets/images/javascript.svg'
|
||||
import cURLSVG from 'assets/images/cURL.svg'
|
||||
|
||||
// API
|
||||
import apiKeyApi from 'api/apikey'
|
||||
import chatflowsApi from 'api/chatflows'
|
||||
|
||||
// Hooks
|
||||
import useApi from 'hooks/useApi'
|
||||
|
||||
function TabPanel(props) {
|
||||
const { children, value, index, ...other } = props
|
||||
return (
|
||||
|
|
@ -39,8 +56,36 @@ function a11yProps(index) {
|
|||
|
||||
const APICodeDialog = ({ show, dialogProps, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const navigate = useNavigate()
|
||||
const dispatch = useDispatch()
|
||||
const codes = ['Python', 'JavaScript', 'cURL']
|
||||
const [value, setValue] = useState(0)
|
||||
const [keyOptions, setKeyOptions] = useState([])
|
||||
const [apiKeys, setAPIKeys] = useState([])
|
||||
const [chatflowApiKeyId, setChatflowApiKeyId] = useState('')
|
||||
const [selectedApiKey, setSelectedApiKey] = useState({})
|
||||
|
||||
const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys)
|
||||
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
|
||||
|
||||
const onApiKeySelected = (keyValue) => {
|
||||
if (keyValue === 'addnewkey') {
|
||||
navigate('/apikey')
|
||||
return
|
||||
}
|
||||
setChatflowApiKeyId(keyValue)
|
||||
setSelectedApiKey(apiKeys.find((key) => key.id === keyValue))
|
||||
const updateBody = {
|
||||
apikeyid: keyValue
|
||||
}
|
||||
updateChatflowApi.request(dialogProps.chatflowid, updateBody)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (updateChatflowApi.data) {
|
||||
dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data })
|
||||
}
|
||||
}, [updateChatflowApi.data, dispatch])
|
||||
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue)
|
||||
|
|
@ -83,6 +128,46 @@ output = query({
|
|||
return ''
|
||||
}
|
||||
|
||||
const getCodeWithAuthorization = (codeLang) => {
|
||||
if (codeLang === 'Python') {
|
||||
return `import requests
|
||||
|
||||
API_URL = "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}"
|
||||
headers = {"Authorization": "Bearer ${selectedApiKey?.apiKey}"}
|
||||
|
||||
def query(payload):
|
||||
response = requests.post(API_URL, headers=headers, json=payload)
|
||||
return response.json()
|
||||
|
||||
output = query({
|
||||
"question": "Hey, how are you?",
|
||||
})
|
||||
`
|
||||
} else if (codeLang === 'JavaScript') {
|
||||
return `async function query(data) {
|
||||
const response = await fetch(
|
||||
"${baseURL}/api/v1/prediction/${dialogProps.chatflowid}",
|
||||
{
|
||||
headers: { Authorization: "Bearer ${selectedApiKey?.apiKey}" },
|
||||
method: "POST",
|
||||
body: {
|
||||
"question": "Hey, how are you?"
|
||||
},
|
||||
}
|
||||
);
|
||||
const result = await response.json();
|
||||
return result;
|
||||
}
|
||||
`
|
||||
} else if (codeLang === 'cURL') {
|
||||
return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\
|
||||
-X POST \\
|
||||
-d '{"question": "Hey, how are you?"}'
|
||||
-H "Authorization: Bearer ${selectedApiKey?.apiKey}"`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const getLang = (codeLang) => {
|
||||
if (codeLang === 'Python') {
|
||||
return 'python'
|
||||
|
|
@ -105,6 +190,40 @@ output = query({
|
|||
return pythonSVG
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllAPIKeysApi.data) {
|
||||
const options = [
|
||||
{
|
||||
label: 'No Authorization',
|
||||
name: ''
|
||||
}
|
||||
]
|
||||
for (const key of getAllAPIKeysApi.data) {
|
||||
options.push({
|
||||
label: key.keyName,
|
||||
name: key.id
|
||||
})
|
||||
}
|
||||
options.push({
|
||||
label: '- Add New Key -',
|
||||
name: 'addnewkey'
|
||||
})
|
||||
setKeyOptions(options)
|
||||
setAPIKeys(getAllAPIKeysApi.data)
|
||||
|
||||
if (dialogProps.chatflowApiKeyId) {
|
||||
setChatflowApiKeyId(dialogProps.chatflowApiKeyId)
|
||||
setSelectedApiKey(getAllAPIKeysApi.data.find((key) => key.id === dialogProps.chatflowApiKeyId))
|
||||
}
|
||||
}
|
||||
}, [dialogProps, getAllAPIKeysApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) getAllAPIKeysApi.request()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [show])
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
open={show}
|
||||
|
|
@ -118,29 +237,42 @@ output = query({
|
|||
{dialogProps.title}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Tabs value={value} onChange={handleChange} aria-label='tabs'>
|
||||
{codes.map((codeLang, index) => (
|
||||
<Tab
|
||||
icon={
|
||||
<img
|
||||
style={{ objectFit: 'cover', height: 'auto', width: 'auto', marginLeft: 10 }}
|
||||
src={getSVG(codeLang)}
|
||||
alt='code'
|
||||
/>
|
||||
}
|
||||
iconPosition='left'
|
||||
key={index}
|
||||
label={codeLang}
|
||||
{...a11yProps(index)}
|
||||
></Tab>
|
||||
))}
|
||||
</Tabs>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<div style={{ flex: 80 }}>
|
||||
<Tabs value={value} onChange={handleChange} aria-label='tabs'>
|
||||
{codes.map((codeLang, index) => (
|
||||
<Tab
|
||||
icon={
|
||||
<img
|
||||
style={{ objectFit: 'cover', height: 'auto', width: 'auto' }}
|
||||
src={getSVG(codeLang)}
|
||||
alt='code'
|
||||
/>
|
||||
}
|
||||
iconPosition='start'
|
||||
key={index}
|
||||
label={codeLang}
|
||||
{...a11yProps(index)}
|
||||
></Tab>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
<div style={{ flex: 20 }}>
|
||||
<Dropdown
|
||||
name='SelectKey'
|
||||
disableClearable={true}
|
||||
options={keyOptions}
|
||||
onSelect={(newValue) => onApiKeySelected(newValue)}
|
||||
value={dialogProps.chatflowApiKeyId ?? chatflowApiKeyId ?? 'Choose an API key'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}></div>
|
||||
{codes.map((codeLang, index) => (
|
||||
<TabPanel key={index} value={value} index={index}>
|
||||
<CopyBlock
|
||||
theme={atomOneDark}
|
||||
text={getCode(codeLang)}
|
||||
text={chatflowApiKeyId ? getCodeWithAuthorization(codeLang) : getCode(codeLang)}
|
||||
language={getLang(codeLang)}
|
||||
showLineNumbers={false}
|
||||
wrapLines
|
||||
|
|
|
|||
|
|
@ -0,0 +1,231 @@
|
|||
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 {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Stack,
|
||||
IconButton,
|
||||
OutlinedInput,
|
||||
Popover
|
||||
} from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
|
||||
// Icons
|
||||
import { IconX, IconCopy } from '@tabler/icons'
|
||||
|
||||
// API
|
||||
import apikeyApi from 'api/apikey'
|
||||
|
||||
// utils
|
||||
import useNotifier from 'utils/useNotifier'
|
||||
|
||||
const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
const theme = useTheme()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
// ==============================|| Snackbar ||============================== //
|
||||
|
||||
useNotifier()
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const [keyName, setKeyName] = useState('')
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const openPopOver = Boolean(anchorEl)
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.type === 'EDIT' && dialogProps.key) {
|
||||
setKeyName(dialogProps.key.keyName)
|
||||
} else if (dialogProps.type === 'ADD') {
|
||||
setKeyName('')
|
||||
}
|
||||
}, [dialogProps])
|
||||
|
||||
const handleClosePopOver = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const addNewKey = async () => {
|
||||
try {
|
||||
const createResp = await apikeyApi.createNewAPI({ keyName })
|
||||
if (createResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'New API key added',
|
||||
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 add new API key: ${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 saveKey = async () => {
|
||||
try {
|
||||
const saveResp = await apikeyApi.updateAPI(dialogProps.key.id, { keyName })
|
||||
if (saveResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'API Key saved',
|
||||
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 save API key: ${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'>
|
||||
{dialogProps.title}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{dialogProps.type === 'EDIT' && (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant='overline'>API Key</Typography>
|
||||
<Stack direction='row' sx={{ mb: 1 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
p: 1,
|
||||
borderRadius: 10,
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
width: 'max-content',
|
||||
height: 'max-content'
|
||||
}}
|
||||
variant='h5'
|
||||
>
|
||||
{dialogProps.key.apiKey}
|
||||
</Typography>
|
||||
<IconButton
|
||||
title='Copy API Key'
|
||||
color='success'
|
||||
onClick={(event) => {
|
||||
navigator.clipboard.writeText(dialogProps.key.apiKey)
|
||||
setAnchorEl(event.currentTarget)
|
||||
setTimeout(() => {
|
||||
handleClosePopOver()
|
||||
}, 1500)
|
||||
}}
|
||||
>
|
||||
<IconCopy />
|
||||
</IconButton>
|
||||
<Popover
|
||||
open={openPopOver}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClosePopOver}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
>
|
||||
<Typography variant='h6' sx={{ pl: 1, pr: 1, color: 'white', background: theme.palette.success.dark }}>
|
||||
Copied!
|
||||
</Typography>
|
||||
</Popover>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Stack sx={{ position: 'relative' }} direction='row'>
|
||||
<Typography variant='overline'>Key Name</Typography>
|
||||
</Stack>
|
||||
<OutlinedInput
|
||||
id='keyName'
|
||||
type='string'
|
||||
fullWidth
|
||||
placeholder='My New Key'
|
||||
value={keyName}
|
||||
name='keyName'
|
||||
onChange={(e) => setKeyName(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<StyledButton variant='contained' onClick={() => (dialogProps.type === 'ADD' ? addNewKey() : saveKey())}>
|
||||
{dialogProps.confirmButtonName}
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
APIKeyDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onConfirm: PropTypes.func
|
||||
}
|
||||
|
||||
export default APIKeyDialog
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
|
||||
|
||||
// material-ui
|
||||
import {
|
||||
Button,
|
||||
Box,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Popover,
|
||||
Typography
|
||||
} 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 APIKeyDialog from './APIKeyDialog'
|
||||
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
|
||||
|
||||
// API
|
||||
import apiKeyApi from 'api/apikey'
|
||||
|
||||
// Hooks
|
||||
import useApi from 'hooks/useApi'
|
||||
import useConfirm from 'hooks/useConfirm'
|
||||
|
||||
// utils
|
||||
import useNotifier from 'utils/useNotifier'
|
||||
|
||||
// Icons
|
||||
import { IconTrash, IconEdit, IconCopy, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons'
|
||||
import APIEmptySVG from 'assets/images/api_empty.svg'
|
||||
|
||||
// ==============================|| APIKey ||============================== //
|
||||
|
||||
const APIKey = () => {
|
||||
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 [showDialog, setShowDialog] = useState(false)
|
||||
const [dialogProps, setDialogProps] = useState({})
|
||||
const [apiKeys, setAPIKeys] = useState([])
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [showApiKeys, setShowApiKeys] = useState([])
|
||||
const openPopOver = Boolean(anchorEl)
|
||||
|
||||
const { confirm } = useConfirm()
|
||||
|
||||
const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys)
|
||||
|
||||
const onShowApiKeyClick = (apikey) => {
|
||||
const index = showApiKeys.indexOf(apikey)
|
||||
if (index > -1) {
|
||||
//showApiKeys.splice(index, 1)
|
||||
const newShowApiKeys = showApiKeys.filter(function (item) {
|
||||
return item !== apikey
|
||||
})
|
||||
setShowApiKeys(newShowApiKeys)
|
||||
} else {
|
||||
setShowApiKeys((prevkeys) => [...prevkeys, apikey])
|
||||
}
|
||||
}
|
||||
|
||||
const handleClosePopOver = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const addNew = () => {
|
||||
const dialogProp = {
|
||||
title: 'Add New API Key',
|
||||
type: 'ADD',
|
||||
cancelButtonName: 'Cancel',
|
||||
confirmButtonName: 'Add'
|
||||
}
|
||||
setDialogProps(dialogProp)
|
||||
setShowDialog(true)
|
||||
}
|
||||
|
||||
const edit = (key) => {
|
||||
const dialogProp = {
|
||||
title: 'Edit API Key',
|
||||
type: 'EDIT',
|
||||
cancelButtonName: 'Cancel',
|
||||
confirmButtonName: 'Save',
|
||||
key
|
||||
}
|
||||
setDialogProps(dialogProp)
|
||||
setShowDialog(true)
|
||||
}
|
||||
|
||||
const deleteKey = async (key) => {
|
||||
const confirmPayload = {
|
||||
title: `Delete`,
|
||||
description: `Delete key ${key.keyName}?`,
|
||||
confirmButtonName: 'Delete',
|
||||
cancelButtonName: 'Cancel'
|
||||
}
|
||||
const isConfirmed = await confirm(confirmPayload)
|
||||
|
||||
if (isConfirmed) {
|
||||
try {
|
||||
const deleteResp = await apiKeyApi.deleteAPI(key.id)
|
||||
if (deleteResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'API key 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 API key: ${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 onConfirm = () => {
|
||||
setShowDialog(false)
|
||||
getAllAPIKeysApi.request()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAllAPIKeysApi.request()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllAPIKeysApi.data) {
|
||||
setAPIKeys(getAllAPIKeysApi.data)
|
||||
}
|
||||
}, [getAllAPIKeysApi.data])
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
|
||||
<Stack flexDirection='row'>
|
||||
<h1>API Keys </h1>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
<StyledButton variant='contained' sx={{ color: 'white', mr: 1, height: 37 }} onClick={addNew} startIcon={<IconPlus />}>
|
||||
Create Key
|
||||
</StyledButton>
|
||||
</Stack>
|
||||
{apiKeys.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={APIEmptySVG} alt='APIEmptySVG' />
|
||||
</Box>
|
||||
<div>No API Keys Yet</div>
|
||||
</Stack>
|
||||
)}
|
||||
{apiKeys.length > 0 && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Key Name</TableCell>
|
||||
<TableCell>API Key</TableCell>
|
||||
<TableCell>Created</TableCell>
|
||||
<TableCell> </TableCell>
|
||||
<TableCell> </TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{apiKeys.map((key, index) => (
|
||||
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
<TableCell component='th' scope='row'>
|
||||
{key.keyName}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{showApiKeys.includes(key.apiKey)
|
||||
? key.apiKey
|
||||
: `${key.apiKey.substring(0, 2)}${'•'.repeat(18)}${key.apiKey.substring(
|
||||
key.apiKey.length - 5
|
||||
)}`}
|
||||
<IconButton
|
||||
title='Copy'
|
||||
color='success'
|
||||
onClick={(event) => {
|
||||
navigator.clipboard.writeText(key.apiKey)
|
||||
setAnchorEl(event.currentTarget)
|
||||
setTimeout(() => {
|
||||
handleClosePopOver()
|
||||
}, 1500)
|
||||
}}
|
||||
>
|
||||
<IconCopy />
|
||||
</IconButton>
|
||||
<IconButton title='Show' color='inherit' onClick={() => onShowApiKeyClick(key.apiKey)}>
|
||||
{showApiKeys.includes(key.apiKey) ? <IconEyeOff /> : <IconEye />}
|
||||
</IconButton>
|
||||
<Popover
|
||||
open={openPopOver}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClosePopOver}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant='h6'
|
||||
sx={{ pl: 1, pr: 1, color: 'white', background: theme.palette.success.dark }}
|
||||
>
|
||||
Copied!
|
||||
</Typography>
|
||||
</Popover>
|
||||
</TableCell>
|
||||
<TableCell>{key.createdAt}</TableCell>
|
||||
<TableCell>
|
||||
<IconButton title='Edit' color='primary' onClick={() => edit(key)}>
|
||||
<IconEdit />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton title='Delete' color='error' onClick={() => deleteKey(key)}>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</MainCard>
|
||||
<APIKeyDialog
|
||||
show={showDialog}
|
||||
dialogProps={dialogProps}
|
||||
onCancel={() => setShowDialog(false)}
|
||||
onConfirm={onConfirm}
|
||||
></APIKeyDialog>
|
||||
<ConfirmDialog />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default APIKey
|
||||
|
|
@ -8,7 +8,7 @@ import { useTheme } from '@mui/material/styles'
|
|||
import { Avatar, Box, ButtonBase, Typography, Stack, TextField } from '@mui/material'
|
||||
|
||||
// icons
|
||||
import { IconSettings, IconChevronLeft, IconDeviceFloppy, IconPencil, IconCheck, IconX, IconWorldWww } from '@tabler/icons'
|
||||
import { IconSettings, IconChevronLeft, IconDeviceFloppy, IconPencil, IconCheck, IconX, IconCode } from '@tabler/icons'
|
||||
|
||||
// project imports
|
||||
import Settings from 'views/settings'
|
||||
|
|
@ -82,7 +82,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
|
|||
const onAPIDialogClick = () => {
|
||||
setAPIDialogProps({
|
||||
title: 'Use this chatflow with API',
|
||||
chatflowid: chatflow.id
|
||||
chatflowid: chatflow.id,
|
||||
chatflowApiKeyId: chatflow.apikeyid
|
||||
})
|
||||
setAPIDialogOpen(true)
|
||||
}
|
||||
|
|
@ -247,7 +248,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
|
|||
color='inherit'
|
||||
onClick={onAPIDialogClick}
|
||||
>
|
||||
<IconWorldWww stroke={1.5} size='1.3rem' />
|
||||
<IconCode stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
<ButtonBase title='Save Chatflow' sx={{ borderRadius: '50%', mr: 2 }}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue