feat: enhance DataFrame Operations component with contains filter and modern UI (#8838)

* feat: enhance DataFrame Operations component with contains filter and modern UI

- Add "contains" filter operator for partial string matching in DataFrame filters
- Update UI to use SortableListInput with icons for consistent modern design
- Add 7 filter operators: equals, not equals, contains, starts with, ends with, greater than, less than
- Fix deselection handling to prevent "unhashable type: list" errors
- Improve dynamic field visibility when operations are deselected
- Add comprehensive test suite with 25 tests covering all operations and edge cases
- Update placeholder text from "Select DataFrame Operation" to "Select Operation"

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [autofix.ci] apply automated fixes

* Update dataframe_operations.py

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
This commit is contained in:
Rodrigo Nader 2025-07-09 18:50:59 -03:00 • committed by GitHub
commit e0400f29eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 504 additions and 79 deletions

View file

@ -1,4 +1,7 @@
import pandas as pd
from langflow.custom.custom_component.component import Component
from langflow.inputs import SortableListInput
from langflow.io import (
BoolInput,
DataFrameInput,
@ -39,12 +42,25 @@ class DataFrameOperationsComponent(Component):
info="The input DataFrame to operate on.",
required=True,
),
DropdownInput(
SortableListInput(
name="operation",
display_name="Operation",
options=OPERATION_CHOICES,
placeholder="Select Operation",
info="Select the DataFrame operation to perform.",
options=[
{"name": "Add Column", "icon": "plus"},
{"name": "Drop Column", "icon": "minus"},
{"name": "Filter", "icon": "filter"},
{"name": "Head", "icon": "arrow-up"},
{"name": "Rename Column", "icon": "pencil"},
{"name": "Replace Value", "icon": "replace"},
{"name": "Select Columns", "icon": "columns"},
{"name": "Sort", "icon": "arrow-up-down"},
{"name": "Tail", "icon": "arrow-down"},
{"name": "Drop Duplicates", "icon": "copy-x"},
],
real_time_refresh=True,
limit=1,
),
StrInput(
name="column_name",
@ -60,6 +76,16 @@ class DataFrameOperationsComponent(Component):
dynamic=True,
show=False,
),
DropdownInput(
name="filter_operator",
display_name="Filter Operator",
options=["equals", "not equals", "contains", "starts with", "ends with", "greater than", "less than"],
value="equals",
info="The operator to apply for filtering rows.",
advanced=False,
dynamic=True,
show=False,
),
BoolInput(
name="ascending",
display_name="Sort Ascending",
@ -126,6 +152,7 @@ class DataFrameOperationsComponent(Component):
dynamic_fields = [
"column_name",
"filter_value",
"filter_operator",
"ascending",
"new_column_name",
"new_column_value",
@ -138,36 +165,57 @@ class DataFrameOperationsComponent(Component):
build_config[field]["show"] = False
if field_name == "operation":
if field_value == "Filter":
# Handle SortableListInput format
if isinstance(field_value, list):
operation_name = field_value[0].get("name", "") if field_value else ""
else:
operation_name = field_value or ""
# If no operation selected, all dynamic fields stay hidden (already set to False above)
if not operation_name:
return build_config
if operation_name == "Filter":
build_config["column_name"]["show"] = True
build_config["filter_value"]["show"] = True
elif field_value == "Sort":
build_config["filter_operator"]["show"] = True
elif operation_name == "Sort":
build_config["column_name"]["show"] = True
build_config["ascending"]["show"] = True
elif field_value == "Drop Column":
elif operation_name == "Drop Column":
build_config["column_name"]["show"] = True
elif field_value == "Rename Column":
elif operation_name == "Rename Column":
build_config["column_name"]["show"] = True
build_config["new_column_name"]["show"] = True
elif field_value == "Add Column":
elif operation_name == "Add Column":
build_config["new_column_name"]["show"] = True
build_config["new_column_value"]["show"] = True
elif field_value == "Select Columns":
elif operation_name == "Select Columns":
build_config["columns_to_select"]["show"] = True
elif field_value in {"Head", "Tail"}:
elif operation_name in {"Head", "Tail"}:
build_config["num_rows"]["show"] = True
elif field_value == "Replace Value":
elif operation_name == "Replace Value":
build_config["column_name"]["show"] = True
build_config["replace_value"]["show"] = True
build_config["replacement_value"]["show"] = True
elif field_value == "Drop Duplicates":
elif operation_name == "Drop Duplicates":
build_config["column_name"]["show"] = True
return build_config
def perform_operation(self) -> DataFrame:
df_copy = self.df.copy()
op = self.operation
# Handle SortableListInput format for operation
operation_input = getattr(self, "operation", [])
if isinstance(operation_input, list) and len(operation_input) > 0:
op = operation_input[0].get("name", "")
else:
op = ""
# If no operation selected, return original DataFrame
if not op:
return df_copy
if op == "Filter":
return self.filter_rows_by_value(df_copy)
@ -194,7 +242,42 @@ class DataFrameOperationsComponent(Component):
raise ValueError(msg)
def filter_rows_by_value(self, df: DataFrame) -> DataFrame:
return DataFrame(df[df[self.column_name] == self.filter_value])
column = df[self.column_name]
filter_value = self.filter_value
# Handle regular DropdownInput format (just a string value)
operator = getattr(self, "filter_operator", "equals") # Default to equals for backward compatibility
if operator == "equals":
mask = column == filter_value
elif operator == "not equals":
mask = column != filter_value
elif operator == "contains":
mask = column.astype(str).str.contains(str(filter_value), na=False)
elif operator == "starts with":
mask = column.astype(str).str.startswith(str(filter_value), na=False)
elif operator == "ends with":
mask = column.astype(str).str.endswith(str(filter_value), na=False)
elif operator == "greater than":
try:
# Try to convert filter_value to numeric for comparison
numeric_value = pd.to_numeric(filter_value)
mask = column > numeric_value
except (ValueError, TypeError):
# If conversion fails, compare as strings
mask = column.astype(str) > str(filter_value)
elif operator == "less than":
try:
# Try to convert filter_value to numeric for comparison
numeric_value = pd.to_numeric(filter_value)
mask = column < numeric_value
except (ValueError, TypeError):
# If conversion fails, compare as strings
mask = column.astype(str) < str(filter_value)
else:
mask = column == filter_value # Fallback to equals
return DataFrame(df[mask])
def sort_by_column(self, df: DataFrame) -> DataFrame:
return DataFrame(df.sort_values(by=self.column_name, ascending=self.ascending))