feat: Quality of Life improvements for Astra Component (#6953)
* feat: Add helper text for Astra DB embedding generation provider * feat: Add helper text support for dropdown and parameter components * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * Update astradb.py * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * feat: Add org_id to excluded metadata keys in dropdown component * fix: Update Astra DB component UI and starter project configuration - Improved AstraDB component's embedding provider link with better accessibility attributes - Updated Vector Store RAG starter project JSON with formatted configuration - Refined UI text and link presentation for better user experience * Remove providers that arent configured * feat: Add required field indicator to node parameters - Implemented visual indicator (*) for required node parameters - Updated custom parameter title rendering to show required status - Enhanced UI to provide clearer input requirements * Revert "Merge branch 'lfoss-683-1' of https://github.com/langflow-ai/langflow into lfoss-683-1" This reverts commit 2eb0fcb4d3d347de0df1f021f761ea63e882b757, reversing changes made to 9ea44aca477e8a418ef82722da4b1e0fc2afa2e1. * [autofix.ci] apply automated fixes * Add changes back * Fix some comments * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * Update astradb.py * feat: Add dynamic disabled state for node dialog and int component * feat: Add dynamic placeholder for node dialog input * [autofix.ci] apply automated fixes * feat: Enhance IntComponent with disabled state styling and stepper interactions * Fix some db creation params * Update astradb.py * fix: Resolve node dialog input value handling in TypeScript * Update astradb.py * feat: Add required field validation to node dialog submission * Update astradb.py * [autofix.ci] apply automated fixes * Update astradb.py * Clean up functions and add read only flag * feat: Add readonly support to IntComponent and improve node dialog validation * fix: Improve dropdown component disabled and empty options handling * console.log removed * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * Read only field and cleanup * Astra DB selector updates * [autofix.ci] apply automated fixes * fix: Improve dropdown component disabled state handling * Fix value of region in create db * [autofix.ci] apply automated fixes * Remove database icon from component * fix: Conditionally render dropdown option icons * Don't show Nvidia if not available * [autofix.ci] apply automated fixes * Exceptions unless pending mean, skip * feat: Enhance AstraDBVectorStoreComponent with HTML sanitization - Added DOMPurify for sanitizing HTML input in the convertStringToHTML function. - Introduced a new sanitizeHTML function to ensure safe rendering of HTML strings. * fix: Correct field key usage in NodeDialog and refactor disabled class name in IntComponent - Updated NodeDialogComponent to use fieldKey instead of underscore for better clarity in mapping field values. - Refactored IntComponent to replace the getDisabledClassName function with a constant for the disabled input class, improving readability. * ✨ (playground.spec.ts): Refactor drag and drop functionality to use hover and click for better interaction 🔧 (chatInputOutput.spec.ts): Update drag and drop functionality to use target position for more precise placement 🔧 (chatInputOutputUser-shard-1.spec.ts): Simplify connecting elements by replacing hover and mouse actions with click for better user experience * 🐛 (generalBugs-prompt.spec.ts): fix issue with selecting the last element with getByTestId to ensure correct element is clicked and tested * ✅ (chatInputOutputUser-shard-1.spec.ts): add 600ms timeout to wait for elements to be interactable before proceeding with the test steps --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Hare <ericrhare@gmail.com> Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
This commit is contained in:
parent
b6333d0e49
commit
4ab4e736be
15 changed files with 1443 additions and 2146 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
import re
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
|
|
||||||
|
|
@ -97,6 +98,11 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
name="embedding_generation_provider",
|
name="embedding_generation_provider",
|
||||||
display_name="Embedding generation method",
|
display_name="Embedding generation method",
|
||||||
info="Provider to use for generating embeddings.",
|
info="Provider to use for generating embeddings.",
|
||||||
|
helper_text=(
|
||||||
|
"To create collections with more embedding provider options, go to "
|
||||||
|
'<a class="underline" href="https://astra.datastax.com/" target=" _blank" '
|
||||||
|
'rel="noopener noreferrer">your database in Astra DB</a>'
|
||||||
|
),
|
||||||
real_time_refresh=True,
|
real_time_refresh=True,
|
||||||
required=True,
|
required=True,
|
||||||
options=[],
|
options=[],
|
||||||
|
|
@ -105,15 +111,14 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
name="embedding_generation_model",
|
name="embedding_generation_model",
|
||||||
display_name="Embedding model",
|
display_name="Embedding model",
|
||||||
info="Model to use for generating embeddings.",
|
info="Model to use for generating embeddings.",
|
||||||
required=True,
|
real_time_refresh=True,
|
||||||
options=[],
|
options=[],
|
||||||
),
|
),
|
||||||
"04_dimension": IntInput(
|
"04_dimension": IntInput(
|
||||||
name="dimension",
|
name="dimension",
|
||||||
display_name="Dimensions (Required only for `Bring your own`)",
|
display_name="Dimensions",
|
||||||
info="Dimensions of the embeddings to generate.",
|
info="Dimensions of the embeddings to generate.",
|
||||||
required=False,
|
value=None,
|
||||||
value=1024,
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -307,9 +312,8 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
|
|
||||||
# Sort the resulting dictionary
|
# Sort the resulting dictionary
|
||||||
return defaultdict(list, dict(sorted(vectorize_providers_mapping.items())))
|
return defaultdict(list, dict(sorted(vectorize_providers_mapping.items())))
|
||||||
except Exception as e:
|
except Exception as _: # noqa: BLE001
|
||||||
msg = f"Error fetching vectorize providers: {e}"
|
return {}
|
||||||
raise ValueError(msg) from e
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create_database_api(
|
async def create_database_api(
|
||||||
|
|
@ -329,6 +333,11 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
# Get the environment, set to prod if null like
|
# Get the environment, set to prod if null like
|
||||||
my_env = environment or "prod"
|
my_env = environment or "prod"
|
||||||
|
|
||||||
|
# Raise a value error if name isn't provided
|
||||||
|
if not new_database_name:
|
||||||
|
msg = "Database name is required to create a new database."
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
# Call the create database function
|
# Call the create database function
|
||||||
return await admin_client.async_create_database(
|
return await admin_client.async_create_database(
|
||||||
name=new_database_name,
|
name=new_database_name,
|
||||||
|
|
@ -366,6 +375,11 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
model_name=embedding_generation_model,
|
model_name=embedding_generation_model,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Raise a value error if name isn't provided
|
||||||
|
if not new_collection_name:
|
||||||
|
msg = "Collection name is required to create a new collection."
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
# Create the collection
|
# Create the collection
|
||||||
return await database.create_collection(
|
return await database.create_collection(
|
||||||
name=new_collection_name,
|
name=new_collection_name,
|
||||||
|
|
@ -406,15 +420,16 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
num_collections = 0
|
|
||||||
if db.status != "PENDING":
|
if db.status != "PENDING":
|
||||||
continue
|
continue
|
||||||
|
num_collections = 0
|
||||||
|
|
||||||
# Add the database to the dictionary
|
# Add the database to the dictionary
|
||||||
db_info_dict[db.info.name] = {
|
db_info_dict[db.info.name] = {
|
||||||
"api_endpoint": api_endpoint,
|
"api_endpoint": api_endpoint,
|
||||||
"collections": num_collections,
|
"collections": num_collections,
|
||||||
"status": db.status if db.status != "ACTIVE" else None,
|
"status": db.status if db.status != "ACTIVE" else None,
|
||||||
|
"org_id": db.org_id if db.org_id else None,
|
||||||
}
|
}
|
||||||
except Exception: # noqa: BLE001, S110
|
except Exception: # noqa: BLE001, S110
|
||||||
pass
|
pass
|
||||||
|
|
@ -460,6 +475,17 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
database_name=self.database_name,
|
database_name=self.database_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_database_id_static(cls, api_endpoint: str) -> str | None:
|
||||||
|
# Pattern matches standard UUID format: 8-4-4-4-12 hexadecimal characters
|
||||||
|
uuid_pattern = r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
|
||||||
|
match = re.search(uuid_pattern, api_endpoint)
|
||||||
|
|
||||||
|
return match.group(0) if match else None
|
||||||
|
|
||||||
|
def get_database_id(self):
|
||||||
|
return self.get_database_id_static(api_endpoint=self.get_api_endpoint())
|
||||||
|
|
||||||
def get_keyspace(self):
|
def get_keyspace(self):
|
||||||
keyspace = self.keyspace
|
keyspace = self.keyspace
|
||||||
|
|
||||||
|
|
@ -508,7 +534,7 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
"status": info["status"],
|
"status": info["status"],
|
||||||
"collections": info["collections"],
|
"collections": info["collections"],
|
||||||
"api_endpoint": info["api_endpoint"],
|
"api_endpoint": info["api_endpoint"],
|
||||||
"icon": "data",
|
"org_id": info["org_id"],
|
||||||
}
|
}
|
||||||
for name, info in self.get_database_list().items()
|
for name, info in self.get_database_list().items()
|
||||||
]
|
]
|
||||||
|
|
@ -573,295 +599,327 @@ class AstraDBVectorStoreComponent(LCVectorStoreComponent):
|
||||||
for col in collection_list
|
for col in collection_list
|
||||||
]
|
]
|
||||||
|
|
||||||
def reset_provider_options(self, build_config: dict):
|
def reset_provider_options(self, build_config: dict) -> dict:
|
||||||
# Get the list of vectorize providers
|
"""Reset provider options and related configurations in the build_config dictionary."""
|
||||||
vectorize_providers = self.get_vectorize_providers(
|
# Extract template path for cleaner access
|
||||||
|
template = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]
|
||||||
|
|
||||||
|
# Get vectorize providers
|
||||||
|
vectorize_providers_api = self.get_vectorize_providers(
|
||||||
token=self.token,
|
token=self.token,
|
||||||
environment=self.environment,
|
environment=self.environment,
|
||||||
api_endpoint=build_config["api_endpoint"]["value"],
|
api_endpoint=build_config["api_endpoint"]["value"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Append a special case for Bring your own
|
# Create a new dictionary with "Bring your own" first
|
||||||
vectorize_providers["Bring your own"] = [None, ["Bring your own"]]
|
vectorize_providers: dict[str, list[list[str]]] = {"Bring your own": [[], []]}
|
||||||
|
|
||||||
# If the collection is set, allow user to see embedding options
|
# Add the remaining items (only Nvidia) from the original dictionary
|
||||||
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
vectorize_providers.update(
|
||||||
"02_embedding_generation_provider"
|
{
|
||||||
]["options"] = [
|
k: v
|
||||||
"Bring your own",
|
for k, v in vectorize_providers_api.items()
|
||||||
"Nvidia",
|
if k.lower() in ["nvidia"] # TODO: Eventually support more
|
||||||
*[key for key in vectorize_providers if key not in ["Bring your own", "Nvidia"]],
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set provider options
|
||||||
|
provider_field = "02_embedding_generation_provider"
|
||||||
|
template[provider_field]["options"] = list(vectorize_providers.keys())
|
||||||
|
|
||||||
|
# Add metadata for each provider option
|
||||||
|
template[provider_field]["options_metadata"] = [
|
||||||
|
{"icon": self.get_provider_icon(provider_name=provider)} for provider in template[provider_field]["options"]
|
||||||
]
|
]
|
||||||
|
|
||||||
# For all not Bring your own or Nvidia providers, add metadata saying configure in Astra DB Portal
|
# Get selected embedding provider
|
||||||
provider_options = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
embedding_provider = template[provider_field]["value"]
|
||||||
"02_embedding_generation_provider"
|
is_bring_your_own = embedding_provider and embedding_provider == "Bring your own"
|
||||||
]["options"]
|
|
||||||
|
|
||||||
# Go over each possible provider and add metadata to configure in Astra DB Portal
|
# Configure embedding model field
|
||||||
for provider in provider_options:
|
model_field = "03_embedding_generation_model"
|
||||||
# Add the icon for the provider
|
template[model_field].update(
|
||||||
my_metadata = {"icon": self.get_provider_icon(provider_name=provider)}
|
{
|
||||||
|
"options": vectorize_providers.get(embedding_provider, [[], []])[1],
|
||||||
|
"placeholder": "Bring your own" if is_bring_your_own else None,
|
||||||
|
"readonly": is_bring_your_own,
|
||||||
|
"required": not is_bring_your_own,
|
||||||
|
"value": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Skip Bring your own and Nvidia, automatically configured
|
# If this is a bring your own, set dimensions to 0
|
||||||
if provider not in {"Bring your own", "Nvidia"}:
|
return self.reset_dimension_field(build_config)
|
||||||
# Add metadata to configure in Astra DB Portal
|
|
||||||
my_metadata[" "] = "Configure in Astra DB Portal"
|
|
||||||
|
|
||||||
# Add the metadata to the options metadata
|
def reset_dimension_field(self, build_config: dict) -> dict:
|
||||||
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
"""Reset dimension field options based on provided configuration."""
|
||||||
"02_embedding_generation_provider"
|
# Extract template path for cleaner access
|
||||||
]["options_metadata"].append(my_metadata)
|
template = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]
|
||||||
|
|
||||||
# And allow the user to see the models based on a selected provider
|
# Get selected embedding model
|
||||||
embedding_provider = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
provider_field = "02_embedding_generation_provider"
|
||||||
"02_embedding_generation_provider"
|
embedding_provider = template[provider_field]["value"]
|
||||||
]["value"]
|
is_bring_your_own = embedding_provider and embedding_provider == "Bring your own"
|
||||||
|
|
||||||
# Set the options for the embedding model based on the provider
|
# Configure dimension field
|
||||||
build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"][
|
dimension_field = "04_dimension"
|
||||||
"03_embedding_generation_model"
|
dimension_value = 1024 if not is_bring_your_own else None # TODO: Dynamically figure this out
|
||||||
]["options"] = vectorize_providers.get(embedding_provider, [[], []])[1]
|
template[dimension_field].update(
|
||||||
|
{
|
||||||
|
"placeholder": dimension_value,
|
||||||
|
"value": dimension_value,
|
||||||
|
"readonly": not is_bring_your_own,
|
||||||
|
"required": is_bring_your_own,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return build_config
|
return build_config
|
||||||
|
|
||||||
def reset_collection_list(self, build_config: dict):
|
def reset_collection_list(self, build_config: dict) -> dict:
|
||||||
# Get the list of options we have based on the token provided
|
"""Reset collection list options based on provided configuration."""
|
||||||
|
# Get collection options
|
||||||
collection_options = self._initialize_collection_options(api_endpoint=build_config["api_endpoint"]["value"])
|
collection_options = self._initialize_collection_options(api_endpoint=build_config["api_endpoint"]["value"])
|
||||||
|
|
||||||
# If we retrieved options based on the token, show the dropdown
|
# Update collection configuration
|
||||||
build_config["collection_name"]["options"] = [col["name"] for col in collection_options]
|
collection_config = build_config["collection_name"]
|
||||||
build_config["collection_name"]["options_metadata"] = [
|
collection_config.update(
|
||||||
{k: v for k, v in col.items() if k != "name"} for col in collection_options
|
{
|
||||||
]
|
"options": [col["name"] for col in collection_options],
|
||||||
|
"options_metadata": [{k: v for k, v in col.items() if k != "name"} for col in collection_options],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Reset the selected collection
|
# Reset selected collection if not in options
|
||||||
if build_config["collection_name"]["value"] not in build_config["collection_name"]["options"]:
|
if collection_config["value"] not in collection_config["options"]:
|
||||||
build_config["collection_name"]["value"] = ""
|
collection_config["value"] = ""
|
||||||
|
|
||||||
# If we have a database, collection name should not be advanced
|
# Set advanced status based on database selection
|
||||||
build_config["collection_name"]["advanced"] = not build_config["database_name"]["value"]
|
collection_config["advanced"] = not build_config["database_name"]["value"]
|
||||||
|
|
||||||
return build_config
|
return build_config
|
||||||
|
|
||||||
def reset_database_list(self, build_config: dict):
|
def reset_database_list(self, build_config: dict) -> dict:
|
||||||
# Get the list of options we have based on the token provided
|
"""Reset database list options and related configurations."""
|
||||||
|
# Get database options
|
||||||
database_options = self._initialize_database_options()
|
database_options = self._initialize_database_options()
|
||||||
|
|
||||||
# Update the list of cloud providers
|
# Update cloud provider options
|
||||||
my_env = self.environment or "prod"
|
env = self.environment or "prod"
|
||||||
build_config["database_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]["02_cloud_provider"][
|
template = build_config["database_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]
|
||||||
"options"
|
template["02_cloud_provider"]["options"] = list(self.map_cloud_providers()[env].keys())
|
||||||
] = list(self.map_cloud_providers()[my_env].keys())
|
|
||||||
|
|
||||||
# If we retrieved options based on the token, show the dropdown
|
# Update database configuration
|
||||||
build_config["database_name"]["options"] = [db["name"] for db in database_options]
|
database_config = build_config["database_name"]
|
||||||
build_config["database_name"]["options_metadata"] = [
|
database_config.update(
|
||||||
{k: v for k, v in db.items() if k != "name"} for db in database_options
|
{
|
||||||
]
|
"options": [db["name"] for db in database_options],
|
||||||
|
"options_metadata": [{k: v for k, v in db.items() if k != "name"} for db in database_options],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Reset the selected database
|
# Reset selections if value not in options
|
||||||
if build_config["database_name"]["value"] not in build_config["database_name"]["options"]:
|
if database_config["value"] not in database_config["options"]:
|
||||||
build_config["database_name"]["value"] = ""
|
database_config["value"] = ""
|
||||||
build_config["api_endpoint"]["value"] = ""
|
build_config["api_endpoint"]["value"] = ""
|
||||||
build_config["collection_name"]["advanced"] = True
|
build_config["collection_name"]["advanced"] = True
|
||||||
|
|
||||||
# If we have a token, database name should not be advanced
|
# Set advanced status based on token presence
|
||||||
build_config["database_name"]["advanced"] = not build_config["token"]["value"]
|
database_config["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) -> dict:
|
||||||
# Reset the list of databases we have based on the token provided
|
"""Reset all build configuration options to default empty state."""
|
||||||
build_config["database_name"]["options"] = []
|
# Reset database configuration
|
||||||
build_config["database_name"]["options_metadata"] = []
|
database_config = build_config["database_name"]
|
||||||
build_config["database_name"]["value"] = ""
|
database_config.update({"options": [], "options_metadata": [], "value": "", "advanced": True})
|
||||||
build_config["database_name"]["advanced"] = True
|
|
||||||
build_config["api_endpoint"]["value"] = ""
|
build_config["api_endpoint"]["value"] = ""
|
||||||
|
|
||||||
# Reset the list of collections and metadata associated
|
# Reset collection configuration
|
||||||
build_config["collection_name"]["options"] = []
|
collection_config = build_config["collection_name"]
|
||||||
build_config["collection_name"]["options_metadata"] = []
|
collection_config.update({"options": [], "options_metadata": [], "value": "", "advanced": True})
|
||||||
build_config["collection_name"]["value"] = ""
|
|
||||||
build_config["collection_name"]["advanced"] = True
|
|
||||||
|
|
||||||
return build_config
|
return build_config
|
||||||
|
|
||||||
async 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) -> dict:
|
||||||
# Callback for database creation
|
"""Update build configuration based on field name and value."""
|
||||||
if field_name == "database_name" and isinstance(field_value, dict) and "01_new_database_name" in field_value:
|
# Early return if no token provided
|
||||||
try:
|
|
||||||
await self.create_database_api(
|
|
||||||
new_database_name=field_value["01_new_database_name"],
|
|
||||||
token=self.token,
|
|
||||||
keyspace=self.get_keyspace(),
|
|
||||||
environment=self.environment,
|
|
||||||
cloud_provider=field_value["02_cloud_provider"],
|
|
||||||
region=field_value["03_region"],
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
msg = f"Error creating database: {e}"
|
|
||||||
raise ValueError(msg) from e
|
|
||||||
|
|
||||||
# Add the new database to the list of options
|
|
||||||
build_config["database_name"]["options"] += [field_value["01_new_database_name"]]
|
|
||||||
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 "01_new_database_name" not in field_value
|
|
||||||
):
|
|
||||||
# Get the cloud provider and environment
|
|
||||||
cloud_provider = field_value["02_cloud_provider"]
|
|
||||||
my_env = self.environment or "prod"
|
|
||||||
|
|
||||||
# Update the list of regions for the cloud provider
|
|
||||||
build_config["database_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]["03_region"][
|
|
||||||
"options"
|
|
||||||
] = self.map_cloud_providers()[my_env][cloud_provider]["regions"]
|
|
||||||
|
|
||||||
return build_config
|
|
||||||
|
|
||||||
# Callback for the creation of collections
|
|
||||||
if (
|
|
||||||
field_name == "collection_name"
|
|
||||||
and isinstance(field_value, dict)
|
|
||||||
and "01_new_collection_name" in field_value
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
# Get the dimension if its a BYO provider
|
|
||||||
dimension = (
|
|
||||||
field_value["04_dimension"]
|
|
||||||
if field_value["02_embedding_generation_provider"] == "Bring your own"
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the collection
|
|
||||||
await self.create_collection_api(
|
|
||||||
new_collection_name=field_value["01_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["02_embedding_generation_provider"],
|
|
||||||
embedding_generation_model=field_value["03_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["01_new_collection_name"]
|
|
||||||
build_config["collection_name"]["options"].append(field_value["01_new_collection_name"])
|
|
||||||
|
|
||||||
# Get the provider and model for the new collection
|
|
||||||
generation_provider = field_value["02_embedding_generation_provider"]
|
|
||||||
provider = (
|
|
||||||
generation_provider.lower() if generation_provider and generation_provider != "Bring your own" else None
|
|
||||||
)
|
|
||||||
generation_model = field_value["03_embedding_generation_model"]
|
|
||||||
model = generation_model if generation_model and generation_model != "Bring your own" else None
|
|
||||||
|
|
||||||
# Set the embedding choice
|
|
||||||
build_config["embedding_choice"]["value"] = "Astra Vectorize" if provider else "Embedding Model"
|
|
||||||
build_config["embedding_model"]["advanced"] = bool(provider)
|
|
||||||
|
|
||||||
# Add the new collection to the list of options
|
|
||||||
icon = self.get_provider_icon(provider_name=generation_provider)
|
|
||||||
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 "01_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
|
# Database creation callback
|
||||||
|
if field_name == "database_name" and isinstance(field_value, dict):
|
||||||
|
if "01_new_database_name" in field_value:
|
||||||
|
await self._create_new_database(build_config, field_value)
|
||||||
|
return self.reset_collection_list(build_config)
|
||||||
|
return self._update_cloud_regions(build_config, field_value)
|
||||||
|
|
||||||
|
# Collection creation callback
|
||||||
|
if field_name == "collection_name" and isinstance(field_value, dict):
|
||||||
|
# Case 1: New collection creation
|
||||||
|
if "01_new_collection_name" in field_value:
|
||||||
|
await self._create_new_collection(build_config, field_value)
|
||||||
|
return build_config
|
||||||
|
|
||||||
|
# Case 2: Update embedding provider options
|
||||||
|
if "02_embedding_generation_provider" in field_value:
|
||||||
|
return self.reset_provider_options(build_config)
|
||||||
|
|
||||||
|
# Case 3: Update dimension field
|
||||||
|
if "03_embedding_generation_model" in field_value:
|
||||||
|
return self.reset_dimension_field(build_config)
|
||||||
|
|
||||||
|
# Initial execution or token/environment change
|
||||||
|
first_run = field_name == "collection_name" and not field_value and not build_config["database_name"]["options"]
|
||||||
if first_run or field_name in {"token", "environment"}:
|
if first_run or field_name in {"token", "environment"}:
|
||||||
return self.reset_database_list(build_config)
|
return self.reset_database_list(build_config)
|
||||||
|
|
||||||
# Refresh the collection name options
|
# Database selection change
|
||||||
if field_name == "database_name" and not isinstance(field_value, dict):
|
if field_name == "database_name" and not isinstance(field_value, dict):
|
||||||
# If missing, refresh the database options
|
return self._handle_database_selection(build_config, field_value)
|
||||||
if field_value not in build_config["database_name"]["options"]:
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Initializing database condition
|
# Collection selection change
|
||||||
pending = build_config["database_name"]["options_metadata"][index_of_name]["status"] == "PENDING"
|
|
||||||
if pending:
|
|
||||||
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
|
|
||||||
]["api_endpoint"]
|
|
||||||
|
|
||||||
# Reset the provider options
|
|
||||||
build_config = self.reset_provider_options(build_config)
|
|
||||||
|
|
||||||
# Reset the list of collections we have based on the token provided
|
|
||||||
return self.reset_collection_list(build_config)
|
|
||||||
|
|
||||||
# Hide embedding model option if opriona_metadata provider is not null
|
|
||||||
if field_name == "collection_name" and not isinstance(field_value, dict):
|
if field_name == "collection_name" and not isinstance(field_value, dict):
|
||||||
# Assume we will be autodetecting the collection:
|
return self._handle_collection_selection(build_config, field_value)
|
||||||
build_config["autodetect_collection"]["value"] = True
|
|
||||||
|
|
||||||
# Reload the collection list
|
return build_config
|
||||||
build_config = self.reset_collection_list(build_config)
|
|
||||||
|
|
||||||
# Set the options for collection name to be the field value if its a new collection
|
async def _create_new_database(self, build_config: dict, field_value: dict) -> None:
|
||||||
if field_value and field_value not in build_config["collection_name"]["options"]:
|
"""Create a new database and update build config options."""
|
||||||
# Add the new collection to the list of options
|
try:
|
||||||
build_config["collection_name"]["options"].append(field_value)
|
await self.create_database_api(
|
||||||
build_config["collection_name"]["options_metadata"].append(
|
new_database_name=field_value["01_new_database_name"],
|
||||||
{
|
token=self.token,
|
||||||
"records": 0,
|
keyspace=self.get_keyspace(),
|
||||||
"provider": None,
|
environment=self.environment,
|
||||||
"icon": "vectorstores",
|
cloud_provider=field_value["02_cloud_provider"],
|
||||||
"model": None,
|
region=field_value["03_region"],
|
||||||
}
|
)
|
||||||
)
|
except Exception as e:
|
||||||
|
msg = f"Error creating database: {e}"
|
||||||
|
raise ValueError(msg) from e
|
||||||
|
|
||||||
# Ensure that autodetect collection is set to False, since its a new collection
|
build_config["database_name"]["options"].append(field_value["01_new_database_name"])
|
||||||
build_config["autodetect_collection"]["value"] = False
|
build_config["database_name"]["options_metadata"].append(
|
||||||
|
{
|
||||||
|
"status": "PENDING",
|
||||||
|
"collections": 0,
|
||||||
|
"api_endpoint": None,
|
||||||
|
"org_id": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# If nothing is selected, can't detect provider - return
|
def _update_cloud_regions(self, build_config: dict, field_value: dict) -> dict:
|
||||||
if not field_value:
|
"""Update cloud provider regions in build config."""
|
||||||
return build_config
|
env = self.environment or "prod"
|
||||||
|
cloud_provider = field_value["02_cloud_provider"]
|
||||||
|
|
||||||
# Find the position of the selected collection to align with metadata
|
# Update the region options based on the selected cloud provider
|
||||||
index_of_name = build_config["collection_name"]["options"].index(field_value)
|
template = build_config["database_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]
|
||||||
value_of_provider = build_config["collection_name"]["options_metadata"][index_of_name]["provider"]
|
template["03_region"]["options"] = self.map_cloud_providers()[env][cloud_provider]["regions"]
|
||||||
|
|
||||||
# If we were able to determine the Vectorize provider, set it accordingly
|
# Reset the the 03_region value if it's not in the new options
|
||||||
build_config["embedding_model"]["advanced"] = bool(value_of_provider)
|
if template["03_region"]["value"] not in template["03_region"]["options"]:
|
||||||
build_config["embedding_choice"]["value"] = "Astra Vectorize" if value_of_provider else "Embedding Model"
|
template["03_region"]["value"] = None
|
||||||
|
|
||||||
|
return build_config
|
||||||
|
|
||||||
|
async def _create_new_collection(self, build_config: dict, field_value: dict) -> None:
|
||||||
|
"""Create a new collection and update build config options."""
|
||||||
|
embedding_provider = field_value.get("02_embedding_generation_provider")
|
||||||
|
try:
|
||||||
|
await self.create_collection_api(
|
||||||
|
new_collection_name=field_value["01_new_collection_name"],
|
||||||
|
token=self.token,
|
||||||
|
api_endpoint=build_config["api_endpoint"]["value"],
|
||||||
|
environment=self.environment,
|
||||||
|
keyspace=self.get_keyspace(),
|
||||||
|
dimension=field_value.get("04_dimension") if embedding_provider == "Bring your own" else None,
|
||||||
|
embedding_generation_provider=embedding_provider,
|
||||||
|
embedding_generation_model=field_value.get("03_embedding_generation_model"),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"Error creating collection: {e}"
|
||||||
|
raise ValueError(msg) from e
|
||||||
|
|
||||||
|
provider = embedding_provider.lower() if embedding_provider and embedding_provider != "Bring your own" else None
|
||||||
|
build_config["collection_name"].update(
|
||||||
|
{
|
||||||
|
"value": field_value["01_new_collection_name"],
|
||||||
|
"options": build_config["collection_name"]["options"] + [field_value["01_new_collection_name"]],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
build_config["embedding_choice"]["value"] = "Astra Vectorize" if provider else "Embedding Model"
|
||||||
|
build_config["embedding_model"]["advanced"] = bool(provider)
|
||||||
|
build_config["collection_name"]["options_metadata"].append(
|
||||||
|
{
|
||||||
|
"records": 0,
|
||||||
|
"provider": provider,
|
||||||
|
"icon": self.get_provider_icon(provider_name=embedding_provider),
|
||||||
|
"model": field_value.get("03_embedding_generation_model"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_database_selection(self, build_config: dict, field_value: str) -> dict:
|
||||||
|
"""Handle database selection and update related configurations."""
|
||||||
|
build_config = self.reset_database_list(build_config)
|
||||||
|
|
||||||
|
# Reset collection list if database selection changes
|
||||||
|
if field_value not in build_config["database_name"]["options"]:
|
||||||
|
build_config["database_name"]["value"] = ""
|
||||||
return build_config
|
return build_config
|
||||||
|
|
||||||
|
# Get the api endpoint for the selected database
|
||||||
|
index = build_config["database_name"]["options"].index(field_value)
|
||||||
|
build_config["api_endpoint"]["value"] = build_config["database_name"]["options_metadata"][index]["api_endpoint"]
|
||||||
|
|
||||||
|
# Get the org_id for the selected database
|
||||||
|
org_id = build_config["database_name"]["options_metadata"][index]["org_id"]
|
||||||
|
if not org_id:
|
||||||
|
return build_config
|
||||||
|
|
||||||
|
# Get the database id for the selected database
|
||||||
|
db_id = self.get_database_id_static(api_endpoint=build_config["api_endpoint"]["value"])
|
||||||
|
keyspace = self.get_keyspace() or "default_keyspace"
|
||||||
|
|
||||||
|
# Update the helper text for the embedding provider field
|
||||||
|
template = build_config["collection_name"]["dialog_inputs"]["fields"]["data"]["node"]["template"]
|
||||||
|
template["02_embedding_generation_provider"]["helper_text"] = (
|
||||||
|
"To create collections with more embedding provider options, go to "
|
||||||
|
f'<a class="underline" target="_blank" rel="noopener noreferrer" '
|
||||||
|
f'href="https://astra.datastax.com/org/{org_id}/database/{db_id}/data-explorer?createCollection=1&namespace={keyspace}">'
|
||||||
|
"your database in Astra DB</a>."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reset provider options
|
||||||
|
build_config = self.reset_provider_options(build_config)
|
||||||
|
|
||||||
|
return self.reset_collection_list(build_config)
|
||||||
|
|
||||||
|
def _handle_collection_selection(self, build_config: dict, field_value: str) -> dict:
|
||||||
|
"""Handle collection selection and update embedding options."""
|
||||||
|
build_config["autodetect_collection"]["value"] = True
|
||||||
|
build_config = self.reset_collection_list(build_config)
|
||||||
|
|
||||||
|
if field_value and field_value not in build_config["collection_name"]["options"]:
|
||||||
|
build_config["collection_name"]["options"].append(field_value)
|
||||||
|
build_config["collection_name"]["options_metadata"].append(
|
||||||
|
{
|
||||||
|
"records": 0,
|
||||||
|
"provider": None,
|
||||||
|
"icon": "vectorstores",
|
||||||
|
"model": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
build_config["autodetect_collection"]["value"] = False
|
||||||
|
|
||||||
|
if not field_value:
|
||||||
|
return build_config
|
||||||
|
|
||||||
|
index = build_config["collection_name"]["options"].index(field_value)
|
||||||
|
provider = build_config["collection_name"]["options_metadata"][index]["provider"]
|
||||||
|
build_config["embedding_model"]["advanced"] = bool(provider)
|
||||||
|
build_config["embedding_choice"]["value"] = "Astra Vectorize" if provider else "Embedding Model"
|
||||||
return build_config
|
return build_config
|
||||||
|
|
||||||
@check_cached_vector_store
|
@check_cached_vector_store
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -72,6 +72,9 @@ class BaseInputMixin(BaseModel, validate_assignment=True): # type: ignore[call-
|
||||||
dynamic: bool = False
|
dynamic: bool = False
|
||||||
"""Specifies if the field is dynamic. Defaults to False."""
|
"""Specifies if the field is dynamic. Defaults to False."""
|
||||||
|
|
||||||
|
helper_text: str | None = None
|
||||||
|
"""Adds a helper text to the field. Defaults to an empty string."""
|
||||||
|
|
||||||
info: str | None = ""
|
info: str | None = ""
|
||||||
"""Additional information about the field to be shown in the tooltip. Defaults to an empty string."""
|
"""Additional information about the field to be shown in the tooltip. Defaults to an empty string."""
|
||||||
|
|
||||||
|
|
|
||||||
2731
src/frontend/package-lock.json
generated
2731
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -110,6 +110,28 @@ export const NodeDialog: React.FC<NodeDialogProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitDialog = async () => {
|
const handleSubmitDialog = async () => {
|
||||||
|
// Validate required fields first
|
||||||
|
const missingRequiredFields = Object.entries(dialogTemplate)
|
||||||
|
.filter(
|
||||||
|
([key, fieldValue]) =>
|
||||||
|
(fieldValue as { required: boolean })?.required === true &&
|
||||||
|
(!fieldValues[key] ||
|
||||||
|
(typeof fieldValues[key] === "string" &&
|
||||||
|
fieldValues[key].trim() === "")),
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
([fieldKey, fieldValue]) =>
|
||||||
|
(fieldValue as { display_name: string })?.display_name || fieldKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (missingRequiredFields.length > 0) {
|
||||||
|
handleErrorData({
|
||||||
|
title: "Missing required fields",
|
||||||
|
list: missingRequiredFields,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
await mutateTemplate(
|
await mutateTemplate(
|
||||||
|
|
@ -162,13 +184,15 @@ export const NodeDialog: React.FC<NodeDialogProps> = ({
|
||||||
<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(dialogTemplate).map(([fieldKey, fieldValue]) => (
|
{Object.entries(dialogTemplate).map(([fieldKey, fieldValue]) => (
|
||||||
<div key={fieldKey}>
|
<div key={fieldKey}>
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
{getCustomParameterTitle({
|
{getCustomParameterTitle({
|
||||||
title:
|
title:
|
||||||
(fieldValue as { display_name: string })?.display_name ??
|
(fieldValue as { display_name: string })?.display_name ??
|
||||||
"",
|
"",
|
||||||
nodeId,
|
nodeId,
|
||||||
isFlexView: false,
|
isFlexView: false,
|
||||||
|
required:
|
||||||
|
(fieldValue as { required: boolean })?.required ?? false,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<ParameterRenderComponent
|
<ParameterRenderComponent
|
||||||
|
|
@ -178,12 +202,16 @@ export const NodeDialog: React.FC<NodeDialogProps> = ({
|
||||||
name={fieldKey}
|
name={fieldKey}
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
templateData={fieldValue as Partial<InputFieldType>}
|
templateData={fieldValue as Partial<InputFieldType>}
|
||||||
templateValue={fieldValues[fieldKey] || ""}
|
templateValue={(fieldValue as { value: string })?.value ?? ""}
|
||||||
editNode={false}
|
editNode={false}
|
||||||
handleNodeClass={() => {}}
|
handleNodeClass={() => {}}
|
||||||
nodeClass={dialogNodeData}
|
nodeClass={dialogNodeData}
|
||||||
disabled={false}
|
disabled={
|
||||||
placeholder=""
|
(fieldValue as { disabled: boolean })?.disabled ?? false
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
(fieldValue as { placeholder: string })?.placeholder ?? ""
|
||||||
|
}
|
||||||
isToolMode={false}
|
isToolMode={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-t
|
||||||
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 {
|
||||||
|
convertStringToHTML,
|
||||||
|
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";
|
||||||
|
|
@ -66,7 +69,8 @@ export default function Dropdown({
|
||||||
const fuse = new Fuse(validOptions, { keys: ["name", "value"] });
|
const fuse = new Fuse(validOptions, { keys: ["name", "value"] });
|
||||||
const PopoverContentDropdown =
|
const PopoverContentDropdown =
|
||||||
children || editNode ? PopoverContent : PopoverContentWithoutPortal;
|
children || editNode ? PopoverContent : PopoverContentWithoutPortal;
|
||||||
const { nodeClass, nodeId, handleNodeClass, tooltip } = baseInputProps;
|
const { nodeClass, nodeId, handleNodeClass, tooltip, helperText } =
|
||||||
|
baseInputProps;
|
||||||
|
|
||||||
// API and store hooks
|
// API and store hooks
|
||||||
const postTemplateValue = usePostTemplateValue({
|
const postTemplateValue = usePostTemplateValue({
|
||||||
|
|
@ -79,7 +83,7 @@ export default function Dropdown({
|
||||||
// Utility functions
|
// Utility functions
|
||||||
const filterMetadataKeys = (
|
const filterMetadataKeys = (
|
||||||
metadata: Record<string, any> = {},
|
metadata: Record<string, any> = {},
|
||||||
excludeKeys: string[] = ["api_endpoint", "icon", "status"],
|
excludeKeys: string[] = ["api_endpoint", "icon", "status", "org_id"],
|
||||||
) => {
|
) => {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(metadata).filter(([key]) => !excludeKeys.includes(key)),
|
Object.entries(metadata).filter(([key]) => !excludeKeys.includes(key)),
|
||||||
|
|
@ -161,7 +165,7 @@ export default function Dropdown({
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderTriggerButton = () => (
|
const renderTriggerButton = () => (
|
||||||
<ShadTooltip content={!value ? (tooltip as string) : ""}>
|
<div className="flex w-full flex-col">
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
|
|
@ -199,7 +203,9 @@ export default function Dropdown({
|
||||||
className="h-4 w-4"
|
className="h-4 w-4"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{value && filteredOptions.includes(value) ? value : placeholderName}{" "}
|
{value && filteredOptions.includes(value)
|
||||||
|
? value
|
||||||
|
: placeholderName}{" "}
|
||||||
</span>
|
</span>
|
||||||
<ForwardedIconComponent
|
<ForwardedIconComponent
|
||||||
name="ChevronsUpDown"
|
name="ChevronsUpDown"
|
||||||
|
|
@ -212,7 +218,12 @@ export default function Dropdown({
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
</ShadTooltip>
|
{helperText && (
|
||||||
|
<span className="pt-2 text-xs text-muted-foreground">
|
||||||
|
{convertStringToHTML(helperText)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderSearchInput = () => (
|
const renderSearchInput = () => (
|
||||||
|
|
@ -302,7 +313,7 @@ export default function Dropdown({
|
||||||
data-testid={`${option}-${index}-option`}
|
data-testid={`${option}-${index}-option`}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center gap-2">
|
<div className="flex w-full items-center gap-2">
|
||||||
{optionsMetaData && optionsMetaData.length > 0 && (
|
{optionsMetaData?.[index]?.icon && (
|
||||||
<ForwardedIconComponent
|
<ForwardedIconComponent
|
||||||
name={optionsMetaData?.[index]?.icon || "Unknown"}
|
name={optionsMetaData?.[index]?.icon || "Unknown"}
|
||||||
className="h-4 w-4 shrink-0 text-primary"
|
className="h-4 w-4 shrink-0 text-primary"
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ export default function IntComponent({
|
||||||
disabled,
|
disabled,
|
||||||
editNode = false,
|
editNode = false,
|
||||||
id = "",
|
id = "",
|
||||||
|
readonly,
|
||||||
}: InputProps<number, IntComponentType>): JSX.Element {
|
}: InputProps<number, IntComponentType>): JSX.Element {
|
||||||
const min = -Infinity;
|
const min = -Infinity;
|
||||||
// Clear component state
|
// Clear component state
|
||||||
|
|
@ -59,6 +60,9 @@ export default function IntComponent({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DISABLED_INPUT_CLASS =
|
||||||
|
"cursor-default bg-secondary border-border border rounded-md py-2 px-3 text-sm text-input placeholder:text-input";
|
||||||
|
|
||||||
const handleNumberChange = (newValue) => {
|
const handleNumberChange = (newValue) => {
|
||||||
handleOnNewValue({ value: Number(newValue) });
|
handleOnNewValue({ value: Number(newValue) });
|
||||||
};
|
};
|
||||||
|
|
@ -87,26 +91,35 @@ export default function IntComponent({
|
||||||
min={getMinValue()}
|
min={getMinValue()}
|
||||||
max={getMaxValue()}
|
max={getMaxValue()}
|
||||||
onChange={handleNumberChange}
|
onChange={handleNumberChange}
|
||||||
|
isDisabled={disabled || readonly}
|
||||||
value={value ?? ""}
|
value={value ?? ""}
|
||||||
>
|
>
|
||||||
<NumberInputField
|
<NumberInputField
|
||||||
className={getInputClassName()}
|
className={
|
||||||
|
disabled || readonly ? DISABLED_INPUT_CLASS : getInputClassName()
|
||||||
|
}
|
||||||
onChange={handleChangeInput}
|
onChange={handleChangeInput}
|
||||||
onKeyDown={(event) => handleKeyDown(event, value, "")}
|
onKeyDown={(event) => handleKeyDown(event, value, "")}
|
||||||
onInput={handleInputChange}
|
onInput={handleInputChange}
|
||||||
disabled={disabled}
|
disabled={disabled || readonly}
|
||||||
placeholder={editNode ? "Integer number" : "Type an integer number"}
|
placeholder={editNode ? "Integer number" : "Type an integer number"}
|
||||||
data-testid={id}
|
data-testid={id}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
/>
|
/>
|
||||||
<NumberInputStepper className={stepperClassName}>
|
<NumberInputStepper className={stepperClassName}>
|
||||||
<NumberIncrementStepper className={incrementStepperClassName}>
|
<NumberIncrementStepper
|
||||||
|
className={incrementStepperClassName}
|
||||||
|
_disabled={{ cursor: "default" }}
|
||||||
|
>
|
||||||
<PlusIcon
|
<PlusIcon
|
||||||
className={iconClassName}
|
className={iconClassName}
|
||||||
strokeWidth={ICON_STROKE_WIDTH}
|
strokeWidth={ICON_STROKE_WIDTH}
|
||||||
/>
|
/>
|
||||||
</NumberIncrementStepper>
|
</NumberIncrementStepper>
|
||||||
<NumberDecrementStepper className={decrementStepperClassName}>
|
<NumberDecrementStepper
|
||||||
|
className={decrementStepperClassName}
|
||||||
|
_disabled={{ cursor: "default" }}
|
||||||
|
>
|
||||||
<MinusIcon
|
<MinusIcon
|
||||||
className={iconClassName}
|
className={iconClassName}
|
||||||
strokeWidth={ICON_STROKE_WIDTH}
|
strokeWidth={ICON_STROKE_WIDTH}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ export function ParameterRenderComponent({
|
||||||
disabled,
|
disabled,
|
||||||
nodeClass,
|
nodeClass,
|
||||||
handleNodeClass,
|
handleNodeClass,
|
||||||
|
helperText: templateData?.helper_text,
|
||||||
readonly: templateData.readonly,
|
readonly: templateData.readonly,
|
||||||
placeholder,
|
placeholder,
|
||||||
isToolMode,
|
isToolMode,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export type BaseInputProps<valueType = any> = {
|
||||||
handleOnNewValue: handleOnNewValueType;
|
handleOnNewValue: handleOnNewValueType;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
nodeClass?: APIClassType;
|
nodeClass?: APIClassType;
|
||||||
|
helperText?: string;
|
||||||
handleNodeClass?: (value: any, code?: string, type?: string) => void;
|
handleNodeClass?: (value: any, code?: string, type?: string) => void;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,12 @@ export function getCustomParameterTitle({
|
||||||
title,
|
title,
|
||||||
nodeId,
|
nodeId,
|
||||||
isFlexView,
|
isFlexView,
|
||||||
|
required,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
isFlexView: boolean;
|
isFlexView: boolean;
|
||||||
|
required: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={cn(isFlexView && "max-w-56 truncate")}>
|
<div className={cn(isFlexView && "max-w-56 truncate")}>
|
||||||
|
|
@ -66,6 +68,7 @@ export function getCustomParameterTitle({
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
|
{required && <span className="text-red-500">*</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
|
import React from "react";
|
||||||
import { FieldParserType } from "../types/api";
|
import { FieldParserType } from "../types/api";
|
||||||
|
|
||||||
function toSnakeCase(str: string): string {
|
function toSnakeCase(str: string): string {
|
||||||
|
|
@ -134,3 +136,13 @@ export const getStatusColor = (status: string): string => {
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const convertStringToHTML = (htmlString: string): JSX.Element => {
|
||||||
|
return React.createElement("span", {
|
||||||
|
dangerouslySetInnerHTML: { __html: sanitizeHTML(htmlString) },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sanitizeHTML = (htmlString: string): string => {
|
||||||
|
return DOMPurify.sanitize(htmlString);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -28,14 +28,11 @@ test(
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByTestId("outputsChat Output")
|
.getByTestId("outputsChat Output")
|
||||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
.hover()
|
||||||
await page.mouse.up();
|
.then(async () => {
|
||||||
await page.mouse.down();
|
await page.getByTestId("add-component-button-chat-output").click();
|
||||||
|
});
|
||||||
|
|
||||||
await adjustScreenView(page);
|
|
||||||
|
|
||||||
await page.getByTestId("zoom_out").click();
|
|
||||||
await page.getByTestId("zoom_out").click();
|
|
||||||
await page.getByTestId("zoom_out").click();
|
await page.getByTestId("zoom_out").click();
|
||||||
await page.getByTestId("zoom_out").click();
|
await page.getByTestId("zoom_out").click();
|
||||||
|
|
||||||
|
|
@ -47,13 +44,9 @@ test(
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByTestId("inputsChat Input")
|
.getByTestId("inputsChat Input")
|
||||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
|
||||||
await page.mouse.up();
|
targetPosition: { x: 100, y: 100 },
|
||||||
await page.mouse.down();
|
});
|
||||||
|
|
||||||
await page.waitForSelector('[data-testid="fit_view"]', {
|
|
||||||
timeout: 100000,
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.getByTestId("sidebar-search-input").click();
|
await page.getByTestId("sidebar-search-input").click();
|
||||||
await page.getByTestId("sidebar-search-input").fill("text output");
|
await page.getByTestId("sidebar-search-input").fill("text output");
|
||||||
|
|
@ -63,92 +56,21 @@ test(
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByTestId("outputsText Output")
|
.getByTestId("outputsText Output")
|
||||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
|
||||||
await page.mouse.up();
|
targetPosition: { x: 300, y: 300 },
|
||||||
await page.mouse.down();
|
});
|
||||||
|
|
||||||
await adjustScreenView(page);
|
await adjustScreenView(page);
|
||||||
|
|
||||||
await page.getByTestId("zoom_out").click();
|
await page
|
||||||
await page.getByTestId("zoom_out").click();
|
.getByTestId("handle-chatinput-noshownode-message-source")
|
||||||
await page.getByTestId("zoom_out").click();
|
.click();
|
||||||
await page.getByTestId("zoom_out").click();
|
|
||||||
|
|
||||||
const elementsChatInput = await page
|
await page.getByTestId("handle-textoutput-shownode-text-left").click();
|
||||||
.locator('[data-testid="handle-chatinput-noshownode-message-source"]')
|
|
||||||
.all();
|
|
||||||
|
|
||||||
let visibleElementHandle;
|
await page.getByTestId("handle-textoutput-shownode-message-right").click();
|
||||||
|
await page.getByTestId("handle-chatoutput-noshownode-text-target").click();
|
||||||
|
|
||||||
for (const element of elementsChatInput) {
|
|
||||||
if (await element.isVisible()) {
|
|
||||||
visibleElementHandle = element;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click and hold on the first element
|
|
||||||
await visibleElementHandle.hover();
|
|
||||||
await page.mouse.down();
|
|
||||||
|
|
||||||
// Move to the second element
|
|
||||||
|
|
||||||
const elementsTextOutput = await page
|
|
||||||
.getByTestId("handle-textoutput-shownode-text-left")
|
|
||||||
.all();
|
|
||||||
|
|
||||||
for (const element of elementsTextOutput) {
|
|
||||||
if (await element.isVisible()) {
|
|
||||||
visibleElementHandle = element;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await visibleElementHandle.hover();
|
|
||||||
|
|
||||||
// Release the mouse
|
|
||||||
await page.mouse.up();
|
|
||||||
|
|
||||||
await page.getByTestId("fit_view").click();
|
|
||||||
await page.getByTestId("fit_view").click();
|
|
||||||
await page.getByTestId("fit_view").click();
|
|
||||||
await page.getByTestId("fit_view").click();
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
const elementsTextOutputRight = await page
|
|
||||||
.locator('[data-testid="handle-textoutput-shownode-message-right"]')
|
|
||||||
.all();
|
|
||||||
|
|
||||||
for (const element of elementsTextOutputRight) {
|
|
||||||
if (await element.isVisible()) {
|
|
||||||
visibleElementHandle = element;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click and hold on the first element
|
|
||||||
await visibleElementHandle.hover();
|
|
||||||
await page.mouse.down();
|
|
||||||
|
|
||||||
//
|
|
||||||
const elementsChatOutput = await page
|
|
||||||
.getByTestId("handle-chatoutput-noshownode-text-target")
|
|
||||||
.all();
|
|
||||||
|
|
||||||
for (const element of elementsChatOutput) {
|
|
||||||
if (await element.isVisible()) {
|
|
||||||
visibleElementHandle = element;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await visibleElementHandle.hover();
|
|
||||||
|
|
||||||
// Release the mouse
|
|
||||||
await page.mouse.up();
|
|
||||||
|
|
||||||
await page.getByTestId("fit_view").click();
|
|
||||||
await page.getByText("Playground", { exact: true }).last().click();
|
await page.getByText("Playground", { exact: true }).last().click();
|
||||||
await page.waitForSelector('[data-testid="input-chat-playground"]', {
|
await page.waitForSelector('[data-testid="input-chat-playground"]', {
|
||||||
timeout: 100000,
|
timeout: 100000,
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ test(
|
||||||
|
|
||||||
await page.getByText("Edit Prompt", { exact: true }).click();
|
await page.getByText("Edit Prompt", { exact: true }).click();
|
||||||
|
|
||||||
await page.getByTestId("edit-prompt-sanitized").click();
|
await page.getByTestId("edit-prompt-sanitized").last().click();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByTestId("modal-promptarea_prompt_template")
|
.getByTestId("modal-promptarea_prompt_template")
|
||||||
|
|
@ -52,18 +52,19 @@ test(
|
||||||
|
|
||||||
let promptSanitizedText = await page
|
let promptSanitizedText = await page
|
||||||
.getByTestId("edit-prompt-sanitized")
|
.getByTestId("edit-prompt-sanitized")
|
||||||
|
.last()
|
||||||
.textContent();
|
.textContent();
|
||||||
|
|
||||||
expect(promptSanitizedText).toBe("THIS IS A TEST");
|
expect(promptSanitizedText).toBe("THIS IS A TEST");
|
||||||
|
|
||||||
await page.getByTestId("edit-prompt-sanitized").click();
|
await page.getByTestId("edit-prompt-sanitized").last().click();
|
||||||
|
|
||||||
await page.keyboard.press(`ControlOrMeta+a`);
|
await page.keyboard.press(`ControlOrMeta+a`);
|
||||||
await page.keyboard.press("Backspace");
|
await page.keyboard.press("Backspace");
|
||||||
|
|
||||||
await page.getByText("Edit Prompt", { exact: true }).click();
|
await page.getByText("Edit Prompt", { exact: true }).click();
|
||||||
|
|
||||||
await page.getByTestId("edit-prompt-sanitized").click();
|
await page.getByTestId("edit-prompt-sanitized").last().click();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByTestId("modal-promptarea_prompt_template")
|
.getByTestId("modal-promptarea_prompt_template")
|
||||||
|
|
@ -73,6 +74,7 @@ test(
|
||||||
|
|
||||||
promptSanitizedText = await page
|
promptSanitizedText = await page
|
||||||
.getByTestId("edit-prompt-sanitized")
|
.getByTestId("edit-prompt-sanitized")
|
||||||
|
.last()
|
||||||
.textContent();
|
.textContent();
|
||||||
|
|
||||||
expect(promptSanitizedText).toBe("THIS IS A TEST 2");
|
expect(promptSanitizedText).toBe("THIS IS A TEST 2");
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,10 @@ test("chat_io_teste", { tag: ["@release", "@workspace"] }, async ({ page }) => {
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByTestId("outputsChat Output")
|
.getByTestId("outputsChat Output")
|
||||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
.hover()
|
||||||
await page.mouse.up();
|
.then(async () => {
|
||||||
await page.mouse.down();
|
await page.getByTestId("add-component-button-chat-output").click();
|
||||||
|
});
|
||||||
|
|
||||||
await page.getByTestId("sidebar-search-input").click();
|
await page.getByTestId("sidebar-search-input").click();
|
||||||
await page.getByTestId("sidebar-search-input").fill("chat input");
|
await page.getByTestId("sidebar-search-input").fill("chat input");
|
||||||
|
|
@ -32,59 +33,19 @@ test("chat_io_teste", { tag: ["@release", "@workspace"] }, async ({ page }) => {
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByTestId("inputsChat Input")
|
.getByTestId("inputsChat Input")
|
||||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
|
||||||
await page.mouse.up();
|
targetPosition: { x: 100, y: 100 },
|
||||||
await page.mouse.down();
|
});
|
||||||
|
|
||||||
await page.waitForSelector('[data-testid="fit_view"]', {
|
await page.waitForSelector('[data-testid="fit_view"]', {
|
||||||
timeout: 100000,
|
timeout: 100000,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId("fit_view").click();
|
await page.getByTestId("fit_view").click();
|
||||||
await page.getByTestId("zoom_out").click();
|
|
||||||
await page.getByTestId("zoom_out").click();
|
|
||||||
await page.getByTestId("zoom_out").click();
|
|
||||||
await page.getByTestId("zoom_out").click();
|
|
||||||
await page.getByTestId("zoom_out").click();
|
|
||||||
await page.getByTestId("zoom_out").click();
|
|
||||||
await page.getByTestId("zoom_out").click();
|
|
||||||
|
|
||||||
const elementsChatInput = await page
|
await page.getByTestId("handle-chatinput-noshownode-message-source").click();
|
||||||
.locator('[data-testid="handle-chatinput-noshownode-message-source"]')
|
await page.getByTestId("handle-chatoutput-noshownode-text-target").click();
|
||||||
.all();
|
|
||||||
|
|
||||||
let visibleElementHandle;
|
|
||||||
|
|
||||||
for (const element of elementsChatInput) {
|
|
||||||
if (await element.isVisible()) {
|
|
||||||
visibleElementHandle = element;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click and hold on the first element
|
|
||||||
await visibleElementHandle.hover();
|
|
||||||
await page.mouse.down();
|
|
||||||
|
|
||||||
// Move to the second element
|
|
||||||
|
|
||||||
const elementsChatOutput = await page
|
|
||||||
.getByTestId("handle-chatoutput-noshownode-text-target")
|
|
||||||
.all();
|
|
||||||
|
|
||||||
for (const element of elementsChatOutput) {
|
|
||||||
if (await element.isVisible()) {
|
|
||||||
visibleElementHandle = element;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await visibleElementHandle.hover();
|
|
||||||
|
|
||||||
// Release the mouse
|
|
||||||
await page.mouse.up();
|
|
||||||
|
|
||||||
await page.getByTestId("fit_view").click();
|
|
||||||
await page.getByText("Playground", { exact: true }).last().click();
|
await page.getByText("Playground", { exact: true }).last().click();
|
||||||
await page.waitForSelector('[data-testid="input-chat-playground"]', {
|
await page.waitForSelector('[data-testid="input-chat-playground"]', {
|
||||||
timeout: 100000,
|
timeout: 100000,
|
||||||
|
|
|
||||||
|
|
@ -95,17 +95,13 @@ test(
|
||||||
.getByTestId("inputlist_str_urls_0")
|
.getByTestId("inputlist_str_urls_0")
|
||||||
.fill("https://www.example.com");
|
.fill("https://www.example.com");
|
||||||
|
|
||||||
// Connect text output to first chat output
|
await page.getByTestId("handle-url-shownode-text-right").nth(0).click();
|
||||||
const urlTextOutput = await page
|
await page.waitForTimeout(600);
|
||||||
.getByTestId("handle-url-shownode-text-right")
|
|
||||||
.nth(0);
|
await page
|
||||||
await urlTextOutput.hover();
|
|
||||||
await page.mouse.down();
|
|
||||||
const firstChatInput = await page
|
|
||||||
.getByTestId("handle-chatoutput-noshownode-text-target")
|
.getByTestId("handle-chatoutput-noshownode-text-target")
|
||||||
.nth(0);
|
.nth(0)
|
||||||
await firstChatInput.hover();
|
.click();
|
||||||
await page.mouse.up();
|
|
||||||
|
|
||||||
// Run flow and test text output inspection
|
// Run flow and test text output inspection
|
||||||
await page.getByTestId("button_run_url").first().click();
|
await page.getByTestId("button_run_url").first().click();
|
||||||
|
|
@ -120,16 +116,16 @@ test(
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
|
|
||||||
// Connect dataframe output to second chat output
|
// Connect dataframe output to second chat output
|
||||||
const urlDataframeOutput = await page
|
await page
|
||||||
.getByTestId("handle-url-shownode-dataframe-right")
|
.getByTestId("handle-url-shownode-dataframe-right")
|
||||||
.nth(0);
|
.nth(0)
|
||||||
await urlDataframeOutput.hover();
|
.click();
|
||||||
await page.mouse.down();
|
await page.waitForTimeout(600);
|
||||||
const secondChatInput = await page
|
await page
|
||||||
.getByTestId("handle-chatoutput-noshownode-text-target")
|
.getByTestId("handle-chatoutput-noshownode-text-target")
|
||||||
.nth(1);
|
.nth(1)
|
||||||
await secondChatInput.hover();
|
.click();
|
||||||
await page.mouse.up();
|
await page.waitForTimeout(600);
|
||||||
|
|
||||||
// Run and verify text output is still shown
|
// Run and verify text output is still shown
|
||||||
await page.getByTestId("button_run_url").first().click();
|
await page.getByTestId("button_run_url").first().click();
|
||||||
|
|
@ -143,6 +139,7 @@ test(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
|
await page.waitForTimeout(600);
|
||||||
|
|
||||||
// Remove text connection
|
// Remove text connection
|
||||||
const textEdge = await page.locator(".react-flow__edge").first();
|
const textEdge = await page.locator(".react-flow__edge").first();
|
||||||
|
|
@ -162,7 +159,7 @@ test(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
|
await page.waitForTimeout(600);
|
||||||
// Remove all connections
|
// Remove all connections
|
||||||
const dataEdge = await page.locator(".react-flow__edge").first();
|
const dataEdge = await page.locator(".react-flow__edge").first();
|
||||||
await dataEdge.click();
|
await dataEdge.click();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue