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

View File

@ -1,6 +1,6 @@
import { OpenAIBaseInput } from 'langchain/dist/types/openai-types'
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { AzureOpenAIInput, ChatOpenAI } from 'langchain/chat_models/openai'
class AzureChatOpenAI_ChatModels implements INode {
@ -11,6 +11,7 @@ class AzureChatOpenAI_ChatModels implements INode {
category: string
description: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
@ -21,12 +22,13 @@ class AzureChatOpenAI_ChatModels implements INode {
this.category = 'Chat Models'
this.description = 'Wrapper around Azure OpenAI large language models that use the Chat endpoint'
this.baseClasses = [this.type, ...getBaseClasses(ChatOpenAI)]
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['azureOpenAIApi']
}
this.inputs = [
{
label: 'Azure OpenAI Api Key',
name: 'azureOpenAIApiKey',
type: 'password'
},
{
label: 'Model Name',
name: 'modelName',
@ -59,26 +61,6 @@ class AzureChatOpenAI_ChatModels implements INode {
default: 0.9,
optional: true
},
{
label: 'Azure OpenAI Api Instance Name',
name: 'azureOpenAIApiInstanceName',
type: 'string',
placeholder: 'YOUR-INSTANCE-NAME'
},
{
label: 'Azure OpenAI Api Deployment Name',
name: 'azureOpenAIApiDeploymentName',
type: 'string',
placeholder: 'YOUR-DEPLOYMENT-NAME'
},
{
label: 'Azure OpenAI Api Version',
name: 'azureOpenAIApiVersion',
type: 'string',
placeholder: '2023-06-01-preview',
description:
'Description of Supported API Versions. Please refer <a target="_blank" href="https://learn.microsoft.com/en-us/azure/cognitive-services/openai/reference#chat-completions">examples</a>'
},
{
label: 'Max Tokens',
name: 'maxTokens',
@ -110,19 +92,21 @@ class AzureChatOpenAI_ChatModels implements INode {
]
}
async init(nodeData: INodeData): Promise<any> {
const azureOpenAIApiKey = nodeData.inputs?.azureOpenAIApiKey as string
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const modelName = nodeData.inputs?.modelName as string
const temperature = nodeData.inputs?.temperature as string
const azureOpenAIApiInstanceName = nodeData.inputs?.azureOpenAIApiInstanceName as string
const azureOpenAIApiDeploymentName = nodeData.inputs?.azureOpenAIApiDeploymentName as string
const azureOpenAIApiVersion = nodeData.inputs?.azureOpenAIApiVersion as string
const maxTokens = nodeData.inputs?.maxTokens as string
const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string
const presencePenalty = nodeData.inputs?.presencePenalty as string
const timeout = nodeData.inputs?.timeout as string
const streaming = nodeData.inputs?.streaming as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const azureOpenAIApiKey = getCredentialParam('azureOpenAIApiKey', credentialData, nodeData)
const azureOpenAIApiInstanceName = getCredentialParam('azureOpenAIApiInstanceName', credentialData, nodeData)
const azureOpenAIApiDeploymentName = getCredentialParam('azureOpenAIApiDeploymentName', credentialData, nodeData)
const azureOpenAIApiVersion = getCredentialParam('azureOpenAIApiVersion', credentialData, nodeData)
const obj: Partial<AzureOpenAIInput> & Partial<OpenAIBaseInput> = {
temperature: parseFloat(temperature),
modelName,

View File

@ -0,0 +1,45 @@
import { INodeParams, INodeCredential } from '../../../src/Interface'
class AzureOpenAIApi implements INodeCredential {
label: string
name: string
description: string
inputs: INodeParams[]
constructor() {
this.label = 'Azure OpenAI API'
this.name = 'azureOpenAIApi'
this.description =
'Refer to <a target="_blank" href="https://azure.microsoft.com/en-us/products/cognitive-services/openai-service">official guide</a> of how to use Azure OpenAI service'
this.inputs = [
{
label: 'Azure OpenAI Api Key',
name: 'azureOpenAIApiKey',
type: 'password',
description: `Refer to <a target="_blank" href="https://learn.microsoft.com/en-us/azure/cognitive-services/openai/quickstart?tabs=command-line&pivots=rest-api#set-up">official guide</a> on how to create API key on Azure OpenAI`
},
{
label: 'Azure OpenAI Api Instance Name',
name: 'azureOpenAIApiInstanceName',
type: 'string',
placeholder: 'YOUR-INSTANCE-NAME'
},
{
label: 'Azure OpenAI Api Deployment Name',
name: 'azureOpenAIApiDeploymentName',
type: 'string',
placeholder: 'YOUR-DEPLOYMENT-NAME'
},
{
label: 'Azure OpenAI Api Version',
name: 'azureOpenAIApiVersion',
type: 'string',
placeholder: '2023-06-01-preview',
description:
'Description of Supported API Versions. Please refer <a target="_blank" href="https://learn.microsoft.com/en-us/azure/cognitive-services/openai/reference#chat-completions">examples</a>'
}
]
}
}
module.exports = { credClass: AzureOpenAIApi }

View File

@ -0,0 +1,21 @@
import { INodeParams, INodeCredential } from '../../../src/Interface'
class AnthropicApi implements INodeCredential {
label: string
name: string
inputs: INodeParams[]
constructor() {
this.label = 'Anthropic API'
this.name = 'anthropicApi'
this.inputs = [
{
label: 'Anthropic Api Key',
name: 'anthropicApiKey',
type: 'password'
}
]
}
}
module.exports = { credClass: AnthropicApi }

View File

@ -1,5 +1,5 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { AnthropicInput, ChatAnthropic } from 'langchain/chat_models/anthropic'
class ChatAnthropic_ChatModels implements INode {
@ -10,6 +10,7 @@ class ChatAnthropic_ChatModels implements INode {
category: string
description: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
@ -20,12 +21,13 @@ class ChatAnthropic_ChatModels implements INode {
this.category = 'Chat Models'
this.description = 'Wrapper around ChatAnthropic large language models that use the Chat endpoint'
this.baseClasses = [this.type, ...getBaseClasses(ChatAnthropic)]
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['anthropicApi']
}
this.inputs = [
{
label: 'ChatAnthropic Api Key',
name: 'anthropicApiKey',
type: 'password'
},
{
label: 'Model Name',
name: 'modelName',
@ -110,15 +112,17 @@ class ChatAnthropic_ChatModels implements INode {
]
}
async init(nodeData: INodeData): Promise<any> {
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const temperature = nodeData.inputs?.temperature as string
const modelName = nodeData.inputs?.modelName as string
const anthropicApiKey = nodeData.inputs?.anthropicApiKey as string
const maxTokensToSample = nodeData.inputs?.maxTokensToSample as string
const topP = nodeData.inputs?.topP as string
const topK = nodeData.inputs?.topK as string
const streaming = nodeData.inputs?.streaming as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const anthropicApiKey = getCredentialParam('anthropicApiKey', credentialData, nodeData)
const obj: Partial<AnthropicInput> & { anthropicApiKey?: string } = {
temperature: parseFloat(temperature),
modelName,

View File

@ -1,5 +1,5 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { ChatOpenAI, OpenAIChatInput } from 'langchain/chat_models/openai'
class ChatOpenAI_ChatModels implements INode {
@ -10,6 +10,7 @@ class ChatOpenAI_ChatModels implements INode {
category: string
description: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
@ -20,12 +21,13 @@ class ChatOpenAI_ChatModels implements INode {
this.category = 'Chat Models'
this.description = 'Wrapper around OpenAI large language models that use the Chat endpoint'
this.baseClasses = [this.type, ...getBaseClasses(ChatOpenAI)]
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['openAIApi']
}
this.inputs = [
{
label: 'OpenAI Api Key',
name: 'openAIApiKey',
type: 'password'
},
{
label: 'Model Name',
name: 'modelName',
@ -119,10 +121,9 @@ class ChatOpenAI_ChatModels implements INode {
]
}
async init(nodeData: INodeData): Promise<any> {
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const temperature = nodeData.inputs?.temperature as string
const modelName = nodeData.inputs?.modelName as string
const openAIApiKey = nodeData.inputs?.openAIApiKey as string
const maxTokens = nodeData.inputs?.maxTokens as string
const topP = nodeData.inputs?.topP as string
const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string
@ -131,6 +132,9 @@ class ChatOpenAI_ChatModels implements INode {
const streaming = nodeData.inputs?.streaming as boolean
const basePath = nodeData.inputs?.basepath as string
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData)
const obj: Partial<OpenAIChatInput> & { openAIApiKey?: string } = {
temperature: parseFloat(temperature),
modelName,

View File

@ -0,0 +1,21 @@
import { INodeParams, INodeCredential } from '../../../src/Interface'
class OpenAIApi implements INodeCredential {
label: string
name: string
inputs: INodeParams[]
constructor() {
this.label = 'OpenAI API'
this.name = 'openAIApi'
this.inputs = [
{
label: 'OpenAI Api Key',
name: 'openAIApiKey',
type: 'password'
}
]
}
}
module.exports = { credClass: OpenAIApi }

View File

@ -1,5 +1,5 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { OpenAI, OpenAIInput } from 'langchain/llms/openai'
class OpenAI_LLMs implements INode {
@ -10,6 +10,7 @@ class OpenAI_LLMs implements INode {
category: string
description: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
@ -20,12 +21,13 @@ class OpenAI_LLMs implements INode {
this.category = 'LLMs'
this.description = 'Wrapper around OpenAI large language models'
this.baseClasses = [this.type, ...getBaseClasses(OpenAI)]
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['openAIApi']
}
this.inputs = [
{
label: 'OpenAI Api Key',
name: 'openAIApiKey',
type: 'password'
},
{
label: 'Model Name',
name: 'modelName',
@ -117,10 +119,9 @@ class OpenAI_LLMs implements INode {
]
}
async init(nodeData: INodeData): Promise<any> {
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const temperature = nodeData.inputs?.temperature as string
const modelName = nodeData.inputs?.modelName as string
const openAIApiKey = nodeData.inputs?.openAIApiKey as string
const maxTokens = nodeData.inputs?.maxTokens as string
const topP = nodeData.inputs?.topP as string
const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string
@ -131,6 +132,9 @@ class OpenAI_LLMs implements INode {
const streaming = nodeData.inputs?.streaming as boolean
const basePath = nodeData.inputs?.basepath as string
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData)
const obj: Partial<OpenAIInput> & { openAIApiKey?: string } = {
temperature: parseFloat(temperature),
modelName,

View File

@ -1,5 +1,5 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { ICommonObject } from '../../../src'
import { MotorheadMemory, MotorheadMemoryInput } from 'langchain/memory'
@ -11,6 +11,7 @@ class MotorMemory_Memory implements INode {
icon: string
category: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
@ -21,6 +22,14 @@ class MotorMemory_Memory implements INode {
this.category = 'Memory'
this.description = 'Remembers previous conversational back and forths directly'
this.baseClasses = [this.type, ...getBaseClasses(MotorheadMemory)]
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
optional: true,
description: 'Only needed when using hosted solution - https://getmetal.io',
credentialNames: ['motorheadMemoryApi']
}
this.inputs = [
{
label: 'Base URL',
@ -43,22 +52,6 @@ class MotorMemory_Memory implements INode {
default: '',
additionalParams: true,
optional: true
},
{
label: 'API Key',
name: 'apiKey',
type: 'password',
description: 'Only needed when using hosted solution - https://getmetal.io',
additionalParams: true,
optional: true
},
{
label: 'Client ID',
name: 'clientId',
type: 'string',
description: 'Only needed when using hosted solution - https://getmetal.io',
additionalParams: true,
optional: true
}
]
}
@ -67,11 +60,13 @@ class MotorMemory_Memory implements INode {
const memoryKey = nodeData.inputs?.memoryKey as string
const baseURL = nodeData.inputs?.baseURL as string
const sessionId = nodeData.inputs?.sessionId as string
const apiKey = nodeData.inputs?.apiKey as string
const clientId = nodeData.inputs?.clientId as string
const chatId = options?.chatId as string
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const apiKey = getCredentialParam('apiKey', credentialData, nodeData)
const clientId = getCredentialParam('clientId', credentialData, nodeData)
let obj: MotorheadMemoryInput = {
returnMessages: true,
sessionId: sessionId ? sessionId : chatId,

View File

@ -0,0 +1,29 @@
import { INodeParams, INodeCredential } from '../../../src/Interface'
class MotorheadMemoryApi implements INodeCredential {
label: string
name: string
description: string
inputs: INodeParams[]
constructor() {
this.label = 'Motorhead Memory API'
this.name = 'motorheadMemoryApi'
this.description =
'Refer to <a target="_blank" href="https://docs.getmetal.io/misc-get-keys">official guide</a> on how to create API key and Client ID on Motorhead Memory'
this.inputs = [
{
label: 'Client ID',
name: 'clientId',
type: 'string'
},
{
label: 'API Key',
name: 'apiKey',
type: 'password'
}
]
}
}
module.exports = { credClass: MotorheadMemoryApi }

View File

@ -1,6 +1,6 @@
import { SystemMessage } from 'langchain/schema'
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { ZepMemory, ZepMemoryInput } from 'langchain/memory/zep'
import { ICommonObject } from '../../../src'
@ -12,6 +12,7 @@ class ZepMemory_Memory implements INode {
icon: string
category: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
@ -22,6 +23,14 @@ class ZepMemory_Memory implements INode {
this.category = 'Memory'
this.description = 'Summarizes the conversation and stores the memory in zep server'
this.baseClasses = [this.type, ...getBaseClasses(ZepMemory)]
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
optional: true,
description: 'Configure JWT authentication on your Zep instance (Optional)',
credentialNames: ['zepMemoryApi']
}
this.inputs = [
{
label: 'Base URL',
@ -44,18 +53,12 @@ class ZepMemory_Memory implements INode {
additionalParams: true,
optional: true
},
{
label: 'API Key',
name: 'apiKey',
type: 'password',
additionalParams: true,
optional: true
},
{
label: 'Size',
name: 'k',
type: 'number',
default: '10',
step: 1,
description: 'Window of size k to surface the last k back-and-forths to use as memory.'
},
{
@ -112,11 +115,13 @@ class ZepMemory_Memory implements INode {
const autoSummaryTemplate = nodeData.inputs?.autoSummaryTemplate as string
const autoSummary = nodeData.inputs?.autoSummary as boolean
const sessionId = nodeData.inputs?.sessionId as string
const apiKey = nodeData.inputs?.apiKey as string
const k = nodeData.inputs?.k as string
const chatId = options?.chatId as string
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const apiKey = getCredentialParam('apiKey', credentialData, nodeData)
const obj: ZepMemoryInput = {
baseURL,
sessionId: sessionId ? sessionId : chatId,

View File

@ -0,0 +1,24 @@
import { INodeParams, INodeCredential } from '../../../src/Interface'
class ZepMemoryApi implements INodeCredential {
label: string
name: string
description: string
inputs: INodeParams[]
constructor() {
this.label = 'Zep Memory Api'
this.name = 'zepMemoryApi'
this.description =
'Refer to <a target="_blank" href="https://docs.getzep.com/deployment/auth/">official guide</a> on how to create API key on Zep'
this.inputs = [
{
label: 'API Key',
name: 'apiKey',
type: 'password'
}
]
}
}
module.exports = { credClass: ZepMemoryApi }

View File

@ -59,7 +59,9 @@ export interface INodeParams {
description?: string
warning?: string
options?: Array<INodeOptionsValue>
credentialNames?: Array<string>
optional?: boolean | INodeDisplay
step?: number
rows?: number
list?: boolean
acceptVariable?: boolean
@ -102,10 +104,18 @@ export interface INodeData extends INodeProperties {
id: string
inputs?: ICommonObject
outputs?: ICommonObject
credential?: string
instance?: any
loadMethod?: string // method to load async options
}
export interface INodeCredential {
label: string
name: string
description?: string
inputs?: INodeParams[]
}
export interface IMessage {
message: string
type: MessageType

View File

@ -6,6 +6,9 @@ import { JSDOM } from 'jsdom'
import { BaseCallbackHandler } from 'langchain/callbacks'
import { Server } from 'socket.io'
import { ChainValues } from 'langchain/dist/schema'
import { DataSource } from 'typeorm'
import { ICommonObject, IDatabaseEntity, INodeData } from './Interface'
import { AES, enc } from 'crypto-js'
export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}}
export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank
@ -350,6 +353,89 @@ export const getEnvironmentVariable = (name: string): string | undefined => {
}
}
/**
* Returns the path of encryption key
* @returns {string}
*/
const getEncryptionKeyFilePath = (): string => {
const checkPaths = [
path.join(__dirname, '..', '..', 'server', 'encryption.key'),
path.join(__dirname, '..', '..', '..', 'server', 'encryption.key'),
path.join(__dirname, '..', '..', '..', '..', 'server', 'encryption.key'),
path.join(__dirname, '..', '..', '..', '..', '..', 'server', 'encryption.key')
]
for (const checkPath of checkPaths) {
if (fs.existsSync(checkPath)) {
return checkPath
}
}
return ''
}
const getEncryptionKeyPath = (): string => {
return process.env.SECRETKEY_PATH ? path.join(process.env.SECRETKEY_PATH, 'encryption.key') : getEncryptionKeyFilePath()
}
/**
* Returns the encryption key
* @returns {Promise<string>}
*/
const getEncryptionKey = async (): Promise<string> => {
try {
return await fs.promises.readFile(getEncryptionKeyPath(), 'utf8')
} catch (error) {
throw new Error(error)
}
}
/**
* Decrypt credential data
* @param {string} encryptedData
* @param {string} componentCredentialName
* @param {IComponentCredentials} componentCredentials
* @returns {Promise<ICommonObject>}
*/
const decryptCredentialData = async (encryptedData: string): Promise<ICommonObject> => {
const encryptKey = await getEncryptionKey()
const decryptedData = AES.decrypt(encryptedData, encryptKey)
try {
return JSON.parse(decryptedData.toString(enc.Utf8))
} catch (e) {
console.error(e)
throw new Error('Credentials could not be decrypted.')
}
}
/**
* Get credential data
* @param {string} selectedCredentialId
* @param {ICommonObject} options
* @returns {Promise<ICommonObject>}
*/
export const getCredentialData = async (selectedCredentialId: string, options: ICommonObject): Promise<ICommonObject> => {
const appDataSource = options.appDataSource as DataSource
const databaseEntities = options.databaseEntities as IDatabaseEntity
try {
const credential = await appDataSource.getRepository(databaseEntities['Credential']).findOneBy({
id: selectedCredentialId
})
if (!credential) throw new Error(`Credential ${selectedCredentialId} not found`)
// Decrpyt credentialData
const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
return decryptedCredentialData
} catch (e) {
throw new Error(e)
}
}
export const getCredentialParam = (paramName: string, credentialData: ICommonObject, nodeData: INodeData): any => {
return (nodeData.inputs as ICommonObject)[paramName] ?? credentialData[paramName]
}
/**
* Custom chain handler class
*/

View File

@ -48,6 +48,7 @@
"@oclif/core": "^1.13.10",
"axios": "^0.27.2",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"express-basic-auth": "^1.2.1",
@ -63,6 +64,7 @@
},
"devDependencies": {
"@types/cors": "^2.8.12",
"@types/crypto-js": "^4.1.1",
"@types/multer": "^1.4.7",
"concurrently": "^7.1.0",
"nodemon": "^2.0.15",

View File

@ -5,6 +5,7 @@ import { DataSource } from 'typeorm'
import { ChatFlow } from './entity/ChatFlow'
import { ChatMessage } from './entity/ChatMessage'
import { Tool } from './entity/Tool'
import { Credential } from './entity/Credential'
export class ChildProcess {
/**
@ -133,7 +134,7 @@ async function initDB() {
type: 'sqlite',
database: path.resolve(homePath, 'database.sqlite'),
synchronize: true,
entities: [ChatFlow, ChatMessage, Tool],
entities: [ChatFlow, ChatMessage, Tool, Credential],
migrations: []
})
return await childAppDataSource.initialize()

View File

@ -3,6 +3,7 @@ import path from 'path'
import { DataSource } from 'typeorm'
import { ChatFlow } from './entity/ChatFlow'
import { ChatMessage } from './entity/ChatMessage'
import { Credential } from './entity/Credential'
import { Tool } from './entity/Tool'
import { getUserHome } from './utils'
@ -15,7 +16,7 @@ export const init = async (): Promise<void> => {
type: 'sqlite',
database: path.resolve(homePath, 'database.sqlite'),
synchronize: true,
entities: [ChatFlow, ChatMessage, Tool],
entities: [ChatFlow, ChatMessage, Tool, Credential],
migrations: []
})
}

View File

@ -38,10 +38,23 @@ export interface ITool {
createdDate: Date
}
export interface ICredential {
id: string
name: string
credentialName: string
encryptedData: string
updatedDate: Date
createdDate: Date
}
export interface IComponentNodes {
[key: string]: INode
}
export interface IComponentCredentials {
[key: string]: INode
}
export interface IVariableDict {
[key: string]: string
}
@ -167,3 +180,17 @@ export interface IChildProcessMessage {
key: string
value?: any
}
export type ICredentialDataDecrypted = ICommonObject
// Plain credential object sent to server
export interface ICredentialReqBody {
name: string
credentialName: string
plainDataObj: ICredentialDataDecrypted
}
// Decrypted credential object sent back to client
export interface ICredentialReturnResponse extends ICredential {
plainDataObj: ICredentialDataDecrypted
}

View File

@ -1,23 +1,33 @@
import { IComponentNodes } from './Interface'
import { IComponentNodes, IComponentCredentials } from './Interface'
import path from 'path'
import { Dirent } from 'fs'
import { getNodeModulesPackagePath } from './utils'
import { promises } from 'fs'
import { ICommonObject } from 'flowise-components'
export class NodesPool {
componentNodes: IComponentNodes = {}
componentCredentials: IComponentCredentials = {}
private credentialIconPath: ICommonObject = {}
/**
* Initialize to get all nodes
* Initialize to get all nodes & credentials
*/
async initialize() {
await this.initializeNodes()
await this.initializeCrdentials()
}
/**
* Initialize nodes
*/
private async initializeNodes() {
const packagePath = getNodeModulesPackagePath('flowise-components')
const nodesPath = path.join(packagePath, 'dist', 'nodes')
const nodeFiles = await this.getFiles(nodesPath)
return Promise.all(
nodeFiles.map(async (file) => {
if (file.endsWith('.js')) {
if (file.endsWith('.js') && !file.endsWith('.credential.js')) {
const nodeModule = await require(file)
if (nodeModule.nodeClass) {
@ -37,6 +47,13 @@ export class NodesPool {
filePath.pop()
const nodeIconAbsolutePath = `${filePath.join('/')}/${newNodeInstance.icon}`
this.componentNodes[newNodeInstance.name].icon = nodeIconAbsolutePath
// Store icon path for componentCredentials
if (newNodeInstance.credential) {
for (const credName of newNodeInstance.credential.credentialNames) {
this.credentialIconPath[credName] = nodeIconAbsolutePath
}
}
}
}
}
@ -44,12 +61,33 @@ export class NodesPool {
)
}
/**
* Initialize credentials
*/
private async initializeCrdentials() {
const packagePath = getNodeModulesPackagePath('flowise-components')
const nodesPath = path.join(packagePath, 'dist', 'nodes')
const nodeFiles = await this.getFiles(nodesPath)
return Promise.all(
nodeFiles.map(async (file) => {
if (file.endsWith('.credential.js')) {
const credentialModule = await require(file)
if (credentialModule.credClass) {
const newCredInstance = new credentialModule.credClass()
newCredInstance.icon = this.credentialIconPath[newCredInstance.name] ?? ''
this.componentCredentials[newCredInstance.name] = newCredInstance
}
}
})
)
}
/**
* Recursive function to get node files
* @param {string} dir
* @returns {string[]}
*/
async getFiles(dir: string): Promise<string[]> {
private async getFiles(dir: string): Promise<string[]> {
const dirents = await promises.readdir(dir, { withFileTypes: true })
const files = await Promise.all(
dirents.map((dirent: Dirent) => {

View File

@ -0,0 +1,24 @@
/* eslint-disable */
import { Entity, Column, PrimaryGeneratedColumn, Index, CreateDateColumn, UpdateDateColumn } from 'typeorm'
import { ICredential } from '../Interface'
@Entity()
export class Credential implements ICredential {
@PrimaryGeneratedColumn('uuid')
id: string
@Column()
name: string
@Column()
credentialName: string
@Column()
encryptedData: string
@CreateDateColumn()
createdDate: Date
@UpdateDateColumn()
updatedDate: Date
}

View File

@ -17,7 +17,8 @@ import {
INodeData,
IDatabaseExport,
IRunChatflowMessageValue,
IChildProcessMessage
IChildProcessMessage,
ICredentialReturnResponse
} from './Interface'
import {
getNodeModulesPackagePath,
@ -39,17 +40,20 @@ import {
isFlowValidForStream,
isVectorStoreFaiss,
databaseEntities,
getApiKey
getApiKey,
transformToCredentialEntity,
decryptCredentialData
} from './utils'
import { cloneDeep } from 'lodash'
import { cloneDeep, omit } from 'lodash'
import { getDataSource } from './DataSource'
import { NodesPool } from './NodesPool'
import { ChatFlow } from './entity/ChatFlow'
import { ChatMessage } from './entity/ChatMessage'
import { Credential } from './entity/Credential'
import { Tool } from './entity/Tool'
import { ChatflowPool } from './ChatflowPool'
import { ICommonObject, INodeOptionsValue } from 'flowise-components'
import { fork } from 'child_process'
import { Tool } from './entity/Tool'
export class App {
app: express.Application
@ -70,10 +74,11 @@ export class App {
.then(async () => {
logger.info('📦 [server]: Data Source has been initialized!')
// Initialize pools
// Initialize nodes pool
this.nodesPool = new NodesPool()
await this.nodesPool.initialize()
// Initialize chatflow pool
this.chatflowPool = new ChatflowPool()
// Initialize API keys
@ -104,6 +109,7 @@ export class App {
'/api/v1/public-chatflows',
'/api/v1/prediction/',
'/api/v1/node-icon/',
'/api/v1/components-credentials-icon/',
'/api/v1/chatflows-streaming'
]
this.app.use((req, res, next) => {
@ -116,7 +122,7 @@ export class App {
const upload = multer({ dest: `${path.join(__dirname, '..', 'uploads')}/` })
// ----------------------------------------
// Nodes
// Components
// ----------------------------------------
// Get all component nodes
@ -129,6 +135,16 @@ export class App {
return res.json(returnData)
})
// Get all component credentials
this.app.get('/api/v1/components-credentials', async (req: Request, res: Response) => {
const returnData = []
for (const credName in this.nodesPool.componentCredentials) {
const clonedCred = cloneDeep(this.nodesPool.componentCredentials[credName])
returnData.push(clonedCred)
}
return res.json(returnData)
})
// Get specific component node via name
this.app.get('/api/v1/nodes/:name', (req: Request, res: Response) => {
if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) {
@ -138,6 +154,27 @@ export class App {
}
})
// Get component credential via name
this.app.get('/api/v1/components-credentials/:name', (req: Request, res: Response) => {
if (!req.params.name.includes('&')) {
if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) {
return res.json(this.nodesPool.componentCredentials[req.params.name])
} else {
throw new Error(`Credential ${req.params.name} not found`)
}
} else {
const returnResponse = []
for (const name of req.params.name.split('&')) {
if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) {
returnResponse.push(this.nodesPool.componentCredentials[name])
} else {
throw new Error(`Credential ${name} not found`)
}
}
return res.json(returnResponse)
}
})
// Returns specific component node icon via name
this.app.get('/api/v1/node-icon/:name', (req: Request, res: Response) => {
if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) {
@ -157,6 +194,25 @@ export class App {
}
})
// Returns specific component credential icon via name
this.app.get('/api/v1/components-credentials-icon/:name', (req: Request, res: Response) => {
if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) {
const credInstance = this.nodesPool.componentCredentials[req.params.name]
if (credInstance.icon === undefined) {
throw new Error(`Credential ${req.params.name} icon not found`)
}
if (credInstance.icon.endsWith('.svg') || credInstance.icon.endsWith('.png') || credInstance.icon.endsWith('.jpg')) {
const filepath = credInstance.icon
res.sendFile(filepath)
} else {
throw new Error(`Credential ${req.params.name} icon is missing icon`)
}
} else {
throw new Error(`Credential ${req.params.name} not found`)
}
})
// load async options
this.app.post('/api/v1/node-load-method/:name', async (req: Request, res: Response) => {
const nodeData: INodeData = req.body
@ -324,6 +380,91 @@ export class App {
return res.json(results)
})
// ----------------------------------------
// Credentials
// ----------------------------------------
// Create new credential
this.app.post('/api/v1/credentials', async (req: Request, res: Response) => {
const body = req.body
const newCredential = await transformToCredentialEntity(body)
const credential = this.AppDataSource.getRepository(Credential).create(newCredential)
const results = await this.AppDataSource.getRepository(Credential).save(credential)
return res.json(results)
})
// Get all credentials
this.app.get('/api/v1/credentials', async (req: Request, res: Response) => {
if (req.query.credentialName) {
let returnCredentials = []
if (Array.isArray(req.query.credentialName)) {
for (let i = 0; i < req.query.credentialName.length; i += 1) {
const name = req.query.credentialName[i] as string
const credentials = await this.AppDataSource.getRepository(Credential).findBy({
credentialName: name
})
returnCredentials.push(...credentials)
}
} else {
const credentials = await this.AppDataSource.getRepository(Credential).findBy({
credentialName: req.query.credentialName as string
})
returnCredentials = [...credentials]
}
return res.json(returnCredentials)
} else {
const credentials = await this.AppDataSource.getRepository(Credential).find()
const returnCredentials = []
for (const credential of credentials) {
returnCredentials.push(omit(credential, ['encryptedData']))
}
return res.json(returnCredentials)
}
})
// Get specific credential
this.app.get('/api/v1/credentials/:id', async (req: Request, res: Response) => {
const credential = await this.AppDataSource.getRepository(Credential).findOneBy({
id: req.params.id
})
if (!credential) return res.status(404).send(`Credential ${req.params.id} not found`)
// Decrpyt credentialData
const decryptedCredentialData = await decryptCredentialData(
credential.encryptedData,
credential.credentialName,
this.nodesPool.componentCredentials
)
const returnCredential: ICredentialReturnResponse = {
...credential,
plainDataObj: decryptedCredentialData
}
return res.json(omit(returnCredential, ['encryptedData']))
})
// Update credential
this.app.put('/api/v1/credentials/:id', async (req: Request, res: Response) => {
const credential = await this.AppDataSource.getRepository(Credential).findOneBy({
id: req.params.id
})
if (!credential) return res.status(404).send(`Credential ${req.params.id} not found`)
const body = req.body
const updateCredential = await transformToCredentialEntity(body)
this.AppDataSource.getRepository(Credential).merge(credential, updateCredential)
const result = await this.AppDataSource.getRepository(Credential).save(credential)
return res.json(result)
})
// Delete all chatmessages from chatflowid
this.app.delete('/api/v1/credentials/:id', async (req: Request, res: Response) => {
const results = await this.AppDataSource.getRepository(Credential).delete({ id: req.params.id })
return res.json(results)
})
// ----------------------------------------
// Tools
// ----------------------------------------
@ -393,7 +534,7 @@ export class App {
const flowData = chatflow.flowData
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
const nodes = parsedFlowData.nodes
const availableConfigs = findAvailableConfigs(nodes)
const availableConfigs = findAvailableConfigs(nodes, this.nodesPool.componentCredentials)
return res.json(availableConfigs)
})

View File

@ -13,18 +13,26 @@ import {
IReactFlowNode,
IVariableDict,
INodeData,
IOverrideConfig
IOverrideConfig,
ICredentialDataDecrypted,
IComponentCredentials,
ICredentialReqBody
} from '../Interface'
import { cloneDeep, get, omit, merge } from 'lodash'
import { ICommonObject, getInputVariables, IDatabaseEntity } from 'flowise-components'
import { scryptSync, randomBytes, timingSafeEqual } from 'crypto'
import { lib, PBKDF2, AES, enc } from 'crypto-js'
import { ChatFlow } from '../entity/ChatFlow'
import { ChatMessage } from '../entity/ChatMessage'
import { Credential } from '../entity/Credential'
import { Tool } from '../entity/Tool'
import { DataSource } from 'typeorm'
const QUESTION_VAR_PREFIX = 'question'
export const databaseEntities: IDatabaseEntity = { ChatFlow: ChatFlow, ChatMessage: ChatMessage, Tool: Tool }
const REDACTED_CREDENTIAL_VALUE = '_FLOWISE_BLANK_07167752-1a71-43b1-bf8f-4f32252165db'
export const databaseEntities: IDatabaseEntity = { ChatFlow: ChatFlow, ChatMessage: ChatMessage, Tool: Tool, Credential: Credential }
/**
* Returns the home folder path of the user if
@ -399,9 +407,8 @@ export const replaceInputsWithConfig = (flowNodeData: INodeData, overrideConfig:
const types = 'inputs'
const getParamValues = (paramsObj: ICommonObject) => {
for (const key in paramsObj) {
const paramValue: string = paramsObj[key]
paramsObj[key] = overrideConfig[key] ?? paramValue
for (const config in overrideConfig) {
paramsObj[config] = overrideConfig[config]
}
}
@ -623,11 +630,12 @@ export const mapMimeTypeToInputField = (mimeType: string) => {
}
/**
* Find all available inpur params config
* Find all available input params config
* @param {IReactFlowNode[]} reactFlowNodes
* @returns {Promise<IOverrideConfig[]>}
* @param {IComponentCredentials} componentCredentials
* @returns {IOverrideConfig[]}
*/
export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[]) => {
export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[], componentCredentials: IComponentCredentials) => {
const configs: IOverrideConfig[] = []
for (const flowNode of reactFlowNodes) {
@ -653,6 +661,23 @@ export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[]) => {
.join(', ')
: 'string'
}
} else if (inputParam.type === 'credential') {
// get component credential inputs
for (const name of inputParam.credentialNames ?? []) {
if (Object.prototype.hasOwnProperty.call(componentCredentials, name)) {
const inputs = componentCredentials[name]?.inputs ?? []
for (const input of inputs) {
obj = {
node: flowNode.data.label,
label: input.label,
name: input.name,
type: input.type === 'password' ? 'string' : input.type
}
configs.push(obj)
}
}
}
continue
} else {
obj = {
node: flowNode.data.label,
@ -705,3 +730,118 @@ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNod
return isChatOrLLMsExist && isValidChainOrAgent && !isVectorStoreFaiss(endingNodeData) && process.env.EXECUTION_MODE !== 'child'
}
/**
* Returns the path of encryption key
* @returns {string}
*/
export const getEncryptionKeyPath = (): string => {
return process.env.SECRETKEY_PATH
? path.join(process.env.SECRETKEY_PATH, 'encryption.key')
: path.join(__dirname, '..', '..', 'encryption.key')
}
/**
* Generate an encryption key
* @returns {string}
*/
export const generateEncryptKey = (): string => {
const salt = lib.WordArray.random(128 / 8)
const key256Bits = PBKDF2(process.env.PASSPHRASE || 'MYPASSPHRASE', salt, {
keySize: 256 / 32,
iterations: 1000
})
return key256Bits.toString()
}
/**
* Returns the encryption key
* @returns {Promise<string>}
*/
export const getEncryptionKey = async (): Promise<string> => {
try {
return await fs.promises.readFile(getEncryptionKeyPath(), 'utf8')
} catch (error) {
const encryptKey = generateEncryptKey()
await fs.promises.writeFile(getEncryptionKeyPath(), encryptKey)
return encryptKey
}
}
/**
* Encrypt credential data
* @param {ICredentialDataDecrypted} plainDataObj
* @returns {Promise<string>}
*/
export const encryptCredentialData = async (plainDataObj: ICredentialDataDecrypted): Promise<string> => {
const encryptKey = await getEncryptionKey()
return AES.encrypt(JSON.stringify(plainDataObj), encryptKey).toString()
}
/**
* Decrypt credential data
* @param {string} encryptedData
* @param {string} componentCredentialName
* @param {IComponentCredentials} componentCredentials
* @returns {Promise<ICredentialDataDecrypted>}
*/
export const decryptCredentialData = async (
encryptedData: string,
componentCredentialName?: string,
componentCredentials?: IComponentCredentials
): Promise<ICredentialDataDecrypted> => {
const encryptKey = await getEncryptionKey()
const decryptedData = AES.decrypt(encryptedData, encryptKey)
try {
if (componentCredentialName && componentCredentials) {
const plainDataObj = JSON.parse(decryptedData.toString(enc.Utf8))
return redactCredentialWithPasswordType(componentCredentialName, plainDataObj, componentCredentials)
}
return JSON.parse(decryptedData.toString(enc.Utf8))
} catch (e) {
console.error(e)
throw new Error('Credentials could not be decrypted.')
}
}
/**
* Transform ICredentialBody from req to Credential entity
* @param {ICredentialReqBody} body
* @returns {Credential}
*/
export const transformToCredentialEntity = async (body: ICredentialReqBody): Promise<Credential> => {
const encryptedData = await encryptCredentialData(body.plainDataObj)
const credentialBody = {
name: body.name,
credentialName: body.credentialName,
encryptedData
}
const newCredential = new Credential()
Object.assign(newCredential, credentialBody)
return newCredential
}
/**
* Redact values that are of password type to avoid sending back to client
* @param {string} componentCredentialName
* @param {ICredentialDataDecrypted} decryptedCredentialObj
* @param {IComponentCredentials} componentCredentials
* @returns {ICredentialDataDecrypted}
*/
export const redactCredentialWithPasswordType = (
componentCredentialName: string,
decryptedCredentialObj: ICredentialDataDecrypted,
componentCredentials: IComponentCredentials
): ICredentialDataDecrypted => {
const plainDataObj = cloneDeep(decryptedCredentialObj)
for (const cred in plainDataObj) {
const inputParam = componentCredentials[componentCredentialName].inputs?.find((inp) => inp.type === 'password' && inp.name === cred)
if (inputParam) {
plainDataObj[cred] = REDACTED_CREDENTIAL_VALUE
}
}
return plainDataObj
}

View File

@ -81,7 +81,8 @@ export function expressRequestLogger(req: Request, res: Response, next: NextFunc
GET: '⬇️',
POST: '⬆️',
PUT: '🖊',
DELETE: '❌'
DELETE: '❌',
OPTION: '🔗'
}
return requetsEmojis[method] || '?'

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

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',

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 />
}
]
}

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'

View File

@ -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
}

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'
}

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

View File

@ -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

View File

@ -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}

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) {

View File

@ -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

View File

@ -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

View File

@ -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

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