feat: Composio Component Upgrade (#6905)
* feat: Add button component and update dropdown rendering * feat: Implement ListSelectionComponent and update ButtonComponent - Add new ListSelectionComponent for selecting actions with search functionality - Refactor ButtonComponent to use ListSelectionComponent - Update ParameterRenderComponent to simplify button rendering - Modify DropdownComponent to prepare for future changes * feat: Add Sortable.js for drag-and-drop functionality and grid icon - Integrated Sortable.js library in index.html and package files - Added GridHorizontalIcon for drag handle visualization - Updated ButtonComponent with commented sortable list example - Improved input styling in ListSelectionComponent * feat: Add ButtonInput for Composio API and update input types - Introduced ButtonInput class in inputs module - Added new button input fields to ComposioAPIComponent - Updated FieldTypes enum to include BUTTON type - Modified frontend ButtonComponent layout - Removed commented-out code in DropdownComponent * [autofix.ci] apply automated fixes * feat: Enhance ButtonComponent with dynamic type and improved UI - Add type prop to ButtonComponent to support different rendering modes - Implement tool name and actions button layouts - Add sample action data for actions button type - Improve sortable list styling and interaction - Refactor ButtonComponent to handle different use cases * refactor: Improve ButtonComponent layout and Sortable configuration - Adjust Sortable animation duration - Modify list item layout and positioning - Remove unnecessary CSS classes - Simplify button and icon positioning * feat: Integrate react-sortablejs for improved drag-and-drop functionality - Replace Sortable.js with react-sortablejs library - Add TypeScript types for SortableJS - Update ButtonComponent to use ReactSortable component - Improve list item rendering and styling - Remove manual Sortable initialization * feat: Enhance ButtonComponent with authentication and dynamic action data - Add isAuthenticated state to control button visibility - Introduce initialActionData and actionData state for dynamic list management - Update button styling and icon for unauthenticated state - Enable ReactSortable to modify action data list * style: Update ButtonComponent styling with accent-themed colors * style: Refine button styling with amber accent theme * feat: Enhance ButtonComponent with dynamic rendering and improved UX * feat: Add external link to DataStax Wikipedia page on button click * feat: Implement delete action for button component action data * feat: Enhance ButtonComponent with authentication and UI improvements * feat: Enhance ListSelectionComponent with dynamic data and multi-select functionality * feat: Improve ListSelectionComponent and ButtonComponent interaction and authentication flow * refactor: Replace ButtonInput with ListSelectionInput in Composio API and input components * [autofix.ci] apply automated fixes * refactor: Optimize ListComponent with memoization and improved state management * feat: Add selection type support to Composio API and ListSelectionComponent for improved user interaction * refactor: Rename action-related props in ListSelectionComponent and ListComponent for clarity and consistency * feat: Expand ListSelectionInput options and enhance ListSelectionComponent with dynamic data and metadata support * refactor: Clean up comments and improve code readability in ListSelectionComponent and ListComponent * [autofix.ci] apply automated fixes * chore: Remove debug log from ParameterRenderComponent and add text/plain type to DRAG_EVENTS_CUSTOM_TYPES * feat: Introduce ConnectionInput and SortableListInput components, replacing ListSelectionInput; enhance ComposioAPIComponent with new input types and improve parameter rendering with connection support * [autofix.ci] apply automated fixes * feat: Enhance ConnectionComponent with new state management and selection handling; update SortableListComponent to utilize new props for improved functionality * feat: Implement SearchBarComponent to enhance ListSelectionComponent with improved search functionality and category selection * feat: Update ComposioAPIComponent with external links and enhance ConnectionComponent with improved state management and dynamic link handling * fix: Update ConnectionComponent layout and button styles for improved UI consistency * chore: Remove unused Sortable.js script from index.html to streamline frontend resources * Update index.tsx * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * Add backend support for composio actions * [autofix.ci] apply automated fixes * Update composio_api.py * Update composio_api.py * [autofix.ci] apply automated fixes * fix: enhance ListSelectionComponent styling and functionality - Updated ListItem component to accept a className prop for better customization. - Improved item name display with truncation for better UI handling. - Adjusted DialogContent styling for responsive width and layout. - Enhanced overflow handling in the selection list for better user experience. * feat: enhance ListSelectionComponent and ConnectionComponent functionality - Added onSelection prop to ListSelectionComponent for handling item selection actions. - Improved item selection logic to trigger onSelection callback. - Refactored connection handling in ConnectionComponent to manage loading state and authentication. - Updated connection link handling to ensure proper link retrieval and usage. * fix: reset search input in ListSelectionComponent after selection - Added functionality to clear the search input by setting it to an empty string when an item is selected. This improves user experience by ensuring the search field is reset after a selection is made. * fix: improve selection logic and styling in ListSelectionComponent and ConnectionComponent - Enhanced ListItem component to conditionally apply a green color for validated items. - Updated selection logic in ListSelectionComponent to consider items with a validated link as selected. - Initialized authentication state in ConnectionComponent based on the connection link status. - Adjusted connection handling to update authentication state when the link changes. * fix: refine item selection handling in ConnectionComponent - Removed the setting of selectedItem state in the handleSelection function to streamline the selection process. - Updated logic to focus on handling new value updates without maintaining a separate selected item state. * fix: enhance ConnectionComponent and ListSelectionComponent functionality - Removed console log from ListSelectionComponent to clean up the code. - Added nodeId, nodeClass, and name props to ConnectionComponent for improved data handling. - Implemented polling logic in ConnectionComponent to validate connections and manage loading states effectively. - Updated loading state handling in the button to reflect polling status. * fix: refactor ConnectionComponent for improved state management and polling logic - Streamlined prop destructuring for better readability. - Enhanced state management for polling and authentication. - Added cleanup effects for polling intervals and timeouts. - Improved event handling for connection button clicks and selection dialog. - Updated comments for clarity and organization. * Support handling the auth parameter * [autofix.ci] apply automated fixes * fix: improve key handling and state management in ListSelection and SortableList components * Updated key assignment in ListSelectionComponent to include index for uniqueness. * Enhanced SortableListComponent with useEffect to initialize listData from props and improved remove handler to update state correctly. * Refactored setListData to ensure proper state updates and value handling. * fix: update selected item handling in ConnectionComponent (#7280) * Added useEffect to set selected item based on value and options. * Modified handleSelection to ensure selected item structure is consistent. * Fix output of component in actions * fix: enhance connection handling in ConnectionComponent * Updated handleConnectionButtonClick to accept a parameter for connection checking. * Modified event handler to call handleConnectionButtonClick with the appropriate argument based on connection state. * Ensured that the connection link opens in a new tab only when the connection check is true. * [autofix.ci] apply automated fixes * Remove the search categories * fix: update list data handling in SortableListComponent * Refactored useEffect to correctly set listData when value changes. * Introduced a temporary variable to hold the value before updating state. * refactor: optimize list data management in SortableListComponent * Replaced useEffect with useMemo for listData initialization. * Simplified state update logic in createRemoveHandler and setListDataHandler. * Improved key assignment for SortableListItem to handle potential undefined names. * Check if validated before passing link * [autofix.ci] apply automated fixes * refactor: improve connection handling and state management in ConnectionComponent * Enhanced state management for tracking connection status and UI states. * Updated polling management to prevent memory leaks and ensure proper cleanup. * Simplified event handlers for connection button clicks and selection handling. * Improved comments for better code clarity and understanding. * fix: improve connection link handling in ConnectionComponent * Added logging for connectionLink to aid in debugging. * Updated useEffect to set the link only if connectionLink is not empty. * Implemented authentication state change when connectionLink is validated. * Clear list of actions when changing tools * Update composio_api.py * [autofix.ci] apply automated fixes * feat: enhance error handling in ConnectionComponent * Introduced ShadTooltip for displaying error messages. * Added state management for error visibility and updated connection link handling based on error data. * Modified button behavior and icon display based on connection status. * refactor: simplify error handling in ConnectionComponent * Removed unused state for error tooltip visibility. * Updated error handling logic to include connection link status in addition to error data. * Properly indicate error status in component * Better management of tool name helper text * refactor: streamline selected item initialization in ConnectionComponent * Simplified the logic for setting the selected item based on the value prop. * Ensured that the selected item is updated correctly when value or options change. * fix: update authentication status in ConnectionComponent * Added logic to set authentication status based on the selected option. * Ensured that authentication state is updated when no option is selected. * feat: update composio package (#7325) update composio package * Update Gmail Agent.json * refactor: optimize InputGlobalComponent logic * Removed unused imports and streamlined the useEffect and useMemo hooks for better performance. * Enhanced the handling of unavailable fields and initial load completion state. * Simplified the logic for setting the selected option based on the value and load_from_db status. * refactor: improve error handling in ConnectionComponent * Removed dependency on errorData in useEffect for setting link state. * Streamlined error handling logic to focus solely on connectionLink status. * refactor: enhance connection handling in ConnectionComponent * Merged error handling logic into a single useEffect for improved clarity. * Updated link state management to reflect the selected item's validation status. * Ensured authentication state is set correctly based on the selected item. * Filter list of available tools * refactor: update ListSelectionComponent to use limit instead of selection type * Removed the SelectionMode type and replaced it with a limit prop for selection. * Adjusted selection logic to enforce the limit on selected items. * Updated related components to reflect the new limit prop instead of type. * [autofix.ci] apply automated fixes * Add limit field and santiziation * Update inputs.py * Update composio_api.py * Show actions only if a tool is selected * [autofix.ci] apply automated fixes * refactor: add limit prop to ParameterRenderComponent and SortableListComponent * Introduced a limit prop to both components to control item rendering and selection behavior. * Updated SortableListItem to conditionally render elements based on the limit value. * Adjusted button visibility and styles in SortableListComponent according to the limit prop. * fix: adjust styles in SortableListItem based on limit prop * Updated height and padding styles for SortableListItem when limit is set to 1. * Ensured consistent spacing and visibility of elements based on the limit value. * Push latest template * style: enhance SortableListItem styles for improved interaction * Updated class names to ensure proper styling for hover and group states. * Improved visibility of the remove button based on the limit prop, enhancing user experience. * Update composio_api.py * style: update SortableListItem max-width for improved layout * Adjusted max-width class for SortableListItem when limit is set to 1, enhancing the component's visual consistency. * Update Gmail Agent.json * style: enhance ListItem component for better interaction * Updated class names to improve hover effects and selected state styling. * Added rounded corners and background color changes for better visual feedback. * style: refine ListSelectionComponent and SortableListComponent layout * Simplified class names in ListItem for cleaner styling. * Moved HelperTextComponent into a dedicated div in SortableListComponent for improved layout and spacing. * refactor: clean up ConnectionComponent code * Removed unused imports and comments for better readability. * Updated polling timeout duration from 30 seconds to 9 seconds to prevent indefinite polling. * Fix bug with link on refresh * Update Gmail Agent.json * Update Gmail Agent.json * Update Gmail Agent.json --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Hare <ericrhare@gmail.com> Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
This commit is contained in:
1
.composio.lock
Normal file
1
.composio.lock
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -66,8 +66,8 @@ dependencies = [
|
||||
"yfinance==0.2.50",
|
||||
"wolframalpha==5.1.3",
|
||||
"astra-assistants[tools]~=2.2.11",
|
||||
"composio-langchain==0.7.1",
|
||||
"composio-core==0.7.1",
|
||||
"composio-langchain==0.7.12",
|
||||
"composio-core==0.7.12",
|
||||
"spider-client==0.1.24",
|
||||
"nltk==3.9.1",
|
||||
"lark==1.2.2",
|
||||
|
||||
@@ -2,20 +2,25 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from composio import Action, App
|
||||
|
||||
# Third-party imports
|
||||
from composio.client.collections import AppAuthScheme
|
||||
from composio.client.exceptions import NoItemsFound
|
||||
from composio_langchain import Action, ComposioToolSet
|
||||
from composio_langchain import ComposioToolSet
|
||||
from langchain_core.tools import Tool
|
||||
from loguru import logger
|
||||
|
||||
# Local imports
|
||||
from langflow.base.langchain_utilities.model import LCToolComponent
|
||||
from langflow.inputs import DropdownInput, LinkInput, MessageTextInput, MultiselectInput, SecretStrInput, StrInput
|
||||
from langflow.inputs import (
|
||||
ConnectionInput,
|
||||
MessageTextInput,
|
||||
SecretStrInput,
|
||||
SortableListInput,
|
||||
)
|
||||
from langflow.io import Output
|
||||
|
||||
# TODO: We get the list from the API but we need to filter it
|
||||
enabled_tools = ["confluence", "discord", "dropbox", "github", "gmail", "linkedin", "notion", "slack", "youtube"]
|
||||
|
||||
|
||||
class ComposioAPIComponent(LCToolComponent):
|
||||
display_name: str = "Composio Tools"
|
||||
@@ -34,58 +39,28 @@ class ComposioAPIComponent(LCToolComponent):
|
||||
info="Refer to https://docs.composio.dev/faq/api_key/api_key",
|
||||
real_time_refresh=True,
|
||||
),
|
||||
DropdownInput(
|
||||
name="app_names",
|
||||
display_name="App Name",
|
||||
ConnectionInput(
|
||||
name="tool_name",
|
||||
display_name="Tool Name",
|
||||
placeholder="Select a tool...",
|
||||
button_metadata={"icon": "unplug", "variant": "destructive"},
|
||||
options=[],
|
||||
search_category=[],
|
||||
value="",
|
||||
connection_link="",
|
||||
info="The name of the tool to use",
|
||||
real_time_refresh=True,
|
||||
),
|
||||
SortableListInput(
|
||||
name="actions",
|
||||
display_name="Actions",
|
||||
placeholder="Select action",
|
||||
helper_text="Please connect before selecting actions.",
|
||||
helper_text_metadata={"icon": "OctagonAlert", "variant": "destructive"},
|
||||
options=[],
|
||||
value="",
|
||||
info="The app name to use. Please refresh after selecting app name",
|
||||
refresh_button=True,
|
||||
required=True,
|
||||
),
|
||||
# Authentication-related inputs (initially hidden)
|
||||
SecretStrInput(
|
||||
name="app_credentials",
|
||||
display_name="App Credentials",
|
||||
required=False,
|
||||
dynamic=True,
|
||||
show=False,
|
||||
info="Credentials for app authentication (API Key, Password, etc)",
|
||||
load_from_db=False,
|
||||
),
|
||||
MessageTextInput(
|
||||
name="username",
|
||||
display_name="Username",
|
||||
required=False,
|
||||
dynamic=True,
|
||||
show=False,
|
||||
info="Username for Basic authentication",
|
||||
),
|
||||
LinkInput(
|
||||
name="auth_link",
|
||||
display_name="Authentication Link",
|
||||
value="",
|
||||
info="Click to authenticate with OAuth2",
|
||||
dynamic=True,
|
||||
show=False,
|
||||
placeholder="Click to authenticate",
|
||||
),
|
||||
StrInput(
|
||||
name="auth_status",
|
||||
display_name="Auth Status",
|
||||
value="Not Connected",
|
||||
info="Current authentication status",
|
||||
dynamic=True,
|
||||
show=False,
|
||||
),
|
||||
MultiselectInput(
|
||||
name="action_names",
|
||||
display_name="Actions to use",
|
||||
required=True,
|
||||
options=[],
|
||||
value=[],
|
||||
info="The actions to pass to agent to execute",
|
||||
dynamic=True,
|
||||
info="The actions to use",
|
||||
limit=1,
|
||||
show=False,
|
||||
),
|
||||
]
|
||||
@@ -94,256 +69,191 @@ class ComposioAPIComponent(LCToolComponent):
|
||||
Output(name="tools", display_name="Tools", method="build_tool"),
|
||||
]
|
||||
|
||||
def _check_for_authorization(self, app: str) -> str:
|
||||
"""Checks if the app is authorized.
|
||||
def sanitize_action_name(self, action_name: str) -> str:
|
||||
# TODO: Maybe restore
|
||||
return action_name
|
||||
|
||||
Args:
|
||||
app (str): The app name to check authorization for.
|
||||
# We want to use title case, and replace underscores with spaces
|
||||
sanitized_name = action_name.replace("_", " ").title()
|
||||
|
||||
Returns:
|
||||
str: The authorization status or URL.
|
||||
"""
|
||||
toolset = self._build_wrapper()
|
||||
entity = toolset.client.get_entity(id=self.entity_id)
|
||||
try:
|
||||
# Check if user is already connected
|
||||
entity.get_connection(app=app)
|
||||
except NoItemsFound:
|
||||
# Get auth scheme for the app
|
||||
auth_scheme = self._get_auth_scheme(app)
|
||||
return self._handle_auth_by_scheme(entity, app, auth_scheme)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.exception("Authorization error")
|
||||
return "Error checking authorization"
|
||||
else:
|
||||
return f"{app} CONNECTED"
|
||||
# Now we want to remove everything from and including the first dot
|
||||
return sanitized_name.replace(self.tool_name.title() + " ", "")
|
||||
|
||||
def _get_auth_scheme(self, app_name: str) -> AppAuthScheme:
|
||||
"""Get the primary auth scheme for an app.
|
||||
def desanitize_action_name(self, action_name: str) -> str:
|
||||
# TODO: Maybe restore
|
||||
return action_name
|
||||
|
||||
Args:
|
||||
app_name (str): The name of the app to get auth scheme for.
|
||||
# We want to reverse what we did above
|
||||
unsanitized_name = action_name.replace(" ", "_").upper()
|
||||
|
||||
Returns:
|
||||
AppAuthScheme: The auth scheme details.
|
||||
"""
|
||||
toolset = self._build_wrapper()
|
||||
try:
|
||||
return toolset.get_auth_scheme_for_app(app=app_name.lower())
|
||||
except Exception: # noqa: BLE001
|
||||
logger.exception(f"Error getting auth scheme for {app_name}")
|
||||
return None
|
||||
# Append the tool_name to it at the beginning, followed by a dot, in all CAPS
|
||||
return f"{self.tool_name.upper()}_{unsanitized_name}"
|
||||
|
||||
def _get_oauth_apps(self, api_key: str) -> list[str]:
|
||||
"""Fetch OAuth-enabled apps from Composio API.
|
||||
def validate_tool(self, build_config: dict, field_value: Any, connected_app_names: list) -> dict:
|
||||
# Get the index of the selected tool in the list of options
|
||||
selected_tool_index = next(
|
||||
(
|
||||
ind
|
||||
for ind, tool in enumerate(build_config["tool_name"]["options"])
|
||||
if tool["name"] == field_value
|
||||
or ("validate" in field_value and tool["name"] == field_value["validate"])
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
Args:
|
||||
api_key (str): The Composio API key.
|
||||
# Set the link to be the text 'validated'
|
||||
build_config["tool_name"]["options"][selected_tool_index]["link"] = "validated"
|
||||
|
||||
Returns:
|
||||
list[str]: A list containing OAuth-enabled app names.
|
||||
"""
|
||||
oauth_apps = []
|
||||
try:
|
||||
url = "https://backend.composio.dev/api/v1/apps"
|
||||
headers = {"x-api-key": api_key}
|
||||
params = {
|
||||
"includeLocal": "true",
|
||||
"additionalFields": "auth_schemes",
|
||||
"sortBy": "alphabet",
|
||||
# Set the helper text and helper text metadata field of the actions now
|
||||
build_config["actions"]["helper_text"] = ""
|
||||
build_config["actions"]["helper_text_metadata"] = {"icon": "Check", "variant": "success"}
|
||||
|
||||
# Get the list of actions available
|
||||
all_actions = list(Action.all())
|
||||
authenticated_actions = sorted(
|
||||
[
|
||||
action
|
||||
for action in all_actions
|
||||
if action.app.lower() in list(connected_app_names) and action.app.lower() == self.tool_name.lower()
|
||||
],
|
||||
key=lambda x: x.name,
|
||||
)
|
||||
|
||||
# Return the list of action names
|
||||
build_config["actions"]["options"] = [
|
||||
{
|
||||
"name": self.sanitize_action_name(action.name),
|
||||
}
|
||||
for action in authenticated_actions
|
||||
]
|
||||
|
||||
response = requests.get(url, headers=headers, params=params, timeout=20)
|
||||
data = response.json()
|
||||
# Lastly, we need to show the actions field
|
||||
build_config["actions"]["show"] = True
|
||||
|
||||
for item in data.get("items", []):
|
||||
for auth_scheme in item.get("auth_schemes", []):
|
||||
if auth_scheme.get("mode") in {"OAUTH1", "OAUTH2"}:
|
||||
oauth_apps.append(item["key"].upper())
|
||||
break
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"Error fetching OAuth apps: {e}")
|
||||
return []
|
||||
else:
|
||||
return oauth_apps
|
||||
return build_config
|
||||
|
||||
def _handle_auth_by_scheme(self, entity: Any, app: str, auth_scheme: AppAuthScheme) -> str:
|
||||
"""Handle authentication based on the auth scheme.
|
||||
def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:
|
||||
# If the list of tools is not available, always update it
|
||||
if field_name == "api_key" or (self.api_key and not build_config["tool_name"]["options"]):
|
||||
if field_name == "api_key" and not field_value:
|
||||
# Reset the list of tools
|
||||
build_config["tool_name"]["options"] = []
|
||||
build_config["tool_name"]["value"] = ""
|
||||
|
||||
Args:
|
||||
entity (Any): The entity instance.
|
||||
app (str): The app name.
|
||||
auth_scheme (AppAuthScheme): The auth scheme details.
|
||||
# Reset the list of actions
|
||||
build_config["actions"]["show"] = False
|
||||
build_config["actions"]["options"] = []
|
||||
build_config["actions"]["value"] = ""
|
||||
|
||||
Returns:
|
||||
str: The authentication status or URL.
|
||||
"""
|
||||
auth_mode = auth_scheme.auth_mode
|
||||
return build_config
|
||||
|
||||
try:
|
||||
# First check if already connected
|
||||
entity.get_connection(app=app)
|
||||
except NoItemsFound:
|
||||
# If not connected, handle new connection based on auth mode
|
||||
if auth_mode == "API_KEY":
|
||||
if hasattr(self, "app_credentials") and self.app_credentials:
|
||||
try:
|
||||
entity.initiate_connection(
|
||||
app_name=app,
|
||||
auth_mode="API_KEY",
|
||||
auth_config={"api_key": self.app_credentials},
|
||||
use_composio_auth=False,
|
||||
force_new_integration=True,
|
||||
)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.error(f"Error connecting with API Key: {e}")
|
||||
return "Invalid API Key"
|
||||
else:
|
||||
return f"{app} CONNECTED"
|
||||
return "Enter API Key"
|
||||
# TODO: Re-enable dynamic tool list
|
||||
# Initialize the Composio ToolSet with your API key
|
||||
# toolset = ComposioToolSet(api_key=self.api_key)
|
||||
|
||||
if (
|
||||
auth_mode == "BASIC"
|
||||
and hasattr(self, "username")
|
||||
and hasattr(self, "app_credentials")
|
||||
and self.username
|
||||
and self.app_credentials
|
||||
):
|
||||
try:
|
||||
entity.initiate_connection(
|
||||
app_name=app,
|
||||
auth_mode="BASIC",
|
||||
auth_config={"username": self.username, "password": self.app_credentials},
|
||||
use_composio_auth=False,
|
||||
force_new_integration=True,
|
||||
)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.error(f"Error connecting with Basic Auth: {e}")
|
||||
return "Invalid credentials"
|
||||
else:
|
||||
return f"{app} CONNECTED"
|
||||
elif auth_mode == "BASIC":
|
||||
return "Enter Username and Password"
|
||||
# Get the entity (e.g., "default" for your user)
|
||||
# entity = toolset.get_entity(self.entity_id)
|
||||
|
||||
if auth_mode == "OAUTH2":
|
||||
try:
|
||||
return self._initiate_default_connection(entity, app)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.error(f"Error initiating OAuth2: {e}")
|
||||
return "OAuth2 initialization failed"
|
||||
# Get all available apps
|
||||
# all_apps = entity.client.apps.get()
|
||||
|
||||
return "Unsupported auth mode"
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.error(f"Error checking connection status: {e}")
|
||||
return f"Error: {e!s}"
|
||||
else:
|
||||
return f"{app} CONNECTED"
|
||||
# Build an object with name, icon, link
|
||||
build_config["tool_name"]["options"] = [
|
||||
{
|
||||
"name": app.title(), # TODO: Switch to app.name
|
||||
"icon": app, # TODO: Switch to app.name
|
||||
"link": (
|
||||
build_config["tool_name"]["options"][ind]["link"]
|
||||
if build_config["tool_name"]["options"]
|
||||
else ""
|
||||
),
|
||||
}
|
||||
# for app in sorted(all_apps, key=lambda x: x.name)
|
||||
for ind, app in enumerate(enabled_tools)
|
||||
]
|
||||
|
||||
def _initiate_default_connection(self, entity: Any, app: str) -> str:
|
||||
connection = entity.initiate_connection(app_name=app, use_composio_auth=True, force_new_integration=True)
|
||||
return connection.redirectUrl
|
||||
|
||||
def _get_connected_app_names_for_entity(self) -> list[str]:
|
||||
toolset = self._build_wrapper()
|
||||
connections = toolset.client.get_entity(id=self.entity_id).get_connections()
|
||||
return list({connection.appUniqueId for connection in connections})
|
||||
|
||||
def _get_normalized_app_name(self) -> str:
|
||||
"""Get app name without connection status suffix.
|
||||
|
||||
Returns:
|
||||
str: Normalized app name.
|
||||
"""
|
||||
return self.app_names.replace(" ✅", "").replace("_connected", "")
|
||||
|
||||
def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict: # noqa: ARG002
|
||||
# Update the available apps options from the API
|
||||
if hasattr(self, "api_key") and self.api_key != "":
|
||||
toolset = self._build_wrapper()
|
||||
build_config["app_names"]["options"] = self._get_oauth_apps(api_key=self.api_key)
|
||||
|
||||
# First, ensure all dynamic fields are hidden by default
|
||||
dynamic_fields = ["app_credentials", "username", "auth_link", "auth_status", "action_names"]
|
||||
for field in dynamic_fields:
|
||||
if field in build_config:
|
||||
if build_config[field]["value"] is None or build_config[field]["value"] == "":
|
||||
build_config[field]["show"] = False
|
||||
build_config[field]["advanced"] = True
|
||||
build_config[field]["load_from_db"] = False
|
||||
else:
|
||||
build_config[field]["show"] = True
|
||||
build_config[field]["advanced"] = False
|
||||
|
||||
if field_name == "app_names" and (not hasattr(self, "app_names") or not self.app_names):
|
||||
build_config["auth_status"]["show"] = True
|
||||
build_config["auth_status"]["value"] = "Please select an app first"
|
||||
return build_config
|
||||
|
||||
if field_name == "app_names" and hasattr(self, "api_key") and self.api_key != "":
|
||||
# app_name = self._get_normalized_app_name()
|
||||
app_name = self.app_names
|
||||
# Handle the click of the Tool Name connect button
|
||||
if field_name == "tool_name" and field_value:
|
||||
# Get the list of apps (tools) we have connected
|
||||
toolset = ComposioToolSet(api_key=self.api_key)
|
||||
connected_apps = [app for app in toolset.get_connected_accounts() if app.status == "ACTIVE"]
|
||||
|
||||
# Get the unique list of appName from the connected apps
|
||||
connected_app_names = [app.appName.lower() for app in connected_apps]
|
||||
|
||||
# Clear out the list of selected actions
|
||||
build_config["actions"]["show"] = True
|
||||
build_config["actions"]["options"] = []
|
||||
build_config["actions"]["value"] = ""
|
||||
|
||||
# Clear out any helper text
|
||||
build_config["tool_name"]["helper_text"] = ""
|
||||
build_config["tool_name"]["helper_text_metadata"] = {}
|
||||
|
||||
# If it's a dictionary, we need to do validation
|
||||
if isinstance(field_value, dict):
|
||||
# If the current field value is a dictionary, it means the user has selected a tool
|
||||
if "validate" not in field_value:
|
||||
return build_config
|
||||
|
||||
# Check if the selected tool is connected
|
||||
check_app = field_value["validate"].lower()
|
||||
|
||||
# If the tool selected is NOT what we are validating, return the build config
|
||||
if check_app != self.tool_name.lower():
|
||||
# Set the helper text and helper text metadata field of the actions now
|
||||
build_config["actions"]["helper_text"] = "Please connect before selecting actions."
|
||||
build_config["actions"]["helper_text_metadata"] = {
|
||||
"icon": "OctagonAlert",
|
||||
"variant": "destructive",
|
||||
}
|
||||
|
||||
return build_config
|
||||
|
||||
# Check if the tool is already validated
|
||||
if check_app not in connected_app_names:
|
||||
return build_config
|
||||
|
||||
# Validate the selected tool
|
||||
return self.validate_tool(build_config, field_value, connected_app_names)
|
||||
|
||||
# Check if the tool is already validated
|
||||
if field_value.lower() in connected_app_names:
|
||||
return self.validate_tool(build_config, field_value, connected_app_names)
|
||||
|
||||
# Get the entity (e.g., "default" for your user)
|
||||
entity = toolset.get_entity(id=self.entity_id)
|
||||
|
||||
# Set the metadata for the actions
|
||||
build_config["actions"]["helper_text_metadata"] = {"icon": "OctagonAlert", "variant": "destructive"}
|
||||
|
||||
# Get the index of the selected tool in the list of options
|
||||
selected_tool_index = next(
|
||||
(ind for ind, tool in enumerate(build_config["tool_name"]["options"]) if tool["name"] == field_value),
|
||||
None,
|
||||
)
|
||||
|
||||
# Initiate a GitHub connection and get the redirect URL
|
||||
try:
|
||||
toolset = self._build_wrapper()
|
||||
entity = toolset.client.get_entity(id=self.entity_id)
|
||||
connection_request = entity.initiate_connection(app_name=getattr(App, field_value.upper()))
|
||||
except Exception as _: # noqa: BLE001
|
||||
# Indicate that there was an error connecting to the tool
|
||||
build_config["tool_name"]["options"][selected_tool_index]["link"] = "error"
|
||||
build_config["tool_name"]["helper_text"] = f"Error connecting to {field_value}"
|
||||
build_config["tool_name"]["helper_text_metadata"] = {
|
||||
"icon": "OctagonAlert",
|
||||
"variant": "destructive",
|
||||
}
|
||||
|
||||
# Always show auth_status when app is selected
|
||||
build_config["auth_status"]["show"] = True
|
||||
build_config["auth_status"]["advanced"] = False
|
||||
return build_config
|
||||
|
||||
try:
|
||||
# Check if already connected
|
||||
entity.get_connection(app=app_name)
|
||||
build_config["auth_status"]["value"] = "✅"
|
||||
build_config["auth_link"]["show"] = False
|
||||
# Show action selection for connected apps
|
||||
build_config["action_names"]["show"] = True
|
||||
build_config["action_names"]["advanced"] = False
|
||||
# Print the direct HTTP link for authentication
|
||||
build_config["tool_name"]["options"][selected_tool_index]["link"] = connection_request.redirectUrl
|
||||
|
||||
except NoItemsFound:
|
||||
# Get auth scheme and show relevant fields
|
||||
auth_scheme = self._get_auth_scheme(app_name)
|
||||
auth_mode = auth_scheme.auth_mode
|
||||
logger.info(f"Auth mode for {app_name}: {auth_mode}")
|
||||
|
||||
if auth_mode == "API_KEY":
|
||||
build_config["app_credentials"]["show"] = True
|
||||
build_config["app_credentials"]["advanced"] = False
|
||||
build_config["app_credentials"]["display_name"] = "API Key"
|
||||
build_config["auth_status"]["value"] = "Enter API Key"
|
||||
|
||||
elif auth_mode == "BASIC":
|
||||
build_config["username"]["show"] = True
|
||||
build_config["username"]["advanced"] = False
|
||||
build_config["app_credentials"]["show"] = True
|
||||
build_config["app_credentials"]["advanced"] = False
|
||||
build_config["app_credentials"]["display_name"] = "Password"
|
||||
build_config["auth_status"]["value"] = "Enter Username and Password"
|
||||
|
||||
elif auth_mode == "OAUTH2":
|
||||
build_config["auth_link"]["show"] = True
|
||||
build_config["auth_link"]["advanced"] = False
|
||||
auth_url = self._initiate_default_connection(entity, app_name)
|
||||
build_config["auth_link"]["value"] = auth_url
|
||||
build_config["auth_status"]["value"] = "Click link to authenticate"
|
||||
|
||||
else:
|
||||
build_config["auth_status"]["value"] = "Unsupported auth mode"
|
||||
|
||||
# Update action names if connected
|
||||
if build_config["auth_status"]["value"] == "✅":
|
||||
all_action_names = [str(action).replace("Action.", "") for action in Action.all()]
|
||||
app_action_names = [
|
||||
action_name
|
||||
for action_name in all_action_names
|
||||
if action_name.lower().startswith(app_name.lower() + "_")
|
||||
]
|
||||
if build_config["action_names"]["options"] != app_action_names:
|
||||
build_config["action_names"]["options"] = app_action_names
|
||||
build_config["action_names"]["value"] = [app_action_names[0]] if app_action_names else [""]
|
||||
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.error(f"Error checking auth status: {e}, app: {app_name}")
|
||||
build_config["auth_status"]["value"] = f"Error: {e!s}"
|
||||
# Set the helper text and helper text metadata field of the actions now
|
||||
build_config["actions"]["helper_text"] = "Please connect before selecting actions."
|
||||
|
||||
return build_config
|
||||
|
||||
@@ -354,7 +264,9 @@ class ComposioAPIComponent(LCToolComponent):
|
||||
Sequence[Tool]: List of configured Composio tools.
|
||||
"""
|
||||
composio_toolset = self._build_wrapper()
|
||||
return composio_toolset.get_tools(actions=self.action_names)
|
||||
return composio_toolset.get_tools(
|
||||
actions=[self.desanitize_action_name(action["name"]) for action in self.actions]
|
||||
)
|
||||
|
||||
def _build_wrapper(self) -> ComposioToolSet:
|
||||
"""Build the Composio toolset wrapper.
|
||||
@@ -371,6 +283,6 @@ class ComposioAPIComponent(LCToolComponent):
|
||||
raise ValueError(msg)
|
||||
return ComposioToolSet(api_key=self.api_key, entity_id=self.entity_id)
|
||||
except ValueError as e:
|
||||
logger.error(f"Error building Composio wrapper: {e}")
|
||||
self.log(f"Error building Composio wrapper: {e}")
|
||||
msg = "Please provide a valid Composio API Key in the component settings"
|
||||
raise ValueError(msg) from e
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
||||
from .inputs import (
|
||||
BoolInput,
|
||||
CodeInput,
|
||||
ConnectionInput,
|
||||
DataFrameInput,
|
||||
DataInput,
|
||||
DefaultPromptField,
|
||||
@@ -21,6 +22,7 @@ from .inputs import (
|
||||
PromptInput,
|
||||
SecretStrInput,
|
||||
SliderInput,
|
||||
SortableListInput,
|
||||
StrInput,
|
||||
TabInput,
|
||||
TableInput,
|
||||
@@ -29,6 +31,7 @@ from .inputs import (
|
||||
__all__ = [
|
||||
"BoolInput",
|
||||
"CodeInput",
|
||||
"ConnectionInput",
|
||||
"DataFrameInput",
|
||||
"DataInput",
|
||||
"DefaultPromptField",
|
||||
@@ -42,7 +45,6 @@ __all__ = [
|
||||
"Input",
|
||||
"IntInput",
|
||||
"LinkInput",
|
||||
"LinkInput",
|
||||
"MessageInput",
|
||||
"MessageTextInput",
|
||||
"MultilineInput",
|
||||
@@ -52,6 +54,7 @@ __all__ = [
|
||||
"PromptInput",
|
||||
"SecretStrInput",
|
||||
"SliderInput",
|
||||
"SortableListInput",
|
||||
"StrInput",
|
||||
"TabInput",
|
||||
"TableInput",
|
||||
|
||||
@@ -24,6 +24,8 @@ class FieldTypes(str, Enum):
|
||||
BOOLEAN = "bool"
|
||||
DICT = "dict"
|
||||
NESTED_DICT = "NestedDict"
|
||||
SORTABLE_LIST = "sortableList"
|
||||
CONNECTION = "connect"
|
||||
FILE = "file"
|
||||
PROMPT = "prompt"
|
||||
CODE = "code"
|
||||
@@ -191,6 +193,34 @@ class DropDownMixin(BaseModel):
|
||||
"""Dictionary of dialog inputs for the field. Default is an empty object."""
|
||||
|
||||
|
||||
class SortableListMixin(BaseModel):
|
||||
helper_text: str | None = None
|
||||
"""Adds a helper text to the field. Defaults to an empty string."""
|
||||
helper_text_metadata: dict[str, Any] | None = None
|
||||
"""Dictionary of metadata for the helper text."""
|
||||
search_category: list[str] = Field(default=[])
|
||||
"""Specifies the category of the field. Defaults to an empty list."""
|
||||
options: list[dict[str, Any]] = Field(default_factory=list)
|
||||
"""List of dictionaries with metadata for each option."""
|
||||
limit: int | None = None
|
||||
"""Specifies the limit of the field. Defaults to None."""
|
||||
|
||||
|
||||
class ConnectionMixin(BaseModel):
|
||||
helper_text: str | None = None
|
||||
"""Adds a helper text to the field. Defaults to an empty string."""
|
||||
helper_text_metadata: dict[str, Any] | None = None
|
||||
"""Dictionary of metadata for the helper text."""
|
||||
connection_link: str | None = None
|
||||
"""Specifies the link of the connection. Defaults to an empty string."""
|
||||
button_metadata: dict[str, Any] | None = None
|
||||
"""Dictionary of metadata for the button."""
|
||||
search_category: list[str] = Field(default=[])
|
||||
"""Specifies the category of the field. Defaults to an empty list."""
|
||||
options: list[dict[str, Any]] = Field(default_factory=list)
|
||||
"""List of dictionaries with metadata for each option."""
|
||||
|
||||
|
||||
class TabMixin(BaseModel):
|
||||
"""Mixin for tab input fields that allows a maximum of 3 values, each with a maximum of 20 characters."""
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from langflow.template.field.base import Input
|
||||
|
||||
from .input_mixin import (
|
||||
BaseInputMixin,
|
||||
ConnectionMixin,
|
||||
DatabaseLoadMixin,
|
||||
DropDownMixin,
|
||||
FieldTypes,
|
||||
@@ -25,6 +26,7 @@ from .input_mixin import (
|
||||
RangeMixin,
|
||||
SerializableFieldTypes,
|
||||
SliderMixin,
|
||||
SortableListMixin,
|
||||
TableMixin,
|
||||
TabMixin,
|
||||
ToolModeMixin,
|
||||
@@ -464,6 +466,30 @@ class DropdownInput(BaseInputMixin, DropDownMixin, MetadataTraceMixin, ToolModeM
|
||||
dialog_inputs: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ConnectionInput(BaseInputMixin, ConnectionMixin, MetadataTraceMixin, ToolModeMixin):
|
||||
"""Represents a connection input field.
|
||||
|
||||
This class represents a connection input field and provides functionality for handling connection values.
|
||||
It inherits from the `BaseInputMixin` and `ConnectionMixin` classes.
|
||||
|
||||
"""
|
||||
|
||||
field_type: SerializableFieldTypes = FieldTypes.CONNECTION
|
||||
|
||||
|
||||
class SortableListInput(BaseInputMixin, SortableListMixin, MetadataTraceMixin, ToolModeMixin):
|
||||
"""Represents a list selection input field.
|
||||
|
||||
This class represents a list selection input field and provides functionality for handling list selection values.
|
||||
It inherits from the `BaseInputMixin` and `ListableInputMixin` classes.
|
||||
|
||||
Attributes:
|
||||
field_type (SerializableFieldTypes): The field type of the input. Defaults to FieldTypes.BUTTON.
|
||||
"""
|
||||
|
||||
field_type: SerializableFieldTypes = FieldTypes.SORTABLE_LIST
|
||||
|
||||
|
||||
class TabInput(BaseInputMixin, TabMixin, MetadataTraceMixin, ToolModeMixin):
|
||||
"""Represents a tab input field.
|
||||
|
||||
@@ -570,6 +596,8 @@ InputTypes: TypeAlias = (
|
||||
| DictInput
|
||||
| DropdownInput
|
||||
| MultiselectInput
|
||||
| SortableListInput
|
||||
| ConnectionInput
|
||||
| FileInput
|
||||
| FloatInput
|
||||
| HandleInput
|
||||
|
||||
@@ -52,7 +52,22 @@ def python_function(text: str) -> str:
|
||||
|
||||
|
||||
PYTHON_BASIC_TYPES = [str, bool, int, float, tuple, list, dict, set]
|
||||
DIRECT_TYPES = ["str", "bool", "dict", "int", "float", "Any", "prompt", "code", "NestedDict", "table", "slider", "tab"]
|
||||
DIRECT_TYPES = [
|
||||
"str",
|
||||
"bool",
|
||||
"dict",
|
||||
"int",
|
||||
"float",
|
||||
"Any",
|
||||
"prompt",
|
||||
"code",
|
||||
"NestedDict",
|
||||
"table",
|
||||
"slider",
|
||||
"tab",
|
||||
"sortableList",
|
||||
"connect",
|
||||
]
|
||||
|
||||
|
||||
LOADERS_INFO: list[dict[str, Any]] = [
|
||||
|
||||
38
src/frontend/package-lock.json
generated
38
src/frontend/package-lock.json
generated
@@ -76,6 +76,7 @@
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-pdf": "^9.0.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-sortablejs": "^6.1.4",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"reactflow": "^11.11.3",
|
||||
"rehype-mathjax": "^4.0.3",
|
||||
@@ -84,6 +85,7 @@
|
||||
"remark-math": "^6.0.0",
|
||||
"shadcn-ui": "^0.9.4",
|
||||
"short-unique-id": "^5.2.0",
|
||||
"sortablejs": "^1.15.6",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uuid": "^10.0.0",
|
||||
@@ -107,6 +109,7 @@
|
||||
"@types/node": "^20.14.2",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
@@ -5562,6 +5565,11 @@
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sortablejs": {
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz",
|
||||
"integrity": "sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg=="
|
||||
},
|
||||
"node_modules/@types/stack-utils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
||||
@@ -6669,6 +6677,11 @@
|
||||
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
|
||||
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
|
||||
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
|
||||
},
|
||||
"node_modules/cli-boxes": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
|
||||
@@ -13126,6 +13139,26 @@
|
||||
"sisteransi": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/react-sortablejs": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/react-sortablejs/-/react-sortablejs-6.1.4.tgz",
|
||||
"integrity": "sha512-fc7cBosfhnbh53Mbm6a45W+F735jwZ1UFIYSrIqcO/gRIFoDyZeMtgKlpV4DdyQfbCzdh5LoALLTDRxhMpTyXQ==",
|
||||
"dependencies": {
|
||||
"classnames": "2.3.1",
|
||||
"tiny-invariant": "1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/sortablejs": "1",
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0",
|
||||
"sortablejs": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-sortablejs/node_modules/tiny-invariant": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
|
||||
"integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
@@ -14297,6 +14330,11 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sortablejs": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz",
|
||||
"integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A=="
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-pdf": "^9.0.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-sortablejs": "^6.1.4",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"reactflow": "^11.11.3",
|
||||
"rehype-mathjax": "^4.0.3",
|
||||
@@ -79,6 +80,7 @@
|
||||
"remark-math": "^6.0.0",
|
||||
"shadcn-ui": "^0.9.4",
|
||||
"short-unique-id": "^5.2.0",
|
||||
"sortablejs": "^1.15.6",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uuid": "^10.0.0",
|
||||
@@ -130,6 +132,7 @@
|
||||
"@types/node": "^20.14.2",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import SearchBarComponent from "@/components/core/parameterRenderComponent/components/searchBarComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog-with-no-close";
|
||||
import { cn } from "@/utils/utils";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
// Update interface with better types
|
||||
interface ListSelectionComponentProps {
|
||||
open: boolean;
|
||||
options: any[];
|
||||
onClose: () => void;
|
||||
setSelectedList: (action: any[]) => void;
|
||||
selectedList: any[];
|
||||
searchCategories?: string[];
|
||||
onSelection?: (action: any) => void;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
const ListItem = ({
|
||||
item,
|
||||
isSelected,
|
||||
onClick,
|
||||
className,
|
||||
}: {
|
||||
item: any;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
}) => (
|
||||
<Button
|
||||
key={item.id}
|
||||
unstyled
|
||||
size="sm"
|
||||
className={cn("w-full rounded-md py-3 pl-3 pr-3 hover:bg-muted", className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.icon && (
|
||||
<ForwardedIconComponent name={item.icon} className="h-5 w-5" />
|
||||
)}
|
||||
<span className="truncate font-semibold">{item.name}</span>
|
||||
{"metaData" in item && item.metaData && (
|
||||
<span className="text-gray-500">{item.metaData}</span>
|
||||
)}
|
||||
{isSelected ? (
|
||||
<ForwardedIconComponent
|
||||
name="check"
|
||||
className={cn(
|
||||
"ml-auto flex h-4 w-4",
|
||||
item.link === "validated" && "text-green-500",
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<span className="ml-auto flex h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
|
||||
const ListSelectionComponent = ({
|
||||
open,
|
||||
onClose,
|
||||
searchCategories = [],
|
||||
onSelection,
|
||||
setSelectedList = () => {},
|
||||
selectedList = [],
|
||||
options,
|
||||
limit = 1,
|
||||
}: ListSelectionComponentProps) => {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
if (!search.trim()) {
|
||||
return options;
|
||||
}
|
||||
const searchTerm = search.toLowerCase();
|
||||
return options.filter((item) =>
|
||||
item.name.toLowerCase().includes(searchTerm),
|
||||
);
|
||||
}, [options, search]);
|
||||
|
||||
const handleSelectAction = useCallback(
|
||||
(action: any) => {
|
||||
if (limit !== 1) {
|
||||
// Multiple selection mode
|
||||
const isAlreadySelected = selectedList.some(
|
||||
(selectedItem) => selectedItem.name === action.name,
|
||||
);
|
||||
|
||||
if (isAlreadySelected) {
|
||||
setSelectedList(
|
||||
selectedList.filter(
|
||||
(selectedItem) => selectedItem.name !== action.name,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Check if we've reached the selection limit
|
||||
if (selectedList.length < limit) {
|
||||
setSelectedList([...selectedList, action]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single selection mode
|
||||
setSelectedList([
|
||||
{
|
||||
name: action.name,
|
||||
icon: "icon" in action ? action.icon : undefined,
|
||||
link: "link" in action ? action.link : undefined,
|
||||
},
|
||||
]);
|
||||
onClose();
|
||||
setSearch("");
|
||||
}
|
||||
},
|
||||
[selectedList, setSelectedList, onClose, limit],
|
||||
);
|
||||
|
||||
const handleCloseDialog = useCallback(() => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleCloseDialog}>
|
||||
<DialogContent className="flex !w-auto w-fit min-w-[20vw] max-w-[50vw] flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<SearchBarComponent
|
||||
searchCategories={searchCategories}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
/>
|
||||
<Button
|
||||
unstyled
|
||||
size="icon"
|
||||
className="ml-auto h-[38px]"
|
||||
onClick={handleCloseDialog}
|
||||
>
|
||||
<ForwardedIconComponent name="x" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex max-h-[80vh] flex-col gap-1 overflow-y-auto">
|
||||
{filteredList.length > 0 ? (
|
||||
filteredList.map((item, index) => (
|
||||
<ListItem
|
||||
key={`${item.name}-${index}`}
|
||||
item={item}
|
||||
isSelected={
|
||||
selectedList.some(
|
||||
(selected) => selected.name === item.name,
|
||||
) || item.link === "validated"
|
||||
}
|
||||
onClick={() => {
|
||||
handleSelectAction(item);
|
||||
onSelection?.(item);
|
||||
}}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="py-3 text-center text-gray-500">
|
||||
No items match your search
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListSelectionComponent;
|
||||
@@ -0,0 +1,244 @@
|
||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value";
|
||||
import ListSelectionComponent from "@/CustomNodes/GenericNode/components/ListSelectionComponent";
|
||||
import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import { APIClassType } from "@/types/api";
|
||||
import { cn } from "@/utils/utils";
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import { InputProps } from "../../types";
|
||||
import HelperTextComponent from "../helperTextComponent";
|
||||
|
||||
type ConnectionComponentProps = {
|
||||
tooltip?: string;
|
||||
name: string;
|
||||
helperText?: string;
|
||||
helperMetadata?: any;
|
||||
options?: any[];
|
||||
searchCategory?: string[];
|
||||
buttonMetadata?: { variant?: string; icon?: string };
|
||||
connectionLink?: string;
|
||||
nodeClass: APIClassType;
|
||||
nodeId: string;
|
||||
};
|
||||
|
||||
const ConnectionComponent = ({
|
||||
tooltip = "",
|
||||
name,
|
||||
helperText = "",
|
||||
helperMetadata = { icon: undefined, variant: "muted-foreground" },
|
||||
options = [],
|
||||
searchCategory = [],
|
||||
buttonMetadata = { variant: "destructive", icon: "unplug" },
|
||||
connectionLink = "",
|
||||
...baseInputProps
|
||||
}: InputProps<any, ConnectionComponentProps>) => {
|
||||
const {
|
||||
value,
|
||||
handleOnNewValue,
|
||||
handleNodeClass,
|
||||
nodeClass,
|
||||
nodeId,
|
||||
placeholder,
|
||||
} = baseInputProps;
|
||||
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(
|
||||
connectionLink === "validated",
|
||||
);
|
||||
const [link, setLink] = useState("");
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<any[]>([]);
|
||||
|
||||
const pollingInterval = useRef<NodeJS.Timeout | null>(null);
|
||||
const pollingTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const postTemplateValue = usePostTemplateValue({
|
||||
parameterId: name,
|
||||
nodeId: nodeId,
|
||||
node: nodeClass,
|
||||
});
|
||||
|
||||
// Initialize selected item from value on component mount
|
||||
useEffect(() => {
|
||||
const selectedOption = value
|
||||
? options.find((option) => option.name === value)
|
||||
: null;
|
||||
setSelectedItem([
|
||||
selectedOption
|
||||
? { name: selectedOption.name, icon: selectedOption.icon }
|
||||
: { name: "", icon: "" },
|
||||
]);
|
||||
|
||||
// Update authentication status based on selected option
|
||||
if (!selectedOption) {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
}, [value, options]);
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionLink !== "") {
|
||||
setLink(connectionLink);
|
||||
if (connectionLink === "validated") {
|
||||
setIsAuthenticated(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (connectionLink === "error") {
|
||||
setLink("error");
|
||||
}
|
||||
}, [connectionLink]);
|
||||
|
||||
// Handles the connection button click to open connection in new tab and start polling
|
||||
const handleConnectionButtonClick = () => {
|
||||
if (selectedItem?.length === 0) return;
|
||||
|
||||
window.open(link, "_blank");
|
||||
|
||||
startPolling();
|
||||
};
|
||||
|
||||
// Initiates polling to check connection status periodically
|
||||
const startPolling = () => {
|
||||
if (!selectedItem[0]?.name) return;
|
||||
|
||||
setLink("loading");
|
||||
|
||||
// Initialize polling
|
||||
setIsPolling(true);
|
||||
|
||||
// Clear existing timers
|
||||
stopPolling();
|
||||
|
||||
// Set up polling interval - check connection status every 3 seconds
|
||||
pollingInterval.current = setInterval(() => {
|
||||
mutateTemplate(
|
||||
{ validate: selectedItem[0]?.name || "" },
|
||||
nodeClass,
|
||||
handleNodeClass,
|
||||
postTemplateValue,
|
||||
setErrorData,
|
||||
name,
|
||||
() => {
|
||||
// Check if the connection was successful
|
||||
if (connectionLink === "validated") {
|
||||
stopPolling();
|
||||
setIsAuthenticated(true);
|
||||
}
|
||||
},
|
||||
nodeClass.tool_mode,
|
||||
);
|
||||
}, 3000);
|
||||
|
||||
// Set timeout to stop polling after 9 seconds to prevent indefinite polling
|
||||
pollingTimeout.current = setTimeout(() => {
|
||||
stopPolling(link !== "");
|
||||
// If we timed out and link is still loading, reset it
|
||||
}, 9000);
|
||||
};
|
||||
|
||||
// Cleans up polling timers to prevent memory leaks
|
||||
const stopPolling = (resetLink = false) => {
|
||||
setIsPolling(false);
|
||||
if (resetLink) {
|
||||
setLink(connectionLink || "");
|
||||
}
|
||||
|
||||
if (pollingInterval.current) clearInterval(pollingInterval.current);
|
||||
if (pollingTimeout.current) clearTimeout(pollingTimeout.current);
|
||||
};
|
||||
|
||||
// Updates selected item and triggers parent component update
|
||||
const handleSelection = (item: any) => {
|
||||
setIsAuthenticated(false);
|
||||
setSelectedItem([{ name: item.name }]);
|
||||
setLink(item.link === "validated" ? "validated" : "loading");
|
||||
if (item.link === "validated") {
|
||||
setIsAuthenticated(true);
|
||||
}
|
||||
handleOnNewValue({ value: item.name }, { skipSnapshot: true });
|
||||
};
|
||||
|
||||
// Dialog control handlers
|
||||
const handleOpenListSelectionDialog = () => setOpen(true);
|
||||
const handleCloseListSelectionDialog = () => setOpen(false);
|
||||
|
||||
// Render component
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div className="flex w-full flex-row items-center gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="xs"
|
||||
role="combobox"
|
||||
onClick={handleOpenListSelectionDialog}
|
||||
className="dropdown-component-outline input-edit-node w-full py-2"
|
||||
>
|
||||
<div className={cn("flex w-full items-center justify-start text-sm")}>
|
||||
{selectedItem[0]?.icon && (
|
||||
<ForwardedIconComponent
|
||||
name={selectedItem[0]?.icon}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
)}
|
||||
<span className="ml-2 truncate">
|
||||
{selectedItem[0]?.name || placeholder}
|
||||
</span>
|
||||
<ForwardedIconComponent
|
||||
name="ChevronsUpDown"
|
||||
className="ml-auto h-5 w-5"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
{!isAuthenticated && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
loading={link === "loading" || isPolling}
|
||||
disabled={!selectedItem[0]?.name || link === "" || link === "error"}
|
||||
className={cn(
|
||||
"h-9 w-10 rounded-md border disabled:opacity-50",
|
||||
buttonMetadata.variant && `border-${buttonMetadata.variant}`,
|
||||
)}
|
||||
onClick={link === "error" ? undefined : handleConnectionButtonClick}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name={
|
||||
link === "error"
|
||||
? "triangle-alert"
|
||||
: buttonMetadata.icon || "unplug"
|
||||
}
|
||||
className={cn(
|
||||
"h-5 w-5",
|
||||
buttonMetadata.variant && `text-${buttonMetadata.variant}`,
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{helperText && (
|
||||
<HelperTextComponent
|
||||
helperText={helperText}
|
||||
helperMetadata={helperMetadata}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ListSelectionComponent
|
||||
open={open}
|
||||
onSelection={handleSelection}
|
||||
onClose={handleCloseListSelectionDialog}
|
||||
searchCategories={searchCategory}
|
||||
setSelectedList={setSelectedItem}
|
||||
selectedList={selectedItem}
|
||||
options={options}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ConnectionComponent);
|
||||
@@ -12,6 +12,8 @@ export default function DropdownComponent({
|
||||
name,
|
||||
dialogInputs,
|
||||
optionsMetaData,
|
||||
nodeClass,
|
||||
nodeId,
|
||||
...baseInputProps
|
||||
}: InputProps<string, DropDownComponentType>) {
|
||||
const onChange = (value: any, dbValue?: boolean, skipSnapshot?: boolean) => {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import { cn } from "@/utils/utils";
|
||||
|
||||
type HelperTextComponentProps = {
|
||||
helperText: string;
|
||||
helperMetadata?: { icon: string | undefined; variant: string };
|
||||
};
|
||||
|
||||
const HelperTextComponent = ({
|
||||
helperText,
|
||||
helperMetadata = { icon: undefined, variant: "muted-foreground" },
|
||||
}: HelperTextComponentProps) => {
|
||||
return (
|
||||
<div className="flex w-full flex-row items-center gap-2">
|
||||
{helperMetadata?.icon && (
|
||||
<ForwardedIconComponent
|
||||
name={helperMetadata?.icon}
|
||||
className={cn(
|
||||
`h-5 w-5`,
|
||||
helperMetadata?.variant && `text-${helperMetadata?.variant}`,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full flex-col text-xs",
|
||||
helperMetadata?.variant && `text-${helperMetadata?.variant}`,
|
||||
)}
|
||||
>
|
||||
{helperText}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelperTextComponent;
|
||||
@@ -1,13 +1,8 @@
|
||||
import {
|
||||
useDeleteGlobalVariables,
|
||||
useGetGlobalVariables,
|
||||
} from "@/controllers/API/queries/variables";
|
||||
import { useGetGlobalVariables } from "@/controllers/API/queries/variables";
|
||||
import GeneralDeleteConfirmationModal from "@/shared/components/delete-confirmation-modal";
|
||||
import GeneralGlobalVariableModal from "@/shared/components/global-variable-modal";
|
||||
import { useGlobalVariablesStore } from "@/stores/globalVariablesStore/globalVariables";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import DeleteConfirmationModal from "../../../../../modals/deleteConfirmationModal";
|
||||
import useAlertStore from "../../../../../stores/alertStore";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
|
||||
import { cn } from "../../../../../utils/utils";
|
||||
import ForwardedIconComponent from "../../../../common/genericIconComponent";
|
||||
import { CommandItem } from "../../../../ui/command";
|
||||
@@ -34,34 +29,63 @@ export default function InputGlobalComponent({
|
||||
(state) => state.unavailableFields,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (globalVariables && !disabled) {
|
||||
if (
|
||||
load_from_db &&
|
||||
!globalVariables.find((variable) => variable.name === value)
|
||||
) {
|
||||
handleOnNewValue(
|
||||
{ value: "", load_from_db: false },
|
||||
{ skipSnapshot: true },
|
||||
);
|
||||
}
|
||||
if (
|
||||
!load_from_db &&
|
||||
value === "" &&
|
||||
unavailableFields &&
|
||||
Object.keys(unavailableFields).includes(display_name ?? "")
|
||||
) {
|
||||
handleOnNewValue(
|
||||
{ value: unavailableFields[display_name ?? ""], load_from_db: true },
|
||||
{ skipSnapshot: true },
|
||||
);
|
||||
}
|
||||
const initialLoadCompleted = useRef(false);
|
||||
|
||||
const valueExists = useMemo(() => {
|
||||
return (
|
||||
globalVariables?.some((variable) => variable.name === value) ?? false
|
||||
);
|
||||
}, [globalVariables, value]);
|
||||
|
||||
const unavailableField = useMemo(() => {
|
||||
if (
|
||||
display_name &&
|
||||
unavailableFields &&
|
||||
Object.keys(unavailableFields).includes(display_name)
|
||||
) {
|
||||
return unavailableFields[display_name];
|
||||
}
|
||||
}, [globalVariables, unavailableFields, disabled]);
|
||||
return null;
|
||||
}, [unavailableFields, display_name]);
|
||||
|
||||
useMemo(() => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (load_from_db && globalVariables && !valueExists) {
|
||||
handleOnNewValue(
|
||||
{ value: "", load_from_db: false },
|
||||
{ skipSnapshot: true },
|
||||
);
|
||||
}
|
||||
}, [
|
||||
globalVariables,
|
||||
unavailableFields,
|
||||
disabled,
|
||||
load_from_db,
|
||||
valueExists,
|
||||
unavailableField,
|
||||
value,
|
||||
handleOnNewValue,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialLoadCompleted.current || disabled || unavailableField === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleOnNewValue(
|
||||
{ value: unavailableField, load_from_db: true },
|
||||
{ skipSnapshot: true },
|
||||
);
|
||||
|
||||
initialLoadCompleted.current = true;
|
||||
}, [unavailableField, disabled, load_from_db, value, handleOnNewValue]);
|
||||
|
||||
function handleDelete(key: string) {
|
||||
if (value === key && load_from_db) {
|
||||
handleOnNewValue({ value: "", load_from_db: false });
|
||||
if (value === key) {
|
||||
handleOnNewValue({ value: "", load_from_db: load_from_db });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,13 +120,7 @@ export default function InputGlobalComponent({
|
||||
onConfirmDelete={() => handleDelete(option)}
|
||||
/>
|
||||
)}
|
||||
selectedOption={
|
||||
load_from_db &&
|
||||
globalVariables &&
|
||||
globalVariables?.map((variable) => variable.name).includes(value ?? "")
|
||||
? value
|
||||
: ""
|
||||
}
|
||||
selectedOption={load_from_db && valueExists ? value : ""}
|
||||
setSelectedOption={(value) => {
|
||||
handleOnNewValue({
|
||||
value: value,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useState } from "react";
|
||||
|
||||
interface SearchBarComponentProps {
|
||||
searchCategories?: string[];
|
||||
search: string;
|
||||
setSearch: (search: string) => void;
|
||||
placeholder?: string;
|
||||
onCategoryChange?: (category: string) => void;
|
||||
}
|
||||
|
||||
const SearchBarComponent = ({
|
||||
searchCategories,
|
||||
search,
|
||||
setSearch,
|
||||
placeholder = "Search tools...",
|
||||
onCategoryChange,
|
||||
}: SearchBarComponentProps) => {
|
||||
const [selectedCategory, setSelectedCategory] = useState(
|
||||
searchCategories?.[0] || "All",
|
||||
);
|
||||
|
||||
const handleCategoryChange = (category: string) => {
|
||||
setSelectedCategory(category);
|
||||
if (onCategoryChange) {
|
||||
onCategoryChange(category);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mr-10 flex w-full items-center rounded-md border">
|
||||
{searchCategories && searchCategories.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="flex items-center gap-2 pl-4 text-sm">
|
||||
<span className="truncate">{selectedCategory}</span>
|
||||
<ForwardedIconComponent
|
||||
name="chevron-down"
|
||||
className="flex h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
{searchCategories.map((category) => (
|
||||
<DropdownMenuItem
|
||||
key={category}
|
||||
onClick={() => handleCategoryChange(category)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<span className="flex items-center gap-2 truncate px-2 py-1 text-sm">
|
||||
{category}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<Input
|
||||
icon="search"
|
||||
placeholder={placeholder}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
inputClassName="border-none focus:ring-0"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBarComponent;
|
||||
@@ -0,0 +1,184 @@
|
||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ListSelectionComponent from "@/CustomNodes/GenericNode/components/ListSelectionComponent";
|
||||
import { cn } from "@/utils/utils";
|
||||
import { memo, useCallback, useMemo, useState } from "react";
|
||||
import { ReactSortable } from "react-sortablejs";
|
||||
import { InputProps } from "../../types";
|
||||
import HelperTextComponent from "../helperTextComponent";
|
||||
|
||||
type SortableListComponentProps = {
|
||||
tooltip?: string;
|
||||
name?: string;
|
||||
helperText?: string;
|
||||
helperMetadata?: any;
|
||||
options?: any[];
|
||||
searchCategory?: string[];
|
||||
icon?: string;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
const SortableListItem = memo(
|
||||
({
|
||||
data,
|
||||
index,
|
||||
onRemove,
|
||||
limit = 1,
|
||||
}: {
|
||||
data: any;
|
||||
index: number;
|
||||
onRemove: () => void;
|
||||
limit?: number;
|
||||
}) => (
|
||||
<li
|
||||
className={cn(
|
||||
"inline-flex h-12 w-full items-center gap-2 text-sm font-medium text-gray-800",
|
||||
limit === 1 ? "h-10 rounded-md bg-muted" : "group cursor-grab",
|
||||
)}
|
||||
>
|
||||
{limit !== 1 && (
|
||||
<ForwardedIconComponent
|
||||
name="grid-horizontal"
|
||||
className="h-5 w-5 fill-gray-300 text-gray-300"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex w-full items-center gap-x-2">
|
||||
{limit !== 1 && (
|
||||
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-400 text-center text-white">
|
||||
{index + 1}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
"truncate text-primary",
|
||||
limit === 1 ? "max-w-56 pl-3" : "max-w-48",
|
||||
)}
|
||||
>
|
||||
{data.name}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant={limit !== 1 ? "outline" : "ghost"}
|
||||
className={cn(
|
||||
"ml-auto h-7 w-7 opacity-0 transition-opacity duration-200",
|
||||
limit === 1
|
||||
? "group pr-3 opacity-100"
|
||||
: "hover:border hover:border-destructive hover:bg-transparent hover:opacity-100",
|
||||
)}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="x"
|
||||
className={cn(
|
||||
"h-6 w-6 text-red-500",
|
||||
limit === 1 && "text-gray-500 group-hover:text-input",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</li>
|
||||
),
|
||||
);
|
||||
|
||||
const SortableListComponent = ({
|
||||
tooltip = "",
|
||||
name,
|
||||
helperText = "",
|
||||
helperMetadata = { icon: undefined, variant: "muted-foreground" },
|
||||
options = [],
|
||||
searchCategory = [],
|
||||
limit,
|
||||
...baseInputProps
|
||||
}: InputProps<any, SortableListComponentProps>) => {
|
||||
const { placeholder, handleOnNewValue, value } = baseInputProps;
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Convert value to an array if it exists, otherwise use empty array
|
||||
const listData = useMemo(() => (Array.isArray(value) ? value : []), [value]);
|
||||
|
||||
const createRemoveHandler = useCallback(
|
||||
(index: number) => () => {
|
||||
const newList = listData.filter((_, i) => i !== index);
|
||||
handleOnNewValue({ value: newList });
|
||||
},
|
||||
[listData, handleOnNewValue],
|
||||
);
|
||||
|
||||
const setListDataHandler = useCallback(
|
||||
(newList: any[]) => {
|
||||
handleOnNewValue({ value: newList });
|
||||
},
|
||||
[handleOnNewValue],
|
||||
);
|
||||
|
||||
const handleCloseListSelectionDialog = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleOpenListSelectionDialog = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="flex w-full flex-row gap-2">
|
||||
{!(limit === 1 && listData.length === 1) && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
role="combobox"
|
||||
onClick={handleOpenListSelectionDialog}
|
||||
className="dropdown-component-outline input-edit-node w-full py-2"
|
||||
>
|
||||
<div className={cn("flex items-center text-sm font-semibold")}>
|
||||
{placeholder}
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{listData.length > 0 && (
|
||||
<div className="flex w-full flex-col">
|
||||
<ReactSortable
|
||||
list={listData}
|
||||
setList={setListDataHandler}
|
||||
className={"flex w-full flex-col"}
|
||||
>
|
||||
{listData.map((data, index) => (
|
||||
<SortableListItem
|
||||
key={`${data?.name || "item"}-${index}`}
|
||||
data={data}
|
||||
index={index}
|
||||
onRemove={createRemoveHandler(index)}
|
||||
limit={limit}
|
||||
/>
|
||||
))}
|
||||
</ReactSortable>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{helperText && (
|
||||
<div className="pt-2">
|
||||
<HelperTextComponent
|
||||
helperText={helperText}
|
||||
helperMetadata={helperMetadata}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ListSelectionComponent
|
||||
open={open}
|
||||
onClose={handleCloseListSelectionDialog}
|
||||
searchCategories={searchCategory}
|
||||
setSelectedList={setListDataHandler}
|
||||
selectedList={listData}
|
||||
options={options}
|
||||
limit={limit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SortableListComponent);
|
||||
@@ -6,6 +6,7 @@ import TabComponent from "@/components/core/parameterRenderComponent/components/
|
||||
import { TEXT_FIELD_TYPES } from "@/constants/constants";
|
||||
import { APIClassType, InputFieldType } from "@/types/api";
|
||||
import { useMemo } from "react";
|
||||
import ConnectionComponent from "./components/connectionComponent";
|
||||
import DictComponent from "./components/dictComponent";
|
||||
import { EmptyParameterComponent } from "./components/emptyParameterComponent";
|
||||
import FloatComponent from "./components/floatComponent";
|
||||
@@ -17,6 +18,7 @@ import LinkComponent from "./components/linkComponent";
|
||||
import MultiselectComponent from "./components/multiselectComponent";
|
||||
import PromptAreaComponent from "./components/promptComponent";
|
||||
import { RefreshParameterComponent } from "./components/refreshParameterComponent";
|
||||
import SortableListComponent from "./components/sortableListComponent";
|
||||
import { StrRenderComponent } from "./components/strRenderComponent";
|
||||
import ToggleShadComponent from "./components/toggleShadComponent";
|
||||
import { InputProps, NodeInfoType } from "./types";
|
||||
@@ -213,6 +215,37 @@ export function ParameterRenderComponent({
|
||||
id={`slider_${id}`}
|
||||
/>
|
||||
);
|
||||
case "sortableList":
|
||||
return (
|
||||
<SortableListComponent
|
||||
{...baseInputProps}
|
||||
helperText={templateData?.helper_text}
|
||||
helperMetadata={templateData?.helper_text_metadata}
|
||||
options={templateData?.options}
|
||||
searchCategory={templateData?.search_category}
|
||||
limit={templateData?.limit}
|
||||
/>
|
||||
);
|
||||
case "connect":
|
||||
const link =
|
||||
templateData?.options?.find(
|
||||
(option: any) => option?.name === templateValue,
|
||||
)?.link || "";
|
||||
|
||||
return (
|
||||
<ConnectionComponent
|
||||
{...baseInputProps}
|
||||
name={name}
|
||||
nodeId={nodeId}
|
||||
nodeClass={nodeClass}
|
||||
helperText={templateData?.helper_text}
|
||||
helperMetadata={templateData?.helper_text_metadata}
|
||||
options={templateData?.options}
|
||||
searchCategory={templateData?.search_category}
|
||||
buttonMetadata={templateData?.button_metadata}
|
||||
connectionLink={link as string}
|
||||
/>
|
||||
);
|
||||
case "tab":
|
||||
return (
|
||||
<TabComponent
|
||||
|
||||
@@ -964,6 +964,7 @@ export const REFETCH_SERVER_HEALTH_INTERVAL = 20000;
|
||||
export const DRAG_EVENTS_CUSTOM_TYPESS = {
|
||||
genericnode: "genericNode",
|
||||
notenode: "noteNode",
|
||||
"text/plain": "text/plain",
|
||||
};
|
||||
|
||||
export const NOTE_NODE_MIN_WIDTH = 324;
|
||||
|
||||
26
src/frontend/src/icons/GridHorizontal/GridHorizontalIcon.jsx
Normal file
26
src/frontend/src/icons/GridHorizontal/GridHorizontalIcon.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
const SVGGridHorizontalIcon = (props) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-grip-horizontal"
|
||||
{...props}
|
||||
>
|
||||
<circle cx="12" cy="9" r="1" />
|
||||
<circle cx="19" cy="9" r="1" />
|
||||
<circle cx="5" cy="9" r="1" />
|
||||
<circle cx="12" cy="15" r="1" />
|
||||
<circle cx="19" cy="15" r="1" />
|
||||
<circle cx="5" cy="15" r="1" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default SVGGridHorizontalIcon;
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-grip-horizontal"><circle cx="12" cy="9" r="1"/><circle cx="19" cy="9" r="1"/><circle cx="5" cy="9" r="1"/><circle cx="12" cy="15" r="1"/><circle cx="19" cy="15" r="1"/><circle cx="5" cy="15" r="1"/></svg>
|
||||
|
After Width: | Height: | Size: 406 B |
9
src/frontend/src/icons/GridHorizontal/index.tsx
Normal file
9
src/frontend/src/icons/GridHorizontal/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { forwardRef } from "react";
|
||||
import SVGGridHorizontalIcon from "./GridHorizontalIcon";
|
||||
|
||||
export const GridHorizontalIcon = forwardRef<
|
||||
SVGSVGElement,
|
||||
React.PropsWithChildren<{}>
|
||||
>((props, ref) => {
|
||||
return <SVGGridHorizontalIcon ref={ref} {...props} />;
|
||||
});
|
||||
@@ -28,6 +28,8 @@
|
||||
--accent-foreground: 0 0% 0%; /* hsl(0, 0%, 0%) */
|
||||
--destructive: 0 72% 51%; /* hsl(0, 72%, 51%) */
|
||||
--destructive-foreground: 0 0% 100%; /* hsl(0, 0%, 100%) */
|
||||
--accent-amber: 26 90% 37%; /* hsl(26, 90%, 37%) */
|
||||
--accent-amber-foreground: 26 90% 37%; /* hsl(26, 90%, 37%) */
|
||||
--ring: 0 0% 0%; /* hsl(0, 0%, 0%) */
|
||||
--primary-hover: 240 4% 16%; /* hsl(240, 4%, 16%) */
|
||||
--secondary-hover: 240 6% 90%; /* hsl(240, 6%, 90%) */
|
||||
@@ -39,6 +41,7 @@
|
||||
--accent-emerald-hover: 152.4 76% 80.4%; /* hsl(152.4, 76%, 80.4%) */
|
||||
--accent-indigo: 226 100% 94%; /* hsl(226, 100%, 94%) */
|
||||
--accent-indigo-foreground: 243 75% 59%; /* hsl(243, 75%, 59%) */
|
||||
|
||||
--accent-pink: 326 78% 95%; /* hsl(326, 78%, 95%) */
|
||||
--accent-pink-foreground: 333 71% 51%; /* hsl(333, 71%, 51%) */
|
||||
--tooltip: 0 0% 0%; /* hsl(0, 0%, 0%) */
|
||||
|
||||
@@ -6,6 +6,7 @@ import { DuckDuckGoIcon } from "@/icons/DuckDuckGo";
|
||||
import { ExaIcon } from "@/icons/Exa";
|
||||
import { GleanIcon } from "@/icons/Glean";
|
||||
import { GoogleDriveIcon } from "@/icons/GoogleDrive";
|
||||
import { GridHorizontalIcon } from "@/icons/GridHorizontal";
|
||||
import { JSIcon } from "@/icons/JSicon";
|
||||
import { LangwatchIcon } from "@/icons/Langwatch";
|
||||
import { MilvusIcon } from "@/icons/Milvus";
|
||||
@@ -725,6 +726,7 @@ export const nodeIconsLucide: iconsType = {
|
||||
IFixitLoader: IFixIcon,
|
||||
CrewAI: CrewAiIcon,
|
||||
NotDiamond: NotDiamondIcon,
|
||||
GridHorizontal: GridHorizontalIcon,
|
||||
Composio: ComposioIcon,
|
||||
Meta: MetaIcon,
|
||||
Midjorney: MidjourneyIcon,
|
||||
|
||||
16
uv.lock
generated
16
uv.lock
generated
@@ -1383,7 +1383,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "composio-core"
|
||||
version = "0.7.1"
|
||||
version = "0.7.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -1404,14 +1404,14 @@ dependencies = [
|
||||
{ name = "sentry-sdk" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/0c/c852ed97a6bd4c615447d49c56687949ee711a868e6ef992cd655a399ee6/composio_core-0.7.1.tar.gz", hash = "sha256:f51d13154df9baad6768a61b40bcab74d057da63e502ece0503b3b61666b43f0", size = 314490 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/8c/baecb880d69098a17e23724ba8c35237428749d47645d353006428991ab4/composio_core-0.7.12.tar.gz", hash = "sha256:5e13db7c298bb1bbf29e40d139656c22dfde3c2a5a675962ede5673c76d376e4", size = 329357 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/03/33bbe17cca76d3ad0bb239424d5a02d7d52dc4831768cf6a5bef5cab6ac7/composio_core-0.7.1-py3-none-any.whl", hash = "sha256:cbe85a6b2e5b5326c8e067e4b88c201d076d7e46b1a35e0b803852cb001fca61", size = 477780 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6c/71ccaaf26f399e10206dafa529e3bb6664e0eeb5f410636d7063b0eb73ee/composio_core-0.7.12-py3-none-any.whl", hash = "sha256:8904cdc47975e70542cf09499c7b90078371a9289452e941a659eb46d42f3b7a", size = 492528 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "composio-langchain"
|
||||
version = "0.7.1"
|
||||
version = "0.7.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "composio-core" },
|
||||
@@ -1420,9 +1420,9 @@ dependencies = [
|
||||
{ name = "langchainhub" },
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/53/dec4238dd7467f3a033eeb85aefb70002464b11a5fe59659e0b90c62841a/composio_langchain-0.7.1.tar.gz", hash = "sha256:8ffd75c8a0165cc178268ad5f3bdff965886f21dc3ee580816c38c647595ed80", size = 4417 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/9d/e9e2a07eb9f582c8c7e4882f2418b32404b25540c994bd191fcc8a354823/composio_langchain-0.7.12.tar.gz", hash = "sha256:e22098542a8c2e309e79fbbd9d05b46f6b7a3bd59f13429ea1469a3bca7f3082", size = 4447 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/ce/52b0e7c72a0274e0a349f7adf931026ef248f7f5482bcf69f83f8c6cc298/composio_langchain-0.7.1-py3-none-any.whl", hash = "sha256:27ec56e6248e6ce2cde1190831323d83935f4ad62d650cbd71afb4aaf2171671", size = 4844 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/46/902dea095cf3c58c3a5269fabad7ee5d45fc303d48f5e334762a3c1dbae4/composio_langchain-0.7.12-py3-none-any.whl", hash = "sha256:5834b7b39aa1aa3400dac8ca01f372b80e999a6d57110b2d6a7c07fd7416cba5", size = 4878 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4706,8 +4706,8 @@ requires-dist = [
|
||||
{ name = "certifi", specifier = ">=2023.11.17,<2025.0.0" },
|
||||
{ name = "chromadb", specifier = "==0.5.23" },
|
||||
{ name = "clickhouse-connect", marker = "extra == 'clickhouse-connect'", specifier = "==0.7.19" },
|
||||
{ name = "composio-core", specifier = "==0.7.1" },
|
||||
{ name = "composio-langchain", specifier = "==0.7.1" },
|
||||
{ name = "composio-core", specifier = "==0.7.12" },
|
||||
{ name = "composio-langchain", specifier = "==0.7.12" },
|
||||
{ name = "couchbase", marker = "extra == 'couchbase'", specifier = ">=4.2.1" },
|
||||
{ name = "crewai", specifier = "==0.102.0" },
|
||||
{ name = "ctransformers", marker = "extra == 'local'", specifier = ">=0.2.10" },
|
||||
|
||||
Reference in New Issue
Block a user