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:
Deon Sanchez 2025-02-14 19:10:12 -07:00 • committed by GitHub
commit c902fb9e11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1314 additions and 538 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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%) */

View file

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

View file

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

View file

@ -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))",