feat: Generic Callback Dialog Input for Custom Component (#6236)
* force dialog * Reimplement backend dialog * Update astradb.py * Clean up dropdown options * Remove unused import * [autofix.ci] apply automated fixes * Update astradb.py * Ruff fixes * Update Vector Store RAG.json * [autofix.ci] apply automated fixes * fix: Conditionally render custom option dialog in dropdown * ✨ (NodeDialogComponent/index.tsx): Add support for passing 'name' prop to NodeDialog component to improve customization and flexibility 📝 (NodeDialogComponent/index.tsx): Update comments and remove unused import to improve code readability and maintainability 🔧 (dropdownComponent/index.tsx): Pass 'name' prop to Dropdown component to enhance customization and flexibility * ✨ Refactor NodeDialog component to improve state management and payload handling * Update astradb.py * [autofix.ci] apply automated fixes * ✨ Enhance NodeDialog and Dropdown components with improved payload handling and type safety * Add DB creation functionality * First version of create * Update astradb.py * Fix ruff errors * Update Vector Store RAG.json * [autofix.ci] apply automated fixes * Update astradb.py * [autofix.ci] apply automated fixes * Update astradb.py * [autofix.ci] apply automated fixes * Update astradb.py * Update astradb.py * Update astradb.py * Update Vector Store RAG.json * [autofix.ci] apply automated fixes * Update astradb.py * [autofix.ci] apply automated fixes * feat: Enhance dropdown and node dialog with loading states and improved UX * refactor: Improve error handling in NodeDialog component * refactor: Update default excluded keys in dropdown metadata filter * [autofix.ci] apply automated fixes * refactor: Update Vector Store RAG starter project JSON with formatting and connection ID corrections * Hide fields that aren't relevant yet * [autofix.ci] apply automated fixes * Update Vector Store RAG.json * [autofix.ci] apply automated fixes * Update astradb.py * feat: Improve dropdown component with loading states and enhanced UX * Update astradb.py * [autofix.ci] apply automated fixes * Update astradb.py * Simon feedback * [autofix.ci] apply automated fixes * feat: Enhance dropdown and UI components with status indicators and loading states * refactor: Update dropdown metadata filtering to exclude 'icon' key * fix: Conditionally render dropdown icon when available * fix: Improve dropdown icon rendering with null checks * chore: Remove debug console log in dropdown component * Add support for icons in the dropdowns * Update astradb.py * Update Vector Store RAG.json * [autofix.ci] apply automated fixes * feat: Enhance dropdown status display and color handling * feat: Add auto-close functionality to node dialog and expand status color handling * feat: Add real-time template refresh for node dialog fields * refactor: Improve node dialog component state management and naming * Async for create collection * [autofix.ci] apply automated fixes * Dynamic provider list generation * Update astradb.py * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * Update astradb.py * [autofix.ci] apply automated fixes --------- Co-authored-by: Eric Hare <ericrhare@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
This commit is contained in:
parent
b8d2e63221
commit
c902fb9e11
11 changed files with 1314 additions and 538 deletions
|
|
@ -1,8 +1,8 @@
|
||||||
import os
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
|
|
||||||
from astrapy import AstraDBAdmin, DataAPIClient, Database
|
from astrapy import AstraDBAdmin, DataAPIClient, Database
|
||||||
|
from astrapy.info import CollectionDescriptor
|
||||||
from langchain_astradb import AstraDBVectorStore, CollectionVectorServiceOptions
|
from langchain_astradb import AstraDBVectorStore, CollectionVectorServiceOptions
|
||||||
|
|
||||||
from langflow.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store
|
from langflow.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store
|
||||||
|
|
@ -36,22 +36,24 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
default_factory=lambda: {
|
default_factory=lambda: {
|
||||||
"data": {
|
"data": {
|
||||||
"node": {
|
"node": {
|
||||||
"description": "Create a new database in Astra DB.",
|
"name": "create_database",
|
||||||
"display_name": "Create New Database",
|
"description": "",
|
||||||
|
"display_name": "Create new database",
|
||||||
"field_order": ["new_database_name", "cloud_provider", "region"],
|
"field_order": ["new_database_name", "cloud_provider", "region"],
|
||||||
"template": {
|
"template": {
|
||||||
"new_database_name": StrInput(
|
"new_database_name": StrInput(
|
||||||
name="new_database_name",
|
name="new_database_name",
|
||||||
display_name="New Database Name",
|
display_name="Name",
|
||||||
info="Name of the new database to create in Astra DB.",
|
info="Name of the new database to create in Astra DB.",
|
||||||
required=True,
|
required=True,
|
||||||
),
|
),
|
||||||
"cloud_provider": DropdownInput(
|
"cloud_provider": DropdownInput(
|
||||||
name="cloud_provider",
|
name="cloud_provider",
|
||||||
display_name="Cloud Provider",
|
display_name="Cloud provider",
|
||||||
info="Cloud provider for the new database.",
|
info="Cloud provider for the new database.",
|
||||||
options=["Amazon Web Services", "Google Cloud Platform", "Microsoft Azure"],
|
options=["Amazon Web Services", "Google Cloud Platform", "Microsoft Azure"],
|
||||||
required=True,
|
required=True,
|
||||||
|
real_time_refresh=True,
|
||||||
),
|
),
|
||||||
"region": DropdownInput(
|
"region": DropdownInput(
|
||||||
name="region",
|
name="region",
|
||||||
|
|
@ -73,8 +75,9 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
default_factory=lambda: {
|
default_factory=lambda: {
|
||||||
"data": {
|
"data": {
|
||||||
"node": {
|
"node": {
|
||||||
"description": "Create a new collection in Astra DB.",
|
"name": "create_collection",
|
||||||
"display_name": "Create New Collection",
|
"description": "",
|
||||||
|
"display_name": "Create new collection",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"new_collection_name",
|
"new_collection_name",
|
||||||
"embedding_generation_provider",
|
"embedding_generation_provider",
|
||||||
|
|
@ -83,23 +86,31 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
"template": {
|
"template": {
|
||||||
"new_collection_name": StrInput(
|
"new_collection_name": StrInput(
|
||||||
name="new_collection_name",
|
name="new_collection_name",
|
||||||
display_name="New Collection Name",
|
display_name="Name",
|
||||||
info="Name of the new collection to create in Astra DB.",
|
info="Name of the new collection to create in Astra DB.",
|
||||||
required=True,
|
required=True,
|
||||||
),
|
),
|
||||||
"embedding_generation_provider": DropdownInput(
|
"embedding_generation_provider": DropdownInput(
|
||||||
name="embedding_generation_provider",
|
name="embedding_generation_provider",
|
||||||
display_name="Embedding Generation Provider",
|
display_name="Embedding generation method",
|
||||||
info="Provider to use for generating embeddings.",
|
info="Provider to use for generating embeddings.",
|
||||||
options=[],
|
real_time_refresh=True,
|
||||||
required=True,
|
required=True,
|
||||||
|
options=["Bring your own", "Nvidia"],
|
||||||
),
|
),
|
||||||
"embedding_generation_model": DropdownInput(
|
"embedding_generation_model": DropdownInput(
|
||||||
name="embedding_generation_model",
|
name="embedding_generation_model",
|
||||||
display_name="Embedding Generation Model",
|
display_name="Embedding model",
|
||||||
info="Model to use for generating embeddings.",
|
info="Model to use for generating embeddings.",
|
||||||
options=[],
|
|
||||||
required=True,
|
required=True,
|
||||||
|
options=[],
|
||||||
|
),
|
||||||
|
"dimension": IntInput(
|
||||||
|
name="dimension",
|
||||||
|
display_name="Dimensions (Required only for `Bring your own`)",
|
||||||
|
info="Dimensions of the embeddings to generate.",
|
||||||
|
required=False,
|
||||||
|
value=1024,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -125,17 +136,18 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
real_time_refresh=True,
|
real_time_refresh=True,
|
||||||
),
|
),
|
||||||
DropdownInput(
|
DropdownInput(
|
||||||
name="api_endpoint",
|
name="database_name",
|
||||||
display_name="Database",
|
display_name="Database",
|
||||||
info="The Database / API Endpoint for the Astra DB instance.",
|
info="The Database name for the Astra DB instance.",
|
||||||
required=True,
|
required=True,
|
||||||
refresh_button=True,
|
refresh_button=True,
|
||||||
real_time_refresh=True,
|
real_time_refresh=True,
|
||||||
|
dialog_inputs=asdict(NewDatabaseInput()),
|
||||||
combobox=True,
|
combobox=True,
|
||||||
),
|
),
|
||||||
StrInput(
|
StrInput(
|
||||||
name="d_api_endpoint",
|
name="api_endpoint",
|
||||||
display_name="Database API Endpoint",
|
display_name="Astra DB API Endpoint",
|
||||||
info="The API Endpoint for the Astra DB instance. Supercedes database selection.",
|
info="The API Endpoint for the Astra DB instance. Supercedes database selection.",
|
||||||
advanced=True,
|
advanced=True,
|
||||||
),
|
),
|
||||||
|
|
@ -146,8 +158,9 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
required=True,
|
required=True,
|
||||||
refresh_button=True,
|
refresh_button=True,
|
||||||
real_time_refresh=True,
|
real_time_refresh=True,
|
||||||
# dialog_inputs=asdict(NewCollectionInput()),
|
dialog_inputs=asdict(NewCollectionInput()),
|
||||||
combobox=True,
|
combobox=True,
|
||||||
|
advanced=True,
|
||||||
),
|
),
|
||||||
StrInput(
|
StrInput(
|
||||||
name="keyspace",
|
name="keyspace",
|
||||||
|
|
@ -238,6 +251,7 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def map_cloud_providers(cls):
|
def map_cloud_providers(cls):
|
||||||
|
# TODO: Programmatically fetch the regions for each cloud provider
|
||||||
return {
|
return {
|
||||||
"Amazon Web Services": {
|
"Amazon Web Services": {
|
||||||
"id": "aws",
|
"id": "aws",
|
||||||
|
|
@ -254,54 +268,87 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_database_api(
|
def get_vectorize_providers(cls, token: str, environment: str | None = None, api_endpoint: str | None = None):
|
||||||
|
try:
|
||||||
|
# Get the admin object
|
||||||
|
admin = AstraDBAdmin(token=token, environment=environment)
|
||||||
|
db_admin = admin.get_database_admin(api_endpoint=api_endpoint)
|
||||||
|
|
||||||
|
# Get the list of embedding providers
|
||||||
|
embedding_providers = db_admin.find_embedding_providers().as_dict()
|
||||||
|
|
||||||
|
vectorize_providers_mapping = {}
|
||||||
|
# Map the provider display name to the provider key and models
|
||||||
|
for provider_key, provider_data in embedding_providers["embeddingProviders"].items():
|
||||||
|
# Get the provider display name and models
|
||||||
|
display_name = provider_data["displayName"]
|
||||||
|
models = [model["name"] for model in provider_data["models"]]
|
||||||
|
|
||||||
|
# Build our mapping
|
||||||
|
vectorize_providers_mapping[display_name] = [provider_key, models]
|
||||||
|
|
||||||
|
# Sort the resulting dictionary
|
||||||
|
return defaultdict(list, dict(sorted(vectorize_providers_mapping.items())))
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"Error fetching vectorize providers: {e}"
|
||||||
|
raise ValueError(msg) from e
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create_database_api(
|
||||||
cls,
|
cls,
|
||||||
token: str,
|
|
||||||
new_database_name: str,
|
new_database_name: str,
|
||||||
cloud_provider: str,
|
cloud_provider: str,
|
||||||
region: str,
|
region: str,
|
||||||
|
token: str,
|
||||||
|
environment: str | None = None,
|
||||||
|
keyspace: str | None = None,
|
||||||
):
|
):
|
||||||
client = DataAPIClient(token=token)
|
client = DataAPIClient(token=token, environment=environment)
|
||||||
|
|
||||||
# Get the admin object
|
# Get the admin object
|
||||||
admin_client = client.get_admin(token=token)
|
admin_client = client.get_admin(token=token)
|
||||||
|
|
||||||
# Call the create database function
|
# Call the create database function
|
||||||
return admin_client.create_database(
|
return await admin_client.async_create_database(
|
||||||
name=new_database_name,
|
name=new_database_name,
|
||||||
cloud_provider=cloud_provider,
|
cloud_provider=cls.map_cloud_providers()[cloud_provider]["id"],
|
||||||
region=region,
|
region=region,
|
||||||
|
keyspace=keyspace,
|
||||||
|
wait_until_active=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_collection_api(
|
async def create_collection_api(
|
||||||
cls,
|
cls,
|
||||||
token: str,
|
|
||||||
database_name: str,
|
|
||||||
new_collection_name: str,
|
new_collection_name: str,
|
||||||
|
token: str,
|
||||||
|
api_endpoint: str,
|
||||||
|
environment: str | None = None,
|
||||||
|
keyspace: str | None = None,
|
||||||
dimension: int | None = None,
|
dimension: int | None = None,
|
||||||
embedding_generation_provider: str | None = None,
|
embedding_generation_provider: str | None = None,
|
||||||
embedding_generation_model: str | None = None,
|
embedding_generation_model: str | None = None,
|
||||||
):
|
):
|
||||||
|
# Create the data API client
|
||||||
client = DataAPIClient(token=token)
|
client = DataAPIClient(token=token)
|
||||||
api_endpoint = cls.get_api_endpoint_static(token=token, database_name=database_name)
|
|
||||||
|
|
||||||
# Get the database object
|
# Get the database object
|
||||||
database = client.get_database(api_endpoint=api_endpoint, token=token)
|
database = client.get_async_database(api_endpoint=api_endpoint, token=token)
|
||||||
|
|
||||||
# Build vectorize options, if needed
|
# Build vectorize options, if needed
|
||||||
vectorize_options = None
|
vectorize_options = None
|
||||||
if not dimension:
|
if not dimension:
|
||||||
vectorize_options = CollectionVectorServiceOptions(
|
vectorize_options = CollectionVectorServiceOptions(
|
||||||
provider=embedding_generation_provider,
|
provider=cls.get_vectorize_providers(
|
||||||
|
token=token, environment=environment, api_endpoint=api_endpoint
|
||||||
|
).get(embedding_generation_provider, [None, []])[0],
|
||||||
model_name=embedding_generation_model,
|
model_name=embedding_generation_model,
|
||||||
authentication=None,
|
|
||||||
parameters=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create the collection
|
# Create the collection
|
||||||
return database.create_collection(
|
return await database.create_collection(
|
||||||
name=new_collection_name,
|
name=new_collection_name,
|
||||||
|
keyspace=keyspace,
|
||||||
dimension=dimension,
|
dimension=dimension,
|
||||||
service=vectorize_options,
|
service=vectorize_options,
|
||||||
)
|
)
|
||||||
|
|
@ -325,16 +372,28 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
db_info_dict = {}
|
db_info_dict = {}
|
||||||
for db in db_list:
|
for db in db_list:
|
||||||
try:
|
try:
|
||||||
|
# Get the API endpoint for the database
|
||||||
api_endpoint = f"https://{db.info.id}-{db.info.region}.apps.astra{env_string}.datastax.com"
|
api_endpoint = f"https://{db.info.id}-{db.info.region}.apps.astra{env_string}.datastax.com"
|
||||||
db_info_dict[db.info.name] = {
|
|
||||||
"api_endpoint": api_endpoint,
|
# Get the number of collections
|
||||||
"collections": len(
|
try:
|
||||||
|
num_collections = len(
|
||||||
list(
|
list(
|
||||||
client.get_database(
|
client.get_database(
|
||||||
api_endpoint=api_endpoint, token=token, keyspace=db.info.keyspace
|
api_endpoint=api_endpoint, token=token, keyspace=db.info.keyspace
|
||||||
).list_collection_names(keyspace=db.info.keyspace)
|
).list_collection_names(keyspace=db.info.keyspace)
|
||||||
)
|
)
|
||||||
),
|
)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
num_collections = 0
|
||||||
|
if db.status != "PENDING":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add the database to the dictionary
|
||||||
|
db_info_dict[db.info.name] = {
|
||||||
|
"api_endpoint": api_endpoint,
|
||||||
|
"collections": num_collections,
|
||||||
|
"status": db.status if db.status != "ACTIVE" else None,
|
||||||
}
|
}
|
||||||
except Exception: # noqa: BLE001, S110
|
except Exception: # noqa: BLE001, S110
|
||||||
pass
|
pass
|
||||||
|
|
@ -364,15 +423,20 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
if not database_name:
|
if not database_name:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Otherwise, get the URL from the database list
|
# Grab the database object
|
||||||
return cls.get_database_list_static(token=token, environment=environment).get(database_name).get("api_endpoint")
|
db = cls.get_database_list_static(token=token, environment=environment).get(database_name)
|
||||||
|
if not db:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_api_endpoint(self, *, api_endpoint: str | None = None):
|
# Otherwise, get the URL from the database list
|
||||||
|
return db.get("api_endpoint")
|
||||||
|
|
||||||
|
def get_api_endpoint(self):
|
||||||
return self.get_api_endpoint_static(
|
return self.get_api_endpoint_static(
|
||||||
token=self.token,
|
token=self.token,
|
||||||
environment=self.environment,
|
environment=self.environment,
|
||||||
api_endpoint=api_endpoint or self.d_api_endpoint,
|
api_endpoint=self.api_endpoint,
|
||||||
database_name=self.api_endpoint,
|
database_name=self.database_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_keyspace(self):
|
def get_keyspace(self):
|
||||||
|
|
@ -388,7 +452,7 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
client = DataAPIClient(token=self.token, environment=self.environment)
|
client = DataAPIClient(token=self.token, environment=self.environment)
|
||||||
|
|
||||||
return client.get_database(
|
return client.get_database(
|
||||||
api_endpoint=self.get_api_endpoint(api_endpoint=api_endpoint),
|
api_endpoint=api_endpoint or self.get_api_endpoint(),
|
||||||
token=self.token,
|
token=self.token,
|
||||||
keyspace=self.get_keyspace(),
|
keyspace=self.get_keyspace(),
|
||||||
)
|
)
|
||||||
|
|
@ -415,40 +479,15 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_vectorize_providers(self):
|
|
||||||
try:
|
|
||||||
self.log("Dynamically updating list of Vectorize providers.")
|
|
||||||
|
|
||||||
# Get the admin object
|
|
||||||
admin = AstraDBAdmin(token=self.token)
|
|
||||||
db_admin = admin.get_database_admin(api_endpoint=self.get_api_endpoint())
|
|
||||||
|
|
||||||
# Get the list of embedding providers
|
|
||||||
embedding_providers = db_admin.find_embedding_providers().as_dict()
|
|
||||||
|
|
||||||
vectorize_providers_mapping = {}
|
|
||||||
# Map the provider display name to the provider key and models
|
|
||||||
for provider_key, provider_data in embedding_providers["embeddingProviders"].items():
|
|
||||||
display_name = provider_data["displayName"]
|
|
||||||
models = [model["name"] for model in provider_data["models"]]
|
|
||||||
|
|
||||||
# TODO: https://astra.datastax.com/api/v2/graphql
|
|
||||||
vectorize_providers_mapping[display_name] = [provider_key, models]
|
|
||||||
|
|
||||||
# Sort the resulting dictionary
|
|
||||||
return defaultdict(list, dict(sorted(vectorize_providers_mapping.items())))
|
|
||||||
except Exception as e: # noqa: BLE001
|
|
||||||
self.log(f"Error fetching Vectorize providers: {e}")
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _initialize_database_options(self):
|
def _initialize_database_options(self):
|
||||||
try:
|
try:
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"name": name,
|
"name": name,
|
||||||
|
"status": info["status"],
|
||||||
"collections": info["collections"],
|
"collections": info["collections"],
|
||||||
"api_endpoint": info["api_endpoint"],
|
"api_endpoint": info["api_endpoint"],
|
||||||
|
"icon": "data",
|
||||||
}
|
}
|
||||||
for name, info in self.get_database_list().items()
|
for name, info in self.get_database_list().items()
|
||||||
]
|
]
|
||||||
|
|
@ -456,7 +495,35 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
msg = f"Error fetching database options: {e}"
|
msg = f"Error fetching database options: {e}"
|
||||||
raise ValueError(msg) from e
|
raise ValueError(msg) from e
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_provider_icon(cls, collection: CollectionDescriptor | None = None, provider_name: str | None = None) -> str:
|
||||||
|
# Get the provider name from the collection
|
||||||
|
provider_name = provider_name or (
|
||||||
|
collection.options.vector.service.provider
|
||||||
|
if collection and collection.options and collection.options.vector and collection.options.vector.service
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# If there is no provider, use the vector store icon
|
||||||
|
if not provider_name or provider_name == "bring your own":
|
||||||
|
return "vectorstores"
|
||||||
|
|
||||||
|
# Special case for certain models
|
||||||
|
# TODO: Add more icons
|
||||||
|
if provider_name == "nvidia":
|
||||||
|
return "NVIDIA"
|
||||||
|
if provider_name == "openai":
|
||||||
|
return "OpenAI"
|
||||||
|
|
||||||
|
# Title case on the provider for the icon if no special case
|
||||||
|
return provider_name.title()
|
||||||
|
|
||||||
def _initialize_collection_options(self, api_endpoint: str | None = None):
|
def _initialize_collection_options(self, api_endpoint: str | None = None):
|
||||||
|
# Nothing to generate if we don't have an API endpoint yet
|
||||||
|
api_endpoint = api_endpoint or self.get_api_endpoint()
|
||||||
|
if not api_endpoint:
|
||||||
|
return []
|
||||||
|
|
||||||
# Retrieve the database object
|
# Retrieve the database object
|
||||||
database = self.get_database_object(api_endpoint=api_endpoint)
|
database = self.get_database_object(api_endpoint=api_endpoint)
|
||||||
|
|
||||||
|
|
@ -471,7 +538,7 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
"provider": (
|
"provider": (
|
||||||
col.options.vector.service.provider if col.options.vector and col.options.vector.service else None
|
col.options.vector.service.provider if col.options.vector and col.options.vector.service else None
|
||||||
),
|
),
|
||||||
"icon": "",
|
"icon": self.get_provider_icon(collection=col),
|
||||||
"model": (
|
"model": (
|
||||||
col.options.vector.service.model_name if col.options.vector and col.options.vector.service else None
|
col.options.vector.service.model_name if col.options.vector and col.options.vector.service else None
|
||||||
),
|
),
|
||||||
|
|
@ -479,9 +546,53 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
for col in collection_list
|
for col in collection_list
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def reset_provider_options(self, build_config: dict):
|
||||||
|
# Get the list of vectorize providers
|
||||||
|
vectorize_providers = self.get_vectorize_providers(
|
||||||
|
token=self.token,
|
||||||
|
environment=self.environment,
|
||||||
|
api_endpoint=build_config["api_endpoint"]["value"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# If the collection is set, allow user to see embedding options
|
||||||
|
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
||||||
|
"embedding_generation_provider"
|
||||||
|
]["options"] = ["Bring your own", "Nvidia", *[key for key in vectorize_providers if key != "Nvidia"]]
|
||||||
|
|
||||||
|
# For all not Bring your own or Nvidia providers, add metadata saying configure in Astra DB Portal
|
||||||
|
provider_options = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
||||||
|
"embedding_generation_provider"
|
||||||
|
]["options"]
|
||||||
|
|
||||||
|
# Go over each possible provider and add metadata to configure in Astra DB Portal
|
||||||
|
for provider in provider_options:
|
||||||
|
# Skip Bring your own and Nvidia, automatically configured
|
||||||
|
if provider in ["Bring your own", "Nvidia"]:
|
||||||
|
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
||||||
|
"embedding_generation_provider"
|
||||||
|
]["options_metadata"].append({"icon": self.get_provider_icon(provider_name=provider.lower())})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add metadata to configure in Astra DB Portal
|
||||||
|
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
||||||
|
"embedding_generation_provider"
|
||||||
|
]["options_metadata"].append({" ": "Configure in Astra DB Portal"})
|
||||||
|
|
||||||
|
# And allow the user to see the models based on a selected provider
|
||||||
|
embedding_provider = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
||||||
|
"embedding_generation_provider"
|
||||||
|
]["value"]
|
||||||
|
|
||||||
|
# Set the options for the embedding model based on the provider
|
||||||
|
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
||||||
|
"embedding_generation_model"
|
||||||
|
]["options"] = vectorize_providers.get(embedding_provider, [[], []])[1]
|
||||||
|
|
||||||
|
return build_config
|
||||||
|
|
||||||
def reset_collection_list(self, build_config: dict):
|
def reset_collection_list(self, build_config: dict):
|
||||||
# Get the list of options we have based on the token provided
|
# Get the list of options we have based on the token provided
|
||||||
collection_options = self._initialize_collection_options()
|
collection_options = self._initialize_collection_options(api_endpoint=build_config["api_endpoint"]["value"])
|
||||||
|
|
||||||
# If we retrieved options based on the token, show the dropdown
|
# If we retrieved options based on the token, show the dropdown
|
||||||
build_config["collection_name"]["options"] = [col["name"] for col in collection_options]
|
build_config["collection_name"]["options"] = [col["name"] for col in collection_options]
|
||||||
|
|
@ -490,7 +601,11 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
]
|
]
|
||||||
|
|
||||||
# Reset the selected collection
|
# Reset the selected collection
|
||||||
build_config["collection_name"]["value"] = ""
|
if build_config["collection_name"]["value"] not in build_config["collection_name"]["options"]:
|
||||||
|
build_config["collection_name"]["value"] = ""
|
||||||
|
|
||||||
|
# If we have a database, collection name should not be advanced
|
||||||
|
build_config["collection_name"]["advanced"] = not build_config["database_name"]["value"]
|
||||||
|
|
||||||
return build_config
|
return build_config
|
||||||
|
|
||||||
|
|
@ -499,84 +614,171 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
database_options = self._initialize_database_options()
|
database_options = self._initialize_database_options()
|
||||||
|
|
||||||
# If we retrieved options based on the token, show the dropdown
|
# If we retrieved options based on the token, show the dropdown
|
||||||
build_config["api_endpoint"]["options"] = [db["name"] for db in database_options]
|
build_config["database_name"]["options"] = [db["name"] for db in database_options]
|
||||||
build_config["api_endpoint"]["options_metadata"] = [
|
build_config["database_name"]["options_metadata"] = [
|
||||||
{k: v for k, v in db.items() if k not in ["name"]} for db in database_options
|
{k: v for k, v in db.items() if k not in ["name"]} for db in database_options
|
||||||
]
|
]
|
||||||
|
|
||||||
# Reset the selected database
|
# Reset the selected database
|
||||||
build_config["api_endpoint"]["value"] = ""
|
if build_config["database_name"]["value"] not in build_config["database_name"]["options"]:
|
||||||
|
build_config["database_name"]["value"] = ""
|
||||||
|
build_config["api_endpoint"]["value"] = ""
|
||||||
|
build_config["collection_name"]["advanced"] = True
|
||||||
|
|
||||||
|
# If we have a token, database name should not be advanced
|
||||||
|
build_config["database_name"]["advanced"] = not build_config["token"]["value"]
|
||||||
|
|
||||||
return build_config
|
return build_config
|
||||||
|
|
||||||
def reset_build_config(self, build_config: dict):
|
def reset_build_config(self, build_config: dict):
|
||||||
# Reset the list of databases we have based on the token provided
|
# Reset the list of databases we have based on the token provided
|
||||||
build_config["api_endpoint"]["options"] = []
|
build_config["database_name"]["options"] = []
|
||||||
build_config["api_endpoint"]["options_metadata"] = []
|
build_config["database_name"]["options_metadata"] = []
|
||||||
|
build_config["database_name"]["value"] = ""
|
||||||
|
build_config["database_name"]["advanced"] = True
|
||||||
build_config["api_endpoint"]["value"] = ""
|
build_config["api_endpoint"]["value"] = ""
|
||||||
build_config["api_endpoint"]["name"] = "Database"
|
|
||||||
|
|
||||||
# Reset the list of collections and metadata associated
|
# Reset the list of collections and metadata associated
|
||||||
build_config["collection_name"]["options"] = []
|
build_config["collection_name"]["options"] = []
|
||||||
build_config["collection_name"]["options_metadata"] = []
|
build_config["collection_name"]["options_metadata"] = []
|
||||||
build_config["collection_name"]["value"] = ""
|
build_config["collection_name"]["value"] = ""
|
||||||
|
build_config["collection_name"]["advanced"] = True
|
||||||
|
|
||||||
return build_config
|
return build_config
|
||||||
|
|
||||||
def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):
|
async def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):
|
||||||
# When the component first executes, this is the update refresh call
|
# Callback for database creation
|
||||||
first_run = field_name == "collection_name" and not field_value and not build_config["api_endpoint"]["options"]
|
if field_name == "database_name" and isinstance(field_value, dict) and "new_database_name" in field_value:
|
||||||
|
try:
|
||||||
|
await self.create_database_api(
|
||||||
|
new_database_name=field_value["new_database_name"],
|
||||||
|
token=self.token,
|
||||||
|
keyspace=self.get_keyspace(),
|
||||||
|
environment=self.environment,
|
||||||
|
cloud_provider=field_value["cloud_provider"],
|
||||||
|
region=field_value["region"],
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"Error creating database: {e}"
|
||||||
|
raise ValueError(msg) from e
|
||||||
|
|
||||||
# If the token has not been provided, simply return
|
# Add the new database to the list of options
|
||||||
|
build_config["database_name"]["options"] = build_config["database_name"]["options"] + [
|
||||||
|
field_value["new_database_name"]
|
||||||
|
]
|
||||||
|
build_config["database_name"]["options_metadata"] = build_config["database_name"]["options_metadata"] + [
|
||||||
|
{"status": "PENDING"}
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.reset_collection_list(build_config)
|
||||||
|
|
||||||
|
# This is the callback required to update the list of regions for a cloud provider
|
||||||
|
if field_name == "database_name" and isinstance(field_value, dict) and "new_database_name" not in field_value:
|
||||||
|
cloud_provider = field_value["cloud_provider"]
|
||||||
|
build_config["database_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]["region"][
|
||||||
|
"options"
|
||||||
|
] = self.map_cloud_providers()[cloud_provider]["regions"]
|
||||||
|
|
||||||
|
return build_config
|
||||||
|
|
||||||
|
# Callback for the creation of collections
|
||||||
|
if field_name == "collection_name" and isinstance(field_value, dict) and "new_collection_name" in field_value:
|
||||||
|
try:
|
||||||
|
# Get the dimension if its a BYO provider
|
||||||
|
dimension = (
|
||||||
|
field_value["dimension"]
|
||||||
|
if field_value["embedding_generation_provider"] == "Bring your own"
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create the collection
|
||||||
|
await self.create_collection_api(
|
||||||
|
new_collection_name=field_value["new_collection_name"],
|
||||||
|
token=self.token,
|
||||||
|
api_endpoint=build_config["api_endpoint"]["value"],
|
||||||
|
environment=self.environment,
|
||||||
|
keyspace=self.get_keyspace(),
|
||||||
|
dimension=dimension,
|
||||||
|
embedding_generation_provider=field_value["embedding_generation_provider"],
|
||||||
|
embedding_generation_model=field_value["embedding_generation_model"],
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"Error creating collection: {e}"
|
||||||
|
raise ValueError(msg) from e
|
||||||
|
|
||||||
|
# Add the new collection to the list of options
|
||||||
|
build_config["collection_name"]["value"] = field_value["new_collection_name"]
|
||||||
|
build_config["collection_name"]["options"].append(field_value["new_collection_name"])
|
||||||
|
|
||||||
|
# Get the provider and model for the new collection
|
||||||
|
generation_provider = field_value["embedding_generation_provider"]
|
||||||
|
provider = generation_provider if generation_provider != "Bring your own" else None
|
||||||
|
generation_model = field_value["embedding_generation_model"]
|
||||||
|
model = generation_model if generation_model else None
|
||||||
|
|
||||||
|
# Add the new collection to the list of options
|
||||||
|
icon = "NVIDIA" if provider == "Nvidia" else "vectorstores"
|
||||||
|
build_config["collection_name"]["options_metadata"] = build_config["collection_name"][
|
||||||
|
"options_metadata"
|
||||||
|
] + [{"records": 0, "provider": provider, "icon": icon, "model": model}]
|
||||||
|
|
||||||
|
return build_config
|
||||||
|
|
||||||
|
# Callback to update the model list based on the embedding provider
|
||||||
|
if (
|
||||||
|
field_name == "collection_name"
|
||||||
|
and isinstance(field_value, dict)
|
||||||
|
and "new_collection_name" not in field_value
|
||||||
|
):
|
||||||
|
return self.reset_provider_options(build_config)
|
||||||
|
|
||||||
|
# When the component first executes, this is the update refresh call
|
||||||
|
first_run = field_name == "collection_name" and not field_value and not build_config["database_name"]["options"]
|
||||||
|
|
||||||
|
# If the token has not been provided, simply return the empty build config
|
||||||
if not self.token:
|
if not self.token:
|
||||||
return self.reset_build_config(build_config)
|
return self.reset_build_config(build_config)
|
||||||
|
|
||||||
# If this is the first execution of the component, reset and build database list
|
# If this is the first execution of the component, reset and build database list
|
||||||
if first_run or field_name in ["token", "environment"]:
|
if first_run or field_name in ["token", "environment"]:
|
||||||
# Reset the build config to ensure we are starting fresh
|
return self.reset_database_list(build_config)
|
||||||
build_config = self.reset_build_config(build_config)
|
|
||||||
build_config = self.reset_database_list(build_config)
|
|
||||||
|
|
||||||
# Get list of regions for a given cloud provider
|
|
||||||
"""
|
|
||||||
cloud_provider = (
|
|
||||||
build_config["api_endpoint"]["dialog_inputs"]["fields"]["data"]["node"]["template"]["cloud_provider"][
|
|
||||||
"value"
|
|
||||||
]
|
|
||||||
or "Amazon Web Services"
|
|
||||||
)
|
|
||||||
build_config["api_endpoint"]["dialog_inputs"]["fields"]["data"]["node"]["template"]["region"][
|
|
||||||
"options"
|
|
||||||
] = self.map_cloud_providers()[cloud_provider]["regions"]
|
|
||||||
"""
|
|
||||||
|
|
||||||
return build_config
|
|
||||||
|
|
||||||
# Refresh the collection name options
|
# Refresh the collection name options
|
||||||
if field_name == "api_endpoint":
|
if field_name == "database_name" and not isinstance(field_value, dict):
|
||||||
# If missing, refresh the database options
|
# If missing, refresh the database options
|
||||||
if not build_config["api_endpoint"]["options"] or not field_value:
|
if field_value not in build_config["database_name"]["options"]:
|
||||||
return self.update_build_config(build_config, field_value=self.token, field_name="token")
|
build_config = await self.update_build_config(build_config, field_value=self.token, field_name="token")
|
||||||
|
build_config["database_name"]["value"] = ""
|
||||||
|
else:
|
||||||
|
# Find the position of the selected database to align with metadata
|
||||||
|
index_of_name = build_config["database_name"]["options"].index(field_value)
|
||||||
|
|
||||||
# Set the underlying api endpoint value of the database
|
# Initializing database condition
|
||||||
if field_value in build_config["api_endpoint"]["options"]:
|
pending = build_config["database_name"]["options_metadata"][index_of_name]["status"] == "PENDING"
|
||||||
index_of_name = build_config["api_endpoint"]["options"].index(field_value)
|
if pending:
|
||||||
build_config["d_api_endpoint"]["value"] = build_config["api_endpoint"]["options_metadata"][
|
return self.update_build_config(build_config, field_value=self.token, field_name="token")
|
||||||
|
|
||||||
|
# Set the API endpoint based on the selected database
|
||||||
|
build_config["api_endpoint"]["value"] = build_config["database_name"]["options_metadata"][
|
||||||
index_of_name
|
index_of_name
|
||||||
]["api_endpoint"]
|
]["api_endpoint"]
|
||||||
else:
|
|
||||||
build_config["d_api_endpoint"]["value"] = ""
|
# Reset the provider options
|
||||||
|
build_config = self.reset_provider_options(build_config)
|
||||||
|
|
||||||
# Reset the list of collections we have based on the token provided
|
# Reset the list of collections we have based on the token provided
|
||||||
return self.reset_collection_list(build_config)
|
return self.reset_collection_list(build_config)
|
||||||
|
|
||||||
# Hide embedding model option if opriona_metadata provider is not null
|
# Hide embedding model option if opriona_metadata provider is not null
|
||||||
if field_name == "collection_name" and field_value:
|
if field_name == "collection_name" and not isinstance(field_value, dict):
|
||||||
# Assume we will be autodetecting the collection:
|
# Assume we will be autodetecting the collection:
|
||||||
build_config["autodetect_collection"]["value"] = True
|
build_config["autodetect_collection"]["value"] = True
|
||||||
|
|
||||||
|
# Reload the collection list
|
||||||
|
build_config = self.reset_collection_list(build_config)
|
||||||
|
|
||||||
# Set the options for collection name to be the field value if its a new collection
|
# Set the options for collection name to be the field value if its a new collection
|
||||||
if field_value not in build_config["collection_name"]["options"]:
|
if field_value and field_value not in build_config["collection_name"]["options"]:
|
||||||
# Add the new collection to the list of options
|
# Add the new collection to the list of options
|
||||||
build_config["collection_name"]["options"].append(field_value)
|
build_config["collection_name"]["options"].append(field_value)
|
||||||
build_config["collection_name"]["options_metadata"].append(
|
build_config["collection_name"]["options_metadata"].append(
|
||||||
|
|
@ -598,36 +800,8 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
build_config["embedding_model"]["advanced"] = False
|
build_config["embedding_model"]["advanced"] = False
|
||||||
build_config["embedding_choice"]["value"] = "Embedding Model"
|
build_config["embedding_choice"]["value"] = "Embedding Model"
|
||||||
|
|
||||||
# For the final step, get the list of vectorize providers
|
|
||||||
"""
|
|
||||||
vectorize_providers = self.get_vectorize_providers()
|
|
||||||
if not vectorize_providers:
|
|
||||||
return build_config
|
return build_config
|
||||||
|
|
||||||
# Allow the user to see the embedding provider options
|
|
||||||
provider_options = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
|
||||||
"embedding_generation_provider"
|
|
||||||
]["options"]
|
|
||||||
if not provider_options:
|
|
||||||
# If the collection is set, allow user to see embedding options
|
|
||||||
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
|
||||||
"embedding_generation_provider"
|
|
||||||
]["options"] = ["Bring your own", "Nvidia", *[key for key in vectorize_providers if key != "Nvidia"]]
|
|
||||||
|
|
||||||
# And allow the user to see the models based on a selected provider
|
|
||||||
model_options = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
|
||||||
"embedding_generation_model"
|
|
||||||
]["options"]
|
|
||||||
if not model_options:
|
|
||||||
embedding_provider = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
|
||||||
"embedding_generation_provider"
|
|
||||||
]["value"]
|
|
||||||
|
|
||||||
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
|
||||||
"embedding_generation_model"
|
|
||||||
]["options"] = vectorize_providers.get(embedding_provider, [[], []])[1]
|
|
||||||
"""
|
|
||||||
|
|
||||||
return build_config
|
return build_config
|
||||||
|
|
||||||
@check_cached_vector_store
|
@check_cached_vector_store
|
||||||
|
|
@ -654,11 +828,11 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
# Get Langflow version and platform information
|
# Get Langflow version and platform information
|
||||||
__version__ = get_version_info()["version"]
|
__version__ = get_version_info()["version"]
|
||||||
langflow_prefix = ""
|
langflow_prefix = ""
|
||||||
if os.getenv("AWS_EXECUTION_ENV") == "AWS_ECS_FARGATE": # TODO: More precise way of detecting
|
# if os.getenv("AWS_EXECUTION_ENV") == "AWS_ECS_FARGATE": # TODO: More precise way of detecting
|
||||||
langflow_prefix = "ds-"
|
# langflow_prefix = "ds-"
|
||||||
|
|
||||||
# Get the database object
|
# Get the database object
|
||||||
database = self.get_database_object(api_endpoint=self.d_api_endpoint)
|
database = self.get_database_object()
|
||||||
autodetect = self.collection_name in database.list_collection_names() and self.autodetect_collection
|
autodetect = self.collection_name in database.list_collection_names() and self.autodetect_collection
|
||||||
|
|
||||||
# Bundle up the auto-detect parameters
|
# Bundle up the auto-detect parameters
|
||||||
|
|
@ -714,7 +888,7 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
if documents and self.deletion_field:
|
if documents and self.deletion_field:
|
||||||
self.log(f"Deleting documents where {self.deletion_field}")
|
self.log(f"Deleting documents where {self.deletion_field}")
|
||||||
try:
|
try:
|
||||||
database = self.get_database_object(api_endpoint=self.d_api_endpoint)
|
database = self.get_database_object()
|
||||||
collection = database.get_collection(self.collection_name, keyspace=database.keyspace)
|
collection = database.get_collection(self.collection_name, keyspace=database.keyspace)
|
||||||
delete_values = list({doc.metadata[self.deletion_field] for doc in documents})
|
delete_values = list({doc.metadata[self.deletion_field] for doc in documents})
|
||||||
self.log(f"Deleting documents where {self.deletion_field} matches {delete_values}.")
|
self.log(f"Deleting documents where {self.deletion_field} matches {delete_values}.")
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -8,105 +8,184 @@ import {
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value";
|
||||||
import { getCustomParameterTitle } from "@/customization/components/custom-parameter";
|
import { getCustomParameterTitle } from "@/customization/components/custom-parameter";
|
||||||
import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value";
|
import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template";
|
||||||
|
import useAlertStore from "@/stores/alertStore";
|
||||||
import useFlowStore from "@/stores/flowStore";
|
import useFlowStore from "@/stores/flowStore";
|
||||||
import { InputFieldType } from "@/types/api";
|
import { APIClassType, InputFieldType } from "@/types/api";
|
||||||
import { cloneDeep } from "lodash";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const NodeDialog = ({
|
interface NodeDialogProps {
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
dialogInputs,
|
|
||||||
nodeId,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
dialogInputs: any;
|
dialogInputs: any;
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
|
name: string;
|
||||||
|
nodeClass: APIClassType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ValueObject {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NodeDialog: React.FC<NodeDialogProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
dialogInputs,
|
||||||
|
nodeId,
|
||||||
|
name,
|
||||||
|
nodeClass,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [fieldValues, setFieldValues] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const nodes = useFlowStore((state) => state.nodes);
|
const nodes = useFlowStore((state) => state.nodes);
|
||||||
const setNode = useFlowStore((state) => state.setNode);
|
const setNode = useFlowStore((state) => state.setNode);
|
||||||
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
|
|
||||||
const handleNewValue = (value: string, key: string) => {
|
const postTemplateValue = usePostTemplateValue({
|
||||||
let rawValue = value;
|
parameterId: name,
|
||||||
|
nodeId: nodeId,
|
||||||
|
node: nodeClass,
|
||||||
|
});
|
||||||
|
|
||||||
if (typeof value === "object" && value) {
|
const { fields, functionality: submitButtonText } = dialogInputs || {};
|
||||||
rawValue = (value as { value: string }).value;
|
const dialogNodeData = fields?.data?.node;
|
||||||
}
|
const dialogTemplate = dialogNodeData?.template || {};
|
||||||
|
|
||||||
const template = cloneDeep(dialogInputs?.fields?.data?.node?.template);
|
const setNodeClass = (newNode: APIClassType) => {
|
||||||
template[key].value = value;
|
const targetNode = nodes.find((node) => node.id === nodeId);
|
||||||
|
if (!targetNode) return;
|
||||||
|
|
||||||
const newNode = cloneDeep(nodes.find((node) => node.id === nodeId));
|
targetNode.data.node = newNode;
|
||||||
if (newNode) {
|
setNode(nodeId, targetNode);
|
||||||
const template = newNode.data.node.template;
|
|
||||||
const databaseFields = template.database_name.dialog_inputs.fields;
|
|
||||||
const nodeTemplate = databaseFields.data.node.template;
|
|
||||||
|
|
||||||
nodeTemplate[key].value = rawValue;
|
|
||||||
}
|
|
||||||
setNode(nodeId, newNode!);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleErrorData = (newState: {
|
||||||
|
title: string;
|
||||||
|
list?: Array<string>;
|
||||||
|
}) => {
|
||||||
|
setErrorData(newState);
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFieldValue = (value: string | ValueObject, fieldKey: string) => {
|
||||||
|
const newValue = typeof value === "object" ? value.value : value;
|
||||||
|
const targetNode = nodes.find((node) => node.id === nodeId);
|
||||||
|
if (!targetNode || !name) return;
|
||||||
|
|
||||||
|
targetNode.data.node.template[name].dialog_inputs.fields.data.node.template[
|
||||||
|
fieldKey
|
||||||
|
].value = newValue;
|
||||||
|
setNode(nodeId, targetNode);
|
||||||
|
setFieldValues((prev) => ({ ...prev, [fieldKey]: newValue }));
|
||||||
|
|
||||||
|
if (dialogTemplate[fieldKey].real_time_refresh) {
|
||||||
|
mutateTemplate(
|
||||||
|
{ [fieldKey]: newValue },
|
||||||
|
nodeClass,
|
||||||
|
setNodeClass,
|
||||||
|
postTemplateValue,
|
||||||
|
handleErrorData,
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDialog = () => {
|
||||||
|
setFieldValues({});
|
||||||
|
const targetNode = nodes.find((node) => node.id === nodeId);
|
||||||
|
if (targetNode && name) {
|
||||||
|
const nodeTemplate = targetNode.data.node.template;
|
||||||
|
Object.keys(dialogTemplate).forEach((key) => {
|
||||||
|
nodeTemplate[name].dialog_inputs.fields.data.node.template[key].value =
|
||||||
|
"";
|
||||||
|
});
|
||||||
|
setNode(nodeId, targetNode);
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitDialog = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
await mutateTemplate(
|
||||||
|
fieldValues,
|
||||||
|
nodeClass,
|
||||||
|
setNodeClass,
|
||||||
|
postTemplateValue,
|
||||||
|
handleErrorData,
|
||||||
|
name,
|
||||||
|
handleCloseDialog,
|
||||||
|
nodeClass.tool_mode,
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
handleCloseDialog();
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onClose}>
|
<Dialog open={open} onOpenChange={handleCloseDialog}>
|
||||||
<DialogContent className="max-w-[700px] gap-2 px-1 py-6">
|
<DialogContent className="max-w-[700px] gap-2 px-1 py-6">
|
||||||
<DialogHeader className="px-5 pb-3">
|
<DialogHeader className="px-5 pb-3">
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="pb-2">
|
<span className="pb-2">{dialogNodeData?.display_name}</span>
|
||||||
{dialogInputs.fields?.data?.node?.display_name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{dialogInputs.fields?.data?.node?.description}
|
{dialogNodeData?.description}
|
||||||
</div>
|
</div>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex flex-col gap-5 overflow-y-auto px-5">
|
<div className="flex flex-col gap-5 overflow-y-auto px-5">
|
||||||
{Object.entries(dialogInputs?.fields?.data?.node?.template ?? {}).map(
|
{Object.entries(dialogTemplate).map(([fieldKey, fieldValue]) => (
|
||||||
([key, value]) => (
|
<div key={fieldKey}>
|
||||||
<div key={key}>
|
<div>
|
||||||
<div>
|
{getCustomParameterTitle({
|
||||||
{getCustomParameterTitle({
|
title:
|
||||||
title:
|
(fieldValue as { display_name: string })?.display_name ??
|
||||||
dialogInputs?.fields?.data?.node?.template[key]
|
"",
|
||||||
.display_name ?? "",
|
nodeId,
|
||||||
nodeId,
|
isFlexView: false,
|
||||||
isFlexView: false,
|
})}
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<ParameterRenderComponent
|
|
||||||
handleOnNewValue={(value: string) =>
|
|
||||||
handleNewValue(value, key)
|
|
||||||
}
|
|
||||||
name={key}
|
|
||||||
nodeId={nodeId}
|
|
||||||
templateData={value as Partial<InputFieldType>}
|
|
||||||
templateValue={
|
|
||||||
dialogInputs?.fields?.data?.node?.template[key].value
|
|
||||||
}
|
|
||||||
editNode={false}
|
|
||||||
handleNodeClass={() => {}}
|
|
||||||
nodeClass={dialogInputs.fields?.data?.node}
|
|
||||||
disabled={false}
|
|
||||||
placeholder=""
|
|
||||||
isToolMode={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
<ParameterRenderComponent
|
||||||
)}
|
handleOnNewValue={(value: string) =>
|
||||||
|
updateFieldValue(value, fieldKey)
|
||||||
|
}
|
||||||
|
name={fieldKey}
|
||||||
|
nodeId={nodeId}
|
||||||
|
templateData={fieldValue as Partial<InputFieldType>}
|
||||||
|
templateValue={fieldValues[fieldKey] || ""}
|
||||||
|
editNode={false}
|
||||||
|
handleNodeClass={() => {}}
|
||||||
|
nodeClass={dialogNodeData}
|
||||||
|
disabled={false}
|
||||||
|
placeholder=""
|
||||||
|
isToolMode={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="px-5 pt-3">
|
<DialogFooter className="px-5 pt-3">
|
||||||
<Button variant="outline" onClick={onClose}>
|
<Button variant="secondary" onClick={handleCloseDialog}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button>{dialogInputs.functionality}</Button>
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={handleSubmitDialog}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
{submitButtonText}
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import ForwardedIconComponent from "../genericIconComponent";
|
||||||
|
|
||||||
|
const FetchIconComponent = ({
|
||||||
|
source,
|
||||||
|
name,
|
||||||
|
}: {
|
||||||
|
source: string;
|
||||||
|
name: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{source ? (
|
||||||
|
<img src={source} alt={name} />
|
||||||
|
) : (
|
||||||
|
<ForwardedIconComponent name="Unknown" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FetchIconComponent;
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const LoadingTextComponent = ({ text }: { text: string }) => {
|
||||||
|
const [dots, setDots] = useState(".");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setDots((prevDots) => (prevDots === "..." ? "" : `${prevDots}.`));
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{`${text}${dots}`}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadingTextComponent;
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
import LoadingTextComponent from "@/components/common/loadingTextComponent";
|
||||||
import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value";
|
import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value";
|
||||||
import NodeDialog from "@/CustomNodes/GenericNode/components/NodeDialogComponent";
|
import NodeDialog from "@/CustomNodes/GenericNode/components/NodeDialogComponent";
|
||||||
import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template";
|
import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template";
|
||||||
import useAlertStore from "@/stores/alertStore";
|
import useAlertStore from "@/stores/alertStore";
|
||||||
|
import { getStatusColor } from "@/utils/stringManipulation";
|
||||||
import { PopoverAnchor } from "@radix-ui/react-popover";
|
import { PopoverAnchor } from "@radix-ui/react-popover";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
|
|
@ -13,7 +15,6 @@ import ShadTooltip from "../../common/shadTooltipComponent";
|
||||||
import { Button } from "../../ui/button";
|
import { Button } from "../../ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
|
|
@ -42,25 +43,41 @@ export default function Dropdown({
|
||||||
dialogInputs,
|
dialogInputs,
|
||||||
...baseInputProps
|
...baseInputProps
|
||||||
}: BaseInputProps & DropDownComponent): JSX.Element {
|
}: BaseInputProps & DropDownComponent): JSX.Element {
|
||||||
const nodeId = baseInputProps?.nodeId;
|
// Initialize state and refs
|
||||||
|
const [open, setOpen] = useState(children ? true : false);
|
||||||
|
const [openDialog, setOpenDialog] = useState(false);
|
||||||
|
const [customValue, setCustomValue] = useState("");
|
||||||
|
const [filteredOptions, setFilteredOptions] = useState(options);
|
||||||
|
const [refreshOptions, setRefreshOptions] = useState(false);
|
||||||
|
const refButton = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
// Initialize utilities and constants
|
||||||
const placeholderName = name
|
const placeholderName = name
|
||||||
? formatPlaceholderName(name)
|
? formatPlaceholderName(name)
|
||||||
: "Choose an option...";
|
: "Choose an option...";
|
||||||
|
|
||||||
const { firstWord } = formatName(name);
|
const { firstWord } = formatName(name);
|
||||||
const [open, setOpen] = useState(children ? true : false);
|
const fuse = new Fuse(options, { keys: ["name", "value"] });
|
||||||
const [openDialog, setOpenDialog] = useState(false);
|
|
||||||
|
|
||||||
const refButton = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
const PopoverContentDropdown =
|
const PopoverContentDropdown =
|
||||||
children || editNode ? PopoverContent : PopoverContentWithoutPortal;
|
children || editNode ? PopoverContent : PopoverContentWithoutPortal;
|
||||||
|
const { nodeClass, nodeId, handleNodeClass, tooltip } = baseInputProps;
|
||||||
|
|
||||||
const [customValue, setCustomValue] = useState("");
|
// API and store hooks
|
||||||
const [filteredOptions, setFilteredOptions] = useState(options);
|
const postTemplateValue = usePostTemplateValue({
|
||||||
|
parameterId: name || "",
|
||||||
|
nodeId: nodeId || "",
|
||||||
|
node: nodeClass!,
|
||||||
|
});
|
||||||
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
|
|
||||||
const fuse = new Fuse(options, { keys: ["name", "value"] });
|
// Utility functions
|
||||||
|
const filterMetadataKeys = (
|
||||||
|
metadata: Record<string, any> = {},
|
||||||
|
excludeKeys: string[] = ["api_endpoint", "icon", "status"],
|
||||||
|
) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(metadata).filter(([key]) => !excludeKeys.includes(key)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const searchRoleByTerm = async (event: ChangeEvent<HTMLInputElement>) => {
|
const searchRoleByTerm = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
|
|
@ -71,28 +88,43 @@ export default function Dropdown({
|
||||||
setCustomValue(value);
|
setCustomValue(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { nodeClass, handleNodeClass } = baseInputProps;
|
const handleRefreshButtonPress = async () => {
|
||||||
|
setRefreshOptions(true);
|
||||||
|
setOpen(false);
|
||||||
|
|
||||||
const postTemplateValue = usePostTemplateValue({
|
await mutateTemplate(
|
||||||
parameterId: name || "",
|
|
||||||
nodeId: id,
|
|
||||||
node: nodeClass!,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { isPending } = postTemplateValue;
|
|
||||||
|
|
||||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
|
||||||
|
|
||||||
const handleRefreshButtonPress = () => {
|
|
||||||
mutateTemplate(
|
|
||||||
value,
|
value,
|
||||||
nodeClass!,
|
nodeClass!,
|
||||||
handleNodeClass,
|
handleNodeClass,
|
||||||
postTemplateValue,
|
postTemplateValue,
|
||||||
setErrorData,
|
setErrorData,
|
||||||
);
|
)?.then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setRefreshOptions(false);
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatTooltipContent = (option: string, index: number) => {
|
||||||
|
if (!optionsMetaData?.[index]) return option;
|
||||||
|
|
||||||
|
const metadata = optionsMetaData[index];
|
||||||
|
const metadataEntries = Object.entries(metadata)
|
||||||
|
.filter(([key, value]) => value !== null && key !== "icon")
|
||||||
|
.map(([key, value]) => {
|
||||||
|
const displayValue =
|
||||||
|
typeof value === "string" && value.length > 20
|
||||||
|
? `${value.substring(0, 30)}...`
|
||||||
|
: String(value);
|
||||||
|
return `${key}: ${displayValue}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return metadataEntries.length > 0
|
||||||
|
? `${firstWord}: ${option}\n${metadataEntries.join("\n")}`
|
||||||
|
: option;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Effects
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabled && value !== "") {
|
if (disabled && value !== "") {
|
||||||
onSelect("", undefined, true);
|
onSelect("", undefined, true);
|
||||||
|
|
@ -109,61 +141,71 @@ export default function Dropdown({
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
// Render helper functions
|
||||||
|
|
||||||
|
const renderLoadingButton = () => (
|
||||||
|
<Button
|
||||||
|
className="dropdown-component-false-outline w-full justify-between py-2 font-normal"
|
||||||
|
variant="primary"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
<LoadingTextComponent text="Loading options" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
const renderTriggerButton = () => (
|
const renderTriggerButton = () => (
|
||||||
<PopoverTrigger asChild>
|
<ShadTooltip content={!value ? (tooltip as string) : ""}>
|
||||||
<Button
|
<PopoverTrigger asChild>
|
||||||
disabled={
|
<Button
|
||||||
disabled ||
|
disabled={
|
||||||
(Object.keys(options).length === 0 &&
|
disabled ||
|
||||||
!combobox &&
|
(Object.keys(options).length === 0 &&
|
||||||
!dialogInputs?.fields?.data?.node?.template)
|
!combobox &&
|
||||||
}
|
!dialogInputs?.fields?.data?.node?.template)
|
||||||
variant="primary"
|
}
|
||||||
size="xs"
|
variant="primary"
|
||||||
role="combobox"
|
size="xs"
|
||||||
ref={refButton}
|
role="combobox"
|
||||||
aria-expanded={open}
|
ref={refButton}
|
||||||
data-testid={id}
|
aria-expanded={open}
|
||||||
className={cn(
|
data-testid={id}
|
||||||
editNode
|
|
||||||
? "dropdown-component-outline input-edit-node"
|
|
||||||
: "dropdown-component-false-outline py-2",
|
|
||||||
"w-full justify-between font-normal",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="flex items-center gap-2 truncate"
|
|
||||||
data-testid={`value-dropdown-${id}`}
|
|
||||||
>
|
|
||||||
{optionsMetaData?.[
|
|
||||||
filteredOptions.findIndex((option) => option === value)
|
|
||||||
]?.icon && (
|
|
||||||
<ForwardedIconComponent
|
|
||||||
name={
|
|
||||||
optionsMetaData[
|
|
||||||
filteredOptions.findIndex((option) => option === value)
|
|
||||||
].icon
|
|
||||||
}
|
|
||||||
className="h-4 w-4"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{value &&
|
|
||||||
value !== "" &&
|
|
||||||
filteredOptions.find((option) => option === value)
|
|
||||||
? filteredOptions.find((option) => option === value)
|
|
||||||
: placeholderName}
|
|
||||||
</span>
|
|
||||||
<ForwardedIconComponent
|
|
||||||
name="ChevronsUpDown"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-2 h-4 w-4 shrink-0 text-foreground",
|
editNode
|
||||||
disabled
|
? "dropdown-component-outline input-edit-node"
|
||||||
? "hover:text-placeholder-foreground"
|
: "dropdown-component-false-outline py-2",
|
||||||
: "hover:text-foreground",
|
"w-full justify-between font-normal",
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
</Button>
|
<span
|
||||||
</PopoverTrigger>
|
className="flex items-center gap-2 truncate"
|
||||||
|
data-testid={`value-dropdown-${id}`}
|
||||||
|
>
|
||||||
|
{optionsMetaData?.[
|
||||||
|
filteredOptions.findIndex((option) => option === value)
|
||||||
|
]?.icon && (
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name={
|
||||||
|
optionsMetaData?.[
|
||||||
|
filteredOptions.findIndex((option) => option === value)
|
||||||
|
]?.icon
|
||||||
|
}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{value && filteredOptions.includes(value) ? value : placeholderName}{" "}
|
||||||
|
</span>
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name="ChevronsUpDown"
|
||||||
|
className={cn(
|
||||||
|
"ml-2 h-4 w-4 shrink-0 text-foreground",
|
||||||
|
disabled
|
||||||
|
? "hover:text-placeholder-foreground"
|
||||||
|
: "hover:text-foreground",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</ShadTooltip>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderSearchInput = () => (
|
const renderSearchInput = () => (
|
||||||
|
|
@ -211,10 +253,7 @@ export default function Dropdown({
|
||||||
<div className="flex items-center gap-2 pl-1">
|
<div className="flex items-center gap-2 pl-1">
|
||||||
<ForwardedIconComponent
|
<ForwardedIconComponent
|
||||||
name="RefreshCcw"
|
name="RefreshCcw"
|
||||||
className={cn(
|
className={cn("refresh-icon h-3 w-3 text-primary")}
|
||||||
"h-3 w-3 text-primary",
|
|
||||||
isPending && "animate-spin",
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
Refresh list
|
Refresh list
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -223,87 +262,118 @@ export default function Dropdown({
|
||||||
<NodeDialog
|
<NodeDialog
|
||||||
open={openDialog}
|
open={openDialog}
|
||||||
dialogInputs={dialogInputs}
|
dialogInputs={dialogInputs}
|
||||||
onClose={() => setOpenDialog(false)}
|
onClose={() => {
|
||||||
|
setOpenDialog(false);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
nodeId={nodeId!}
|
nodeId={nodeId!}
|
||||||
|
name={name!}
|
||||||
|
nodeClass={nodeClass!}
|
||||||
/>
|
/>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderOptionsList = () => (
|
const renderOptionsList = () => (
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>No values found.</CommandEmpty>
|
|
||||||
<CommandGroup defaultChecked={false}>
|
<CommandGroup defaultChecked={false}>
|
||||||
{filteredOptions?.map((option, index) => (
|
{filteredOptions?.length > 0 ? (
|
||||||
<ShadTooltip key={index} delayDuration={700} content={option}>
|
filteredOptions?.map((option, index) => (
|
||||||
<div>
|
<ShadTooltip
|
||||||
<CommandItem
|
key={index}
|
||||||
value={option}
|
delayDuration={700}
|
||||||
onSelect={(currentValue) => {
|
styleClasses="whitespace-pre-wrap"
|
||||||
onSelect(currentValue);
|
content={formatTooltipContent(option, index)}
|
||||||
setOpen(false);
|
>
|
||||||
}}
|
<div>
|
||||||
className="items-center"
|
<CommandItem
|
||||||
data-testid={`${option}-${index}-option`}
|
value={option}
|
||||||
>
|
onSelect={(currentValue) => {
|
||||||
<div className="flex w-full items-center gap-2">
|
onSelect(currentValue);
|
||||||
{optionsMetaData?.[index]?.icon ? (
|
setOpen(false);
|
||||||
<ForwardedIconComponent
|
}}
|
||||||
name={optionsMetaData?.[index]?.icon}
|
className="items-center"
|
||||||
className="h-4 w-4 shrink-0 text-primary"
|
data-testid={`${option}-${index}-option`}
|
||||||
/>
|
>
|
||||||
) : null}
|
<div className="flex w-full items-center gap-2">
|
||||||
<div
|
{optionsMetaData && optionsMetaData.length > 0 && (
|
||||||
className={cn("flex truncate", {
|
<ForwardedIconComponent
|
||||||
"flex-col":
|
name={optionsMetaData?.[index]?.icon || "Unknown"}
|
||||||
optionsMetaData && optionsMetaData?.length > 0,
|
className="h-4 w-4 shrink-0 text-primary"
|
||||||
"w-full pl-2": !optionsMetaData?.[index]?.icon,
|
/>
|
||||||
})}
|
)}
|
||||||
>
|
<div
|
||||||
<div className="flex truncate">{option}</div>
|
className={cn("flex truncate", {
|
||||||
{optionsMetaData && optionsMetaData?.length > 0 ? (
|
"flex-col":
|
||||||
<div className="flex w-full items-center text-muted-foreground">
|
optionsMetaData && optionsMetaData?.length > 0,
|
||||||
{Object.entries(optionsMetaData?.[index] || {})
|
"w-full pl-2": !optionsMetaData?.[index]?.icon,
|
||||||
.filter(
|
})}
|
||||||
([key, value]) => value !== null && key !== "icon",
|
>
|
||||||
|
<div className="flex truncate">
|
||||||
|
{option}{" "}
|
||||||
|
<span
|
||||||
|
className={`flex items-center pl-2 text-xs ${getStatusColor(
|
||||||
|
optionsMetaData?.[index]?.status,
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
<LoadingTextComponent
|
||||||
|
text={optionsMetaData?.[
|
||||||
|
index
|
||||||
|
]?.status?.toLowerCase()}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{optionsMetaData && optionsMetaData?.length > 0 ? (
|
||||||
|
<div className="flex w-full items-center text-muted-foreground">
|
||||||
|
{Object.entries(
|
||||||
|
filterMetadataKeys(optionsMetaData?.[index] || {}),
|
||||||
)
|
)
|
||||||
.map(([key, value], i, arr) => (
|
.filter(
|
||||||
<div
|
([key, value]) =>
|
||||||
key={key}
|
value !== null && key !== "icon",
|
||||||
className={cn("flex items-center", {
|
)
|
||||||
truncate: i === arr.length - 1,
|
.map(([key, value], i, arr) => (
|
||||||
})}
|
|
||||||
>
|
|
||||||
{i > 0 && (
|
|
||||||
<ForwardedIconComponent
|
|
||||||
name="Circle"
|
|
||||||
className="mx-1 h-1 w-1 overflow-visible fill-muted-foreground"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className={cn("text-xs", {
|
key={key}
|
||||||
|
className={cn("flex items-center", {
|
||||||
truncate: i === arr.length - 1,
|
truncate: i === arr.length - 1,
|
||||||
})}
|
})}
|
||||||
>{`${String(value)} ${key}`}</div>
|
>
|
||||||
</div>
|
{i > 0 && (
|
||||||
))}
|
<ForwardedIconComponent
|
||||||
</div>
|
name="Circle"
|
||||||
) : (
|
className="mx-1 h-1 w-1 overflow-visible fill-muted-foreground"
|
||||||
<div className="ml-auto flex">
|
/>
|
||||||
<ForwardedIconComponent
|
)}
|
||||||
name="Check"
|
<div
|
||||||
className={cn(
|
className={cn("text-xs", {
|
||||||
"h-4 w-4 shrink-0 text-primary",
|
truncate: i === arr.length - 1,
|
||||||
value === option ? "opacity-100" : "opacity-0",
|
})}
|
||||||
)}
|
>{`${String(value)} ${key}`}</div>
|
||||||
/>
|
</div>
|
||||||
</div>
|
))}
|
||||||
)}
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="ml-auto flex">
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name="Check"
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 shrink-0 text-primary",
|
||||||
|
value === option ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CommandItem>
|
||||||
</CommandItem>
|
</div>
|
||||||
</div>
|
</ShadTooltip>
|
||||||
</ShadTooltip>
|
))
|
||||||
))}
|
) : (
|
||||||
|
<CommandItem disabled className="text-center text-sm">
|
||||||
|
No options found
|
||||||
|
</CommandItem>
|
||||||
|
)}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
{dialogInputs && dialogInputs?.fields && renderCustomOptionDialog()}
|
{dialogInputs && dialogInputs?.fields && renderCustomOptionDialog()}
|
||||||
|
|
@ -320,12 +390,13 @@ export default function Dropdown({
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Command>
|
<Command>
|
||||||
{renderSearchInput()}
|
{filteredOptions?.length > 0 && renderSearchInput()}
|
||||||
{renderOptionsList()}
|
{renderOptionsList()}
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContentDropdown>
|
</PopoverContentDropdown>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Loading state
|
||||||
if (Object.keys(options).length === 0 && !combobox && isLoading) {
|
if (Object.keys(options).length === 0 && !combobox && isLoading) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -334,10 +405,13 @@ export default function Dropdown({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Main render
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={children ? () => {} : setOpen}>
|
<Popover open={open} onOpenChange={children ? () => {} : setOpen}>
|
||||||
{children ? (
|
{children ? (
|
||||||
<PopoverAnchor>{children}</PopoverAnchor>
|
<PopoverAnchor>{children}</PopoverAnchor>
|
||||||
|
) : refreshOptions || isLoading ? (
|
||||||
|
renderLoadingButton()
|
||||||
) : (
|
) : (
|
||||||
renderTriggerButton()
|
renderTriggerButton()
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export type BaseInputProps<valueType = any> = {
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
isToolMode?: boolean;
|
isToolMode?: boolean;
|
||||||
|
tooltip?: string;
|
||||||
metadata?: any;
|
metadata?: any;
|
||||||
nodeId?: string;
|
nodeId?: string;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,8 @@
|
||||||
--placeholder-foreground: 240 4% 46%; /* hsl(240, 4%, 46%) */
|
--placeholder-foreground: 240 4% 46%; /* hsl(240, 4%, 46%) */
|
||||||
--canvas: 0 0% 0%; /* hsl(0, 0%, 0%) */
|
--canvas: 0 0% 0%; /* hsl(0, 0%, 0%) */
|
||||||
--canvas-dot: 240 5.3% 26.1%; /* hsl(240, 5.3%, 26.1%) */
|
--canvas-dot: 240 5.3% 26.1%; /* hsl(240, 5.3%, 26.1%) */
|
||||||
|
--accent-amber: 26 90% 37%; /* hsl(26, 90%, 37%) */
|
||||||
|
--accent-amber-foreground: 26 90% 37%; /* hsl(26, 90%, 37%) */
|
||||||
--accent-emerald: 164 86% 16%; /* hsl(164, 86%, 16%) */
|
--accent-emerald: 164 86% 16%; /* hsl(164, 86%, 16%) */
|
||||||
--accent-emerald-foreground: 158 64% 52%; /* hsl(158, 64%, 52%) */
|
--accent-emerald-foreground: 158 64% 52%; /* hsl(158, 64%, 52%) */
|
||||||
--accent-emerald-hover: 163.1 88.1% 19.8%; /* hsl(163.1, 88.1%, 19.8%) */
|
--accent-emerald-hover: 163.1 88.1% 19.8%; /* hsl(163.1, 88.1%, 19.8%) */
|
||||||
|
|
|
||||||
|
|
@ -113,3 +113,24 @@ export function parseString(
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getStatusColor = (status: string): string => {
|
||||||
|
const amberStatuses = [
|
||||||
|
"initializing",
|
||||||
|
"pending",
|
||||||
|
"hibernating",
|
||||||
|
"hiberated",
|
||||||
|
"maintenance",
|
||||||
|
"parked",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (amberStatuses.includes(status?.toLowerCase())) {
|
||||||
|
return "text-accent-amber-foreground";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status?.toLowerCase() === "terminating") {
|
||||||
|
return "red-500";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -741,7 +741,7 @@ export const formatName = (name) => {
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
const firstWord =
|
const firstWord =
|
||||||
formattedName.split(" ")[0].charAt(0).toUpperCase() +
|
formattedName.split(" ")[0].charAt(0) +
|
||||||
formattedName.split(" ")[0].slice(1);
|
formattedName.split(" ")[0].slice(1);
|
||||||
|
|
||||||
return { formattedName, firstWord };
|
return { formattedName, firstWord };
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,10 @@ const config = {
|
||||||
DEFAULT: "hsl(var(--accent))",
|
DEFAULT: "hsl(var(--accent))",
|
||||||
foreground: "hsl(var(--accent-foreground))",
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
},
|
},
|
||||||
|
"accent-amber": {
|
||||||
|
DEFAULT: "hsl(var(--accent-amber))",
|
||||||
|
foreground: "hsl(var(--accent-amber-foreground))",
|
||||||
|
},
|
||||||
"accent-emerald": {
|
"accent-emerald": {
|
||||||
DEFAULT: "hsl(var(--accent-emerald))",
|
DEFAULT: "hsl(var(--accent-emerald))",
|
||||||
foreground: "hsl(var(--accent-emerald-foreground))",
|
foreground: "hsl(var(--accent-emerald-foreground))",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue