Chore/read write tools update (#5275)
* add tools warning * Enhance file handling tools with security features - Introduced new input parameters: workspacePath, enforceWorkspaceBoundaries, maxFileSize, and allowedExtensions for better control over file operations. - Added validation for file paths and sizes to prevent unsafe operations. - Implemented workspace boundary checks to restrict file access based on user-defined settings.
This commit is contained in:
parent
a0dca552a2
commit
1fb12cd931
|
|
@ -1,9 +1,10 @@
|
|||
import { z } from 'zod'
|
||||
import path from 'path'
|
||||
import { StructuredTool, ToolParams } from '@langchain/core/tools'
|
||||
import { Serializable } from '@langchain/core/load/serializable'
|
||||
import { NodeFileStore } from 'langchain/stores/file/node'
|
||||
import { INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { getBaseClasses } from '../../../src/utils'
|
||||
import { getBaseClasses, getUserHome } from '../../../src/utils'
|
||||
import { SecureFileStore, FileSecurityConfig } from '../../../src/SecureFileStore'
|
||||
|
||||
abstract class BaseFileStore extends Serializable {
|
||||
abstract readFile(path: string): Promise<string>
|
||||
|
|
@ -20,30 +21,91 @@ class ReadFile_Tools implements INode {
|
|||
category: string
|
||||
baseClasses: string[]
|
||||
inputs: INodeParams[]
|
||||
warning: string
|
||||
|
||||
constructor() {
|
||||
this.label = 'Read File'
|
||||
this.name = 'readFile'
|
||||
this.version = 1.0
|
||||
this.version = 2.0
|
||||
this.type = 'ReadFile'
|
||||
this.icon = 'readfile.svg'
|
||||
this.category = 'Tools'
|
||||
this.warning = 'This tool can be used to read files from the disk. It is recommended to use this tool with caution.'
|
||||
this.description = 'Read file from disk'
|
||||
this.baseClasses = [this.type, 'Tool', ...getBaseClasses(ReadFileTool)]
|
||||
this.inputs = [
|
||||
{
|
||||
label: 'Base Path',
|
||||
name: 'basePath',
|
||||
placeholder: `C:\\Users\\User\\Desktop`,
|
||||
label: 'Workspace Path',
|
||||
name: 'workspacePath',
|
||||
placeholder: `C:\\Users\\User\\MyProject`,
|
||||
type: 'string',
|
||||
description: 'Base workspace directory for file operations. All file paths will be relative to this directory.',
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
label: 'Enforce Workspace Boundaries',
|
||||
name: 'enforceWorkspaceBoundaries',
|
||||
type: 'boolean',
|
||||
description: 'When enabled, restricts file access to the workspace directory for security. Recommended: true',
|
||||
default: true,
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
label: 'Max File Size (MB)',
|
||||
name: 'maxFileSize',
|
||||
type: 'number',
|
||||
description: 'Maximum file size in megabytes that can be read',
|
||||
default: 10,
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
label: 'Allowed Extensions',
|
||||
name: 'allowedExtensions',
|
||||
type: 'string',
|
||||
description: 'Comma-separated list of allowed file extensions (e.g., .txt,.json,.md). Leave empty to allow all.',
|
||||
placeholder: '.txt,.json,.md,.py,.js',
|
||||
optional: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async init(nodeData: INodeData): Promise<any> {
|
||||
const basePath = nodeData.inputs?.basePath as string
|
||||
const store = basePath ? new NodeFileStore(basePath) : new NodeFileStore()
|
||||
const workspacePath = nodeData.inputs?.workspacePath as string
|
||||
const enforceWorkspaceBoundaries = nodeData.inputs?.enforceWorkspaceBoundaries !== false // Default to true
|
||||
const maxFileSize = nodeData.inputs?.maxFileSize as number
|
||||
const allowedExtensions = nodeData.inputs?.allowedExtensions as string
|
||||
|
||||
// Parse allowed extensions
|
||||
const allowedExtensionsList = allowedExtensions ? allowedExtensions.split(',').map((ext) => ext.trim().toLowerCase()) : []
|
||||
|
||||
let store: BaseFileStore
|
||||
|
||||
if (workspacePath) {
|
||||
// Create secure file store with workspace boundaries
|
||||
const config: FileSecurityConfig = {
|
||||
workspacePath,
|
||||
enforceWorkspaceBoundaries,
|
||||
maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, // Convert MB to bytes
|
||||
allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
|
||||
}
|
||||
store = new SecureFileStore(config)
|
||||
} else {
|
||||
// Fallback to current working directory with security warnings
|
||||
if (enforceWorkspaceBoundaries) {
|
||||
const fallbackWorkspacePath = path.join(getUserHome(), '.flowise')
|
||||
console.warn(`[ReadFile] No workspace path specified, using ${fallbackWorkspacePath} with security restrictions`)
|
||||
store = new SecureFileStore({
|
||||
workspacePath: fallbackWorkspacePath,
|
||||
enforceWorkspaceBoundaries: true,
|
||||
maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined,
|
||||
allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
|
||||
})
|
||||
} else {
|
||||
console.warn('[ReadFile] SECURITY WARNING: Workspace boundaries disabled - unrestricted file access enabled')
|
||||
store = SecureFileStore.createUnsecure()
|
||||
}
|
||||
}
|
||||
|
||||
return new ReadFileTool({ store })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { z } from 'zod'
|
||||
import path from 'path'
|
||||
import { StructuredTool, ToolParams } from '@langchain/core/tools'
|
||||
import { Serializable } from '@langchain/core/load/serializable'
|
||||
import { NodeFileStore } from 'langchain/stores/file/node'
|
||||
import { INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { getBaseClasses } from '../../../src/utils'
|
||||
import { getBaseClasses, getUserHome } from '../../../src/utils'
|
||||
import { SecureFileStore, FileSecurityConfig } from '../../../src/SecureFileStore'
|
||||
|
||||
abstract class BaseFileStore extends Serializable {
|
||||
abstract readFile(path: string): Promise<string>
|
||||
|
|
@ -20,30 +21,91 @@ class WriteFile_Tools implements INode {
|
|||
category: string
|
||||
baseClasses: string[]
|
||||
inputs: INodeParams[]
|
||||
warning: string
|
||||
|
||||
constructor() {
|
||||
this.label = 'Write File'
|
||||
this.name = 'writeFile'
|
||||
this.version = 1.0
|
||||
this.version = 2.0
|
||||
this.type = 'WriteFile'
|
||||
this.icon = 'writefile.svg'
|
||||
this.category = 'Tools'
|
||||
this.warning = 'This tool can be used to write files to the disk. It is recommended to use this tool with caution.'
|
||||
this.description = 'Write file to disk'
|
||||
this.baseClasses = [this.type, 'Tool', ...getBaseClasses(WriteFileTool)]
|
||||
this.inputs = [
|
||||
{
|
||||
label: 'Base Path',
|
||||
name: 'basePath',
|
||||
placeholder: `C:\\Users\\User\\Desktop`,
|
||||
label: 'Workspace Path',
|
||||
name: 'workspacePath',
|
||||
placeholder: `C:\\Users\\User\\MyProject`,
|
||||
type: 'string',
|
||||
description: 'Base workspace directory for file operations. All file paths will be relative to this directory.',
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
label: 'Enforce Workspace Boundaries',
|
||||
name: 'enforceWorkspaceBoundaries',
|
||||
type: 'boolean',
|
||||
description: 'When enabled, restricts file access to the workspace directory for security. Recommended: true',
|
||||
default: true,
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
label: 'Max File Size (MB)',
|
||||
name: 'maxFileSize',
|
||||
type: 'number',
|
||||
description: 'Maximum file size in megabytes that can be written',
|
||||
default: 10,
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
label: 'Allowed Extensions',
|
||||
name: 'allowedExtensions',
|
||||
type: 'string',
|
||||
description: 'Comma-separated list of allowed file extensions (e.g., .txt,.json,.md). Leave empty to allow all.',
|
||||
placeholder: '.txt,.json,.md,.py,.js',
|
||||
optional: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async init(nodeData: INodeData): Promise<any> {
|
||||
const basePath = nodeData.inputs?.basePath as string
|
||||
const store = basePath ? new NodeFileStore(basePath) : new NodeFileStore()
|
||||
const workspacePath = nodeData.inputs?.workspacePath as string
|
||||
const enforceWorkspaceBoundaries = nodeData.inputs?.enforceWorkspaceBoundaries !== false // Default to true
|
||||
const maxFileSize = nodeData.inputs?.maxFileSize as number
|
||||
const allowedExtensions = nodeData.inputs?.allowedExtensions as string
|
||||
|
||||
// Parse allowed extensions
|
||||
const allowedExtensionsList = allowedExtensions ? allowedExtensions.split(',').map((ext) => ext.trim().toLowerCase()) : []
|
||||
|
||||
let store: BaseFileStore
|
||||
|
||||
if (workspacePath) {
|
||||
// Create secure file store with workspace boundaries
|
||||
const config: FileSecurityConfig = {
|
||||
workspacePath,
|
||||
enforceWorkspaceBoundaries,
|
||||
maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, // Convert MB to bytes
|
||||
allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
|
||||
}
|
||||
store = new SecureFileStore(config)
|
||||
} else {
|
||||
// Fallback to current working directory with security warnings
|
||||
if (enforceWorkspaceBoundaries) {
|
||||
const fallbackWorkspacePath = path.join(getUserHome(), '.flowise')
|
||||
console.warn(`[WriteFile] No workspace path specified, using ${fallbackWorkspacePath} with security restrictions`)
|
||||
store = new SecureFileStore({
|
||||
workspacePath: fallbackWorkspacePath,
|
||||
enforceWorkspaceBoundaries: true,
|
||||
maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined,
|
||||
allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
|
||||
})
|
||||
} else {
|
||||
console.warn('[WriteFile] SECURITY WARNING: Workspace boundaries disabled - unrestricted file access enabled')
|
||||
store = SecureFileStore.createUnsecure()
|
||||
}
|
||||
}
|
||||
|
||||
return new WriteFileTool({ store })
|
||||
}
|
||||
}
|
||||
|
|
@ -68,7 +130,7 @@ export class WriteFileTool extends StructuredTool {
|
|||
|
||||
name = 'write_file'
|
||||
|
||||
description = 'Write file from disk'
|
||||
description = 'Write file to disk'
|
||||
|
||||
store: BaseFileStore
|
||||
|
||||
|
|
@ -80,7 +142,7 @@ export class WriteFileTool extends StructuredTool {
|
|||
|
||||
async _call({ file_path, text }: z.infer<typeof this.schema>) {
|
||||
await this.store.writeFile(file_path, text)
|
||||
return 'File written to successfully.'
|
||||
return `File written to ${file_path} successfully.`
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ export interface INodeProperties {
|
|||
documentation?: string
|
||||
color?: string
|
||||
hint?: string
|
||||
warning?: string
|
||||
}
|
||||
|
||||
export interface INode extends INodeProperties {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
import { Serializable } from '@langchain/core/load/serializable'
|
||||
import { NodeFileStore } from 'langchain/stores/file/node'
|
||||
import { isUnsafeFilePath, isWithinWorkspace } from './validator'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
/**
|
||||
* Security configuration for file operations
|
||||
*/
|
||||
export interface FileSecurityConfig {
|
||||
/** Base workspace path - all file operations are restricted to this directory */
|
||||
workspacePath: string
|
||||
/** Whether to enforce workspace boundaries (default: true) */
|
||||
enforceWorkspaceBoundaries?: boolean
|
||||
/** Maximum file size in bytes (default: 10MB) */
|
||||
maxFileSize?: number
|
||||
/** Allowed file extensions (if empty, all extensions allowed) */
|
||||
allowedExtensions?: string[]
|
||||
/** Blocked file extensions */
|
||||
blockedExtensions?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Secure file store that enforces workspace boundaries and validates file operations
|
||||
*/
|
||||
export class SecureFileStore extends Serializable {
|
||||
lc_namespace = ['flowise', 'components', 'stores', 'file']
|
||||
|
||||
private config: Required<FileSecurityConfig>
|
||||
private nodeFileStore: NodeFileStore
|
||||
|
||||
constructor(config: FileSecurityConfig) {
|
||||
super()
|
||||
|
||||
// Set default configuration
|
||||
this.config = {
|
||||
workspacePath: config.workspacePath,
|
||||
enforceWorkspaceBoundaries: config.enforceWorkspaceBoundaries ?? true,
|
||||
maxFileSize: config.maxFileSize ?? 10 * 1024 * 1024, // 10MB default
|
||||
allowedExtensions: config.allowedExtensions ?? [],
|
||||
blockedExtensions: config.blockedExtensions ?? [
|
||||
'.exe',
|
||||
'.bat',
|
||||
'.cmd',
|
||||
'.sh',
|
||||
'.ps1',
|
||||
'.vbs',
|
||||
'.scr',
|
||||
'.com',
|
||||
'.pif',
|
||||
'.dll',
|
||||
'.sys',
|
||||
'.msi',
|
||||
'.jar'
|
||||
]
|
||||
}
|
||||
|
||||
// Validate workspace path
|
||||
if (!this.config.workspacePath || !path.isAbsolute(this.config.workspacePath)) {
|
||||
throw new Error('Workspace path must be an absolute path')
|
||||
}
|
||||
|
||||
// Ensure workspace directory exists
|
||||
if (!fs.existsSync(this.config.workspacePath)) {
|
||||
throw new Error(`Workspace directory does not exist: ${this.config.workspacePath}`)
|
||||
}
|
||||
|
||||
// Initialize the underlying NodeFileStore with workspace path
|
||||
this.nodeFileStore = new NodeFileStore(this.config.workspacePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a file path against security policies
|
||||
*/
|
||||
private validateFilePath(filePath: string): void {
|
||||
// Check for unsafe path patterns
|
||||
if (isUnsafeFilePath(filePath)) {
|
||||
throw new Error(`Unsafe file path detected: ${filePath}`)
|
||||
}
|
||||
|
||||
// Enforce workspace boundaries if enabled
|
||||
if (this.config.enforceWorkspaceBoundaries) {
|
||||
if (!isWithinWorkspace(filePath, this.config.workspacePath)) {
|
||||
throw new Error(`File path outside workspace boundaries: ${filePath}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check file extension
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
|
||||
// Check blocked extensions
|
||||
if (this.config.blockedExtensions.includes(ext)) {
|
||||
throw new Error(`File extension not allowed: ${ext}`)
|
||||
}
|
||||
|
||||
// Check allowed extensions (if specified)
|
||||
if (this.config.allowedExtensions.length > 0 && !this.config.allowedExtensions.includes(ext)) {
|
||||
throw new Error(`File extension not in allowed list: ${ext}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates file size
|
||||
*/
|
||||
private validateFileSize(content: string): void {
|
||||
const sizeInBytes = Buffer.byteLength(content, 'utf8')
|
||||
if (sizeInBytes > this.config.maxFileSize) {
|
||||
throw new Error(`File size exceeds maximum allowed size: ${sizeInBytes} > ${this.config.maxFileSize}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a file with security validation
|
||||
*/
|
||||
async readFile(filePath: string): Promise<string> {
|
||||
this.validateFilePath(filePath)
|
||||
|
||||
try {
|
||||
return await this.nodeFileStore.readFile(filePath)
|
||||
} catch (error) {
|
||||
// Provide generic error message to avoid information leakage
|
||||
throw new Error(`Failed to read file: ${path.basename(filePath)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a file with security validation
|
||||
*/
|
||||
async writeFile(filePath: string, contents: string): Promise<void> {
|
||||
this.validateFilePath(filePath)
|
||||
this.validateFileSize(contents)
|
||||
|
||||
try {
|
||||
// Ensure the directory exists
|
||||
const dir = path.dirname(path.resolve(this.config.workspacePath, filePath))
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
await this.nodeFileStore.writeFile(filePath, contents)
|
||||
} catch (error) {
|
||||
// Provide generic error message to avoid information leakage
|
||||
throw new Error(`Failed to write file: ${path.basename(filePath)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the workspace configuration
|
||||
*/
|
||||
getConfig(): Readonly<Required<FileSecurityConfig>> {
|
||||
return { ...this.config }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a secure file store with workspace enforcement disabled (for backward compatibility)
|
||||
* WARNING: This should only be used when absolutely necessary and with proper user consent
|
||||
*/
|
||||
static createUnsecure(basePath?: string): SecureFileStore {
|
||||
const workspacePath = basePath || process.cwd()
|
||||
return new SecureFileStore({
|
||||
workspacePath,
|
||||
enforceWorkspaceBoundaries: false,
|
||||
maxFileSize: 50 * 1024 * 1024, // 50MB for unsecure mode
|
||||
blockedExtensions: [] // No extension restrictions in unsecure mode
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -41,3 +41,64 @@ export const isPathTraversal = (path: string): boolean => {
|
|||
|
||||
return dangerousPatterns.some((pattern) => path.toLowerCase().includes(pattern))
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced path validation for workspace-scoped file operations
|
||||
* @param {string} filePath The file path to validate
|
||||
* @returns {boolean} True if path traversal detected, false otherwise
|
||||
*/
|
||||
export const isUnsafeFilePath = (filePath: string): boolean => {
|
||||
if (!filePath || typeof filePath !== 'string') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for path traversal patterns
|
||||
const dangerousPatterns = [
|
||||
/\.\./, // Directory traversal (..)
|
||||
/%2e%2e/i, // URL encoded ..
|
||||
/%2f/i, // URL encoded /
|
||||
/%5c/i, // URL encoded \
|
||||
/\0/, // Null bytes
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/[\x00-\x1f]/, // Control characters
|
||||
/^\/[^/]/, // Absolute Unix paths (starting with /)
|
||||
/^[a-zA-Z]:\\/, // Absolute Windows paths (C:\)
|
||||
/^\\\\[^\\]/, // UNC paths (\\server\)
|
||||
/^\\\\\?\\/ // Extended-length paths (\\?\)
|
||||
]
|
||||
|
||||
return dangerousPatterns.some((pattern) => pattern.test(filePath))
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a file path is within the allowed workspace boundaries
|
||||
* @param {string} filePath The file path to validate
|
||||
* @param {string} workspacePath The workspace base path
|
||||
* @returns {boolean} True if path is within workspace, false otherwise
|
||||
*/
|
||||
export const isWithinWorkspace = (filePath: string, workspacePath: string): boolean => {
|
||||
if (!filePath || !workspacePath) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const path = require('path')
|
||||
|
||||
// Resolve both paths to absolute paths
|
||||
const resolvedFilePath = path.resolve(workspacePath, filePath)
|
||||
const resolvedWorkspacePath = path.resolve(workspacePath)
|
||||
|
||||
// Normalize paths to handle different separators
|
||||
const normalizedFilePath = path.normalize(resolvedFilePath)
|
||||
const normalizedWorkspacePath = path.normalize(resolvedWorkspacePath)
|
||||
|
||||
// Check if the file path starts with the workspace path
|
||||
const relativePath = path.relative(normalizedWorkspacePath, normalizedFilePath)
|
||||
|
||||
// If relative path starts with '..' or is absolute, it's outside workspace
|
||||
return !relativePath.startsWith('..') && !path.isAbsolute(relativePath)
|
||||
} catch (error) {
|
||||
// If any error occurs during path resolution, deny access
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,6 +191,8 @@ const AgentFlowNode = ({ data }) => {
|
|||
componentNode?.deprecateMessage ??
|
||||
'This node will be deprecated in the next release. Change to a new node tagged with NEW'
|
||||
)
|
||||
} else if (componentNode.warning) {
|
||||
setWarningMessage(componentNode.warning)
|
||||
} else {
|
||||
setWarningMessage('')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import PropTypes from 'prop-types'
|
|||
import { cloneDeep } from 'lodash'
|
||||
|
||||
// Material
|
||||
import { Accordion, AccordionSummary, AccordionDetails, Box, Typography } from '@mui/material'
|
||||
import { Accordion, AccordionSummary, AccordionDetails, Box, Typography, Tooltip, IconButton } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||
import { IconSettings } from '@tabler/icons-react'
|
||||
import { IconSettings, IconAlertTriangle } from '@tabler/icons-react'
|
||||
|
||||
// Project imports
|
||||
import NodeInputHandler from '../canvas/NodeInputHandler'
|
||||
|
|
@ -292,8 +292,21 @@ export const ConfigInput = ({ data, inputParam, disabled = false, arrayIndex = n
|
|||
>
|
||||
<Accordion sx={{ background: 'transparent' }} expanded={expanded} onChange={handleAccordionChange}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />} sx={{ background: 'transparent' }}>
|
||||
<IconSettings stroke={1.5} size='1.3rem' />
|
||||
<Typography sx={{ ml: 1 }}>{selectedComponentNodeData?.label} Parameters</Typography>
|
||||
<div style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<IconSettings stroke={1.5} size='1.3rem' />
|
||||
<Typography sx={{ ml: 1 }}>{selectedComponentNodeData?.label} Parameters</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
{selectedComponentNodeData?.warning && (
|
||||
<Tooltip
|
||||
title={<span style={{ whiteSpace: 'pre-line' }}>{selectedComponentNodeData.warning}</span>}
|
||||
placement='top'
|
||||
>
|
||||
<IconButton sx={{ height: 35, width: 35 }}>
|
||||
<IconAlertTriangle size={20} color='orange' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{(selectedComponentNodeData.inputParams ?? [])
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ const CanvasNode = ({ data }) => {
|
|||
componentNode?.deprecateMessage ??
|
||||
'This node will be deprecated in the next release. Change to a new node tagged with NEW'
|
||||
)
|
||||
} else if (componentNode.warning) {
|
||||
setWarningMessage(componentNode.warning)
|
||||
} else {
|
||||
setWarningMessage('')
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue