feat: add new node Input type for data in table format (#2635)
* feat: add Table component and related functionality This commit adds the Table component and related functionality to the codebase. The Table component is used to display tabular data and includes features such as pagination, row deletion, row duplication, and adding new rows. The TableOptions component is also added to provide options for resetting the grid and adding new rows. Additionally, the necessary types and interfaces are updated to support the Table component. This feature enhances the user experience by allowing them to interact with tabular data in a more intuitive way. * feat: add Edit Data trigger to TableNodeComponent * [autofix.ci] apply automated fixes * feat: add TableSchema class for defining table structure * feat: add TableMixin class for table-related functionality * feat: add TableInput class for table-related functionality * feat: add TableInput to io module * feat: update Column model in table schema This commit updates the `Column` model in the table schema to include the `display_name` and `name` fields instead of `header` and `field`. It also adds validation for the `formatter` field to accept either a `FormatterType` enum value or a string. This change improves the clarity and flexibility of the table schema. * feat: add displayEmptyAlert prop to TableComponent This commit adds the `displayEmptyAlert` prop to the `TableComponent` in order to control whether an alert is displayed when the table has no data. By default, the alert will be shown, but it can be disabled by setting `displayEmptyAlert` to `false`. This feature enhances the flexibility of the table component by allowing users to customize the behavior when there are no rows in the table. * This commit improves the TableAutoCellRender component by adding support for a custom formatter. The formatter can be specified as a prop and allows for rendering the cell value in different formats, such as JSON. This enhancement enhances the flexibility and customization options of the TableAutoCellRender component. * feat: add FormatColumns function to utils.ts This commit adds the `FormatColumns` function to `utils.ts` file. The function takes an array of `ColumnField` objects and returns an array of `ColDef` objects. It maps each `ColumnField` to a `ColDef` with properties like `headerName`, `field`, `sortable`, and `filter`. If a `ColumnField` has a `formatter` property, it sets the `cellDataType` or `cellRendererParams` accordingly. This function enhances the flexibility and customization options for formatting columns in the table. * feat: enhance TableNodeComponent with FormatColumns function This commit enhances the TableNodeComponent by utilizing the FormatColumns function from utils.ts. The FormatColumns function takes an array of ColumnField objects and returns an array of ColDef objects, allowing for flexible and customizable column formatting in the table. By integrating this function, the TableNodeComponent now has improved column handling capabilities. * chore: Update TableNodeComponent and TableComponent This commit updates the TableNodeComponent and TableComponent to improve column handling and customization options. The TableNodeComponent now utilizes the FormatColumns function from utils.ts, allowing for flexible and customizable column formatting in the table. The TableComponent now has a new prop, displayEmptyAlert, which controls whether an alert is displayed when the table has no data. These enhancements enhance the flexibility and customization options of the table components. * [autofix.ci] apply automated fixes * feat: Update TableNodeComponent and TableComponent This commit updates the TableNodeComponent and TableComponent to improve column handling and customization options. It utilizes the FormatColumns function from utils.ts for flexible and customizable column formatting in the table. The TableComponent now has a new prop, displayEmptyAlert, to control the display of an alert when the table has no data. These enhancements enhance the flexibility and customization options of the table components. * feat: initialize table field values as DataFrame * feat: Enhance TableNodeComponent with duplicateRow function This commit enhances the TableNodeComponent by adding the duplicateRow function. This function allows users to duplicate selected rows in the table. When called, it clones the selected nodes and adds the duplicated rows to the table. This feature enhances the flexibility and customization options of the TableNodeComponent. * [autofix.ci] apply automated fixes * feat: Remove "text" from basic_types in FormatColumns function This commit removes the "text" value from the basic_types set in the FormatColumns function in utils.ts. The basic_types set is used to determine the column type for formatting in the table. By removing "text", we ensure that only "date" and "number" types are considered as basic types. This change improves the accuracy and consistency of column formatting in the table. * fix: alingment bug on AgGrid cell * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * Styled the Open Table button on TableNodeComponent * Fixed type of ref on tableComponent * Creaed a TableModal component, that receives the props that are passed to the Table, as well as a title, and creates a modal * Used the TableModal on the TableNodeComponent * Fixed looks of TableModal * Added description set on tableModal * Add description field to TableNodeComponent * Fixed text of description if info is not provided * Added TableComponent in tableNodeCellRenderer * Added styling based on editNode * Added Auto Size to table modal * refactor: update TableOptions component styling and behavior - Update TableOptions component to dynamically apply text color based on selection - Remove unnecessary console.log statement - Improve hover behavior for the Trash2 icon * chore: Remove unnecessary imports and initialize empty columns array in TableNodeComponent * feat: Add default values for sortable and filterable in Column model The code changes in `table.py` modify the `Column` model in the `langflow.schema` module. The `sortable` and `filterable` attributes of the `Column` model now have default values of `True`. This change ensures that new instances of the `Column` model will have these attributes set to `True` by default. Based on the recent user commits and repository commits, the commit message follows the established convention of using a prefix to indicate the type of change (`feat` for a new feature) and provides a clear and concise description of the changes made. * feat(utils.ts): add check for empty columns array in FormatColumns function to prevent errors * feat: Add validation for TableInput value in inputs.py The code changes in `inputs.py` add a validation function for the `value` attribute of the `TableInput` class. The function checks if the value is a list of dictionaries and raises a `ValueError` if it is not. This ensures that the `TableInput` instances have a valid value that is a list of dictionaries. Based on the recent user commits and repository commits, the commit message follows the established convention of using a prefix to indicate the type of change (`feat` for a new feature) and provides a clear and concise description of the changes made. * [autofix.ci] apply automated fixes * feat: extend editable field to json field * [autofix.ci] apply automated fixes * feat: Add validation for TableInput value in inputs.py * feat(validate.py): add exception handling to catch and re-raise ValidationError with a more informative error message * chore: Refactor error message in build_custom_component_template function * fix(validate.py): improve error message formatting in create_class function Refactor the error message formatting in the `create_class` function in `validate.py` to improve readability and clarity. Instead of using a list comprehension to extract the error messages, the code now uses a nested list comprehension to split the error messages and extract the relevant information. This change ensures that the error message is properly formatted and provides more informative details about the validation errors. Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com> * feat: Update TableMixin to support TableSchema or list of Columns The TableMixin class in input_mixin.py has been updated to support either a TableSchema object or a list of Columns for the table_schema attribute. This change allows for more flexibility in defining the table schema for input validation. * feat: Update TableNodeComponent to generate backend columns from value Refactor the TableNodeComponent to generate backend columns from the value when the columns prop is not provided. This change ensures that the component can handle dynamic column generation based on the value, improving flexibility and usability. Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com> * Refactor extractColumnsFromRows function to return only ColDef objects The extractColumnsFromRows function in utils.ts has been refactored to return only ColDef objects instead of a combination of ColDef and ColGroupDef objects. This change simplifies the function's return type and improves consistency in the codebase. Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com> * [autofix.ci] apply automated fixes * refactor: Generate backend columns from value in TableNodeComponent Refactor the TableNodeComponent to generate backend columns from the value when the columns prop is not provided. This change ensures that the component can handle dynamic column generation based on the value, improving flexibility and usability. * feat: Update TableNodeComponent to handle number and date properly * fix bug that delete all rows on modal close * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com>
This commit is contained in:
parent
18dfd9a3d6
commit
007c38afed
21 changed files with 496 additions and 54 deletions
|
|
@ -421,7 +421,7 @@ def build_custom_component_template(
|
|||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": (f"Something went wrong while building the custom component. Hints: {str(exc)}"),
|
||||
"error": (f"Error building Component: {str(exc)}"),
|
||||
"traceback": traceback.format_exc(),
|
||||
},
|
||||
) from exc
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import types
|
|||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Dict, Iterator, List, Mapping, Optional, Set
|
||||
|
||||
import pandas as pd
|
||||
from loguru import logger
|
||||
|
||||
from langflow.exceptions.component import ComponentBuildException
|
||||
|
|
@ -373,6 +374,13 @@ class Vertex:
|
|||
params[field_name] = val
|
||||
elif isinstance(val, str):
|
||||
params[field_name] = val != ""
|
||||
elif field.get("type") == "table" and val is not None:
|
||||
# check if the value is a list of dicts
|
||||
# if it is, create a pandas dataframe from it
|
||||
if isinstance(val, list) and all(isinstance(item, dict) for item in val):
|
||||
params[field_name] = pd.DataFrame(val)
|
||||
else:
|
||||
raise ValueError(f"Invalid value type {type(val)} for field {field_name}")
|
||||
elif val is not None and val != "":
|
||||
params[field_name] = val
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from .inputs import (
|
|||
PromptInput,
|
||||
SecretStrInput,
|
||||
StrInput,
|
||||
TableInput,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
|
@ -34,4 +35,5 @@ __all__ = [
|
|||
"SecretStrInput",
|
||||
"StrInput",
|
||||
"MessageTextInput",
|
||||
"TableInput",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from pydantic import BaseModel, ConfigDict, Field, PlainSerializer, field_valida
|
|||
|
||||
from langflow.field_typing.range_spec import RangeSpec
|
||||
from langflow.inputs.validators import CoalesceBool
|
||||
from langflow.schema.table import Column, TableSchema
|
||||
|
||||
|
||||
class FieldTypes(str, Enum):
|
||||
|
|
@ -18,6 +19,7 @@ class FieldTypes(str, Enum):
|
|||
FILE = "file"
|
||||
PROMPT = "prompt"
|
||||
OTHER = "other"
|
||||
TABLE = "table"
|
||||
|
||||
|
||||
SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value, return_type=str)]
|
||||
|
|
@ -136,3 +138,16 @@ class DropDownMixin(BaseModel):
|
|||
|
||||
class MultilineMixin(BaseModel):
|
||||
multiline: CoalesceBool = True
|
||||
|
||||
|
||||
class TableMixin(BaseModel):
|
||||
table_schema: Optional[TableSchema | list[Column]] = None
|
||||
|
||||
@field_validator("table_schema")
|
||||
@classmethod
|
||||
def validate_table_schema(cls, v):
|
||||
if isinstance(v, list) and all(isinstance(column, Column) for column in v):
|
||||
return TableSchema(columns=v)
|
||||
if isinstance(v, TableSchema):
|
||||
return v
|
||||
raise ValueError("table_schema must be a TableSchema or a list of Columns")
|
||||
|
|
|
|||
|
|
@ -19,9 +19,29 @@ from .input_mixin import (
|
|||
MultilineMixin,
|
||||
RangeMixin,
|
||||
SerializableFieldTypes,
|
||||
TableMixin,
|
||||
)
|
||||
|
||||
|
||||
class TableInput(BaseInputMixin, MetadataTraceMixin, TableMixin, ListableInputMixin):
|
||||
field_type: Optional[SerializableFieldTypes] = FieldTypes.TABLE
|
||||
is_list: bool = True
|
||||
|
||||
@field_validator("value")
|
||||
@classmethod
|
||||
def validate_value(cls, v: Any, _info):
|
||||
# Check if value is a list of dicts
|
||||
if not isinstance(v, list):
|
||||
raise ValueError(f"TableInput value must be a list of dictionaries or Data. Value '{v}' is not a list.")
|
||||
|
||||
for item in v:
|
||||
if not isinstance(item, (dict, Data)):
|
||||
raise ValueError(
|
||||
f"TableInput value must be a list of dictionaries or Data. Item '{item}' is not a dictionary or Data."
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
class HandleInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin):
|
||||
"""
|
||||
Represents an Input that has a Handle to a specific type (e.g. BaseLanguageModel, BaseRetriever, etc.)
|
||||
|
|
@ -338,4 +358,5 @@ InputTypes = Union[
|
|||
StrInput,
|
||||
MessageTextInput,
|
||||
MessageInput,
|
||||
TableInput,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from langflow.inputs import (
|
|||
PromptInput,
|
||||
SecretStrInput,
|
||||
StrInput,
|
||||
TableInput,
|
||||
)
|
||||
from langflow.template import Output
|
||||
|
||||
|
|
@ -36,4 +37,5 @@ __all__ = [
|
|||
"StrInput",
|
||||
"MessageTextInput",
|
||||
"Output",
|
||||
"TableInput",
|
||||
]
|
||||
|
|
|
|||
31
src/backend/base/langflow/schema/table.py
Normal file
31
src/backend/base/langflow/schema/table.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class FormatterType(str, Enum):
|
||||
date = "date"
|
||||
text = "text"
|
||||
number = "number"
|
||||
json = "json"
|
||||
|
||||
|
||||
class Column(BaseModel):
|
||||
display_name: str
|
||||
name: str
|
||||
sortable: bool = Field(default=True)
|
||||
filterable: bool = Field(default=True)
|
||||
formatter: Optional[FormatterType | str] = None
|
||||
|
||||
@field_validator("formatter")
|
||||
def validate_formatter(cls, value):
|
||||
if isinstance(value, str):
|
||||
return FormatterType(value)
|
||||
if isinstance(value, FormatterType):
|
||||
return value
|
||||
raise ValueError("Invalid formatter type")
|
||||
|
||||
|
||||
class TableSchema(BaseModel):
|
||||
columns: List[Column]
|
||||
|
|
@ -4,6 +4,8 @@ import importlib
|
|||
from types import FunctionType
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from langflow.field_typing.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
|
||||
|
||||
|
||||
|
|
@ -168,8 +170,12 @@ def create_class(code, class_name):
|
|||
|
||||
class_code = extract_class_code(module, class_name)
|
||||
compiled_class = compile_class_code(class_code)
|
||||
|
||||
return build_class_constructor(compiled_class, exec_globals, class_name)
|
||||
try:
|
||||
return build_class_constructor(compiled_class, exec_globals, class_name)
|
||||
except ValidationError as e:
|
||||
messages = [error["msg"].split(",", 1) for error in e.errors()]
|
||||
error_message = "\n".join([message[1] if len(message) > 1 else message[0] for message in messages])
|
||||
raise ValueError(error_message) from e
|
||||
|
||||
|
||||
def create_type_ignore_class():
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import TableNodeComponent from "@/components/TableNodeComponent";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
|
@ -524,6 +525,17 @@ export default function ParameterComponent({
|
|||
/>
|
||||
</div>
|
||||
</Case>
|
||||
<Case condition={left === true && type === "table"}>
|
||||
<div className="mt-2 w-full">
|
||||
<TableNodeComponent
|
||||
description={info || "Add or edit data"}
|
||||
columns={data.node?.template[name]?.table_schema?.columns}
|
||||
onChange={handleOnNewValue}
|
||||
tableTitle={data.node?.template[name]?.display_name ?? "Table"}
|
||||
value={data.node?.template[name]?.value}
|
||||
/>
|
||||
</div>
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
|
|
|
|||
149
src/frontend/src/components/TableNodeComponent/index.tsx
Normal file
149
src/frontend/src/components/TableNodeComponent/index.tsx
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import TableModal from "@/modals/tableModal";
|
||||
import { FormatColumns, generateBackendColumnsFromValue } from "@/utils/utils";
|
||||
import { DataTypeDefinition, SelectionChangedEvent } from "ag-grid-community";
|
||||
import { AgGridReact } from "ag-grid-react";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { ForwardedIconComponent } from "../../components/genericIconComponent";
|
||||
import { TableComponentType } from "../../types/components";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export default function TableNodeComponent({
|
||||
tableTitle,
|
||||
description,
|
||||
value,
|
||||
onChange,
|
||||
editNode = false,
|
||||
id = "",
|
||||
columns,
|
||||
}: TableComponentType): JSX.Element {
|
||||
const dataTypeDefinitions: {
|
||||
[cellDataType: string]: DataTypeDefinition<any>;
|
||||
} = useMemo(() => {
|
||||
return {
|
||||
// override `date` to handle custom date format `dd/mm/yyyy`
|
||||
date: {
|
||||
baseDataType: "date",
|
||||
extendsDataType: "date",
|
||||
valueParser: (params) => {
|
||||
if (params.newValue == null) {
|
||||
return null;
|
||||
}
|
||||
// convert from `dd/mm/yyyy`
|
||||
const dateParts = params.newValue.split("/");
|
||||
return dateParts.length === 3
|
||||
? new Date(
|
||||
parseInt(dateParts[2]),
|
||||
parseInt(dateParts[1]) - 1,
|
||||
parseInt(dateParts[0]),
|
||||
)
|
||||
: null;
|
||||
},
|
||||
valueFormatter: (params) => {
|
||||
let date = params.value;
|
||||
if (typeof params.value === "string") {
|
||||
date = new Date(params.value);
|
||||
}
|
||||
// convert to `dd/mm/yyyy`
|
||||
return date == null
|
||||
? ""
|
||||
: `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`;
|
||||
},
|
||||
},
|
||||
number: {
|
||||
baseDataType: "number",
|
||||
extendsDataType: "number",
|
||||
valueFormatter: (params) =>
|
||||
params.value == null ? "" : `${params.value}`,
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
const [selectedNodes, setSelectedNodes] = useState<Array<any>>([]);
|
||||
const agGrid = useRef<AgGridReact>(null);
|
||||
const componentColumns = columns
|
||||
? columns
|
||||
: generateBackendColumnsFromValue(value ?? []);
|
||||
const AgColumns = FormatColumns(componentColumns);
|
||||
function setAllRows() {
|
||||
if (agGrid.current && !agGrid.current.api.isDestroyed()) {
|
||||
const rows: any = [];
|
||||
agGrid.current.api.forEachNode((node) => rows.push(node.data));
|
||||
onChange(rows);
|
||||
}
|
||||
}
|
||||
function deleteRow() {
|
||||
if (agGrid.current && selectedNodes.length > 0) {
|
||||
agGrid.current.api.applyTransaction({
|
||||
remove: selectedNodes.map((node) => node.data),
|
||||
});
|
||||
setSelectedNodes([]);
|
||||
setAllRows();
|
||||
}
|
||||
}
|
||||
function duplicateRow() {
|
||||
if (agGrid.current && selectedNodes.length > 0) {
|
||||
const toDuplicate = selectedNodes.map((node) => cloneDeep(node.data));
|
||||
setSelectedNodes([]);
|
||||
const rows: any = [];
|
||||
onChange([...value, ...toDuplicate]);
|
||||
}
|
||||
}
|
||||
function addRow() {
|
||||
const newRow = {};
|
||||
componentColumns.forEach((column) => {
|
||||
newRow[column.name] = null;
|
||||
});
|
||||
onChange([...value, newRow]);
|
||||
}
|
||||
|
||||
function updateComponent() {
|
||||
setAllRows();
|
||||
}
|
||||
const editable = componentColumns.map((column) => {
|
||||
const isCustomEdit =
|
||||
column.formatter &&
|
||||
(column.formatter === "text" || column.formatter === "json");
|
||||
return {
|
||||
field: column.name,
|
||||
onUpdate: updateComponent,
|
||||
editableCell: isCustomEdit ? false : true,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={"flex w-full items-center"}>
|
||||
<div className="flex w-full items-center gap-3" data-testid={"div-" + id}>
|
||||
<TableModal
|
||||
dataTypeDefinitions={dataTypeDefinitions}
|
||||
autoSizeStrategy={{ type: "fitGridWidth", defaultMinWidth: 100 }}
|
||||
tableTitle={tableTitle}
|
||||
description={description}
|
||||
ref={agGrid}
|
||||
onSelectionChanged={(event: SelectionChangedEvent) => {
|
||||
setSelectedNodes(event.api.getSelectedNodes());
|
||||
}}
|
||||
rowSelection="multiple"
|
||||
suppressRowClickSelection={true}
|
||||
editable={editable}
|
||||
pagination={true}
|
||||
addRow={addRow}
|
||||
onDelete={deleteRow}
|
||||
onDuplicate={duplicateRow}
|
||||
displayEmptyAlert={false}
|
||||
className="h-full w-full"
|
||||
columnDefs={AgColumns}
|
||||
rowData={value}
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
size={editNode ? "xs" : "default"}
|
||||
className="w-full"
|
||||
>
|
||||
<ForwardedIconComponent name="Table" className="mt-px h-4 w-4" />
|
||||
<span className="font-normal">Open Table</span>
|
||||
</Button>
|
||||
</TableModal>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,12 +1,20 @@
|
|||
import DictAreaModal from "../../modals/dictAreaModal";
|
||||
|
||||
export default function ObjectRender({ object }: { object: any }): JSX.Element {
|
||||
//TODO check object type
|
||||
|
||||
export default function ObjectRender({
|
||||
object,
|
||||
setValue,
|
||||
}: {
|
||||
object: any;
|
||||
setValue?: (value: any) => void;
|
||||
}): JSX.Element {
|
||||
let preview =
|
||||
object === null || object === undefined ? "" : JSON.stringify(object);
|
||||
if (object === null || object === undefined) {
|
||||
}
|
||||
return (
|
||||
<DictAreaModal value={object}>
|
||||
<DictAreaModal onChange={setValue} value={object ?? {}}>
|
||||
<div className="flex h-full w-full items-center align-middle transition-all">
|
||||
<div className="truncate">{JSON.stringify(object)}</div>
|
||||
<div className="truncate">{preview}</div>
|
||||
</div>
|
||||
</DictAreaModal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { ColDef, Column } from "ag-grid-community";
|
||||
import TextModal from "../../modals/textModal";
|
||||
|
||||
export default function StringReader({
|
||||
|
|
@ -6,13 +5,14 @@ export default function StringReader({
|
|||
setValue,
|
||||
editable = false,
|
||||
}: {
|
||||
string: string;
|
||||
string: string | null;
|
||||
setValue: (value: string) => void;
|
||||
editable: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<TextModal editable={editable} setValue={setValue} value={string}>
|
||||
<span className="truncate">{string}</span>
|
||||
<TextModal editable={editable} setValue={setValue} value={string ?? ""}>
|
||||
{/* INVISIBLE CHARACTER TO PREVENT AGgrid bug */}
|
||||
<span className="truncate">{string ?? ""}</span>
|
||||
</TextModal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,35 +9,30 @@ export default function TableOptions({
|
|||
deleteRow,
|
||||
hasSelection,
|
||||
stateChange,
|
||||
addRow,
|
||||
}: {
|
||||
resetGrid: () => void;
|
||||
duplicateRow?: () => void;
|
||||
deleteRow?: () => void;
|
||||
addRow?: () => void;
|
||||
hasSelection: boolean;
|
||||
stateChange: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className={cn("absolute bottom-3 left-6")}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
<ShadTooltip content="Reset Columns">
|
||||
<Button
|
||||
unstyled
|
||||
onClick={() => {
|
||||
resetGrid();
|
||||
}}
|
||||
disabled={!stateChange}
|
||||
>
|
||||
<IconComponent
|
||||
name="RotateCcw"
|
||||
strokeWidth={2}
|
||||
className={cn(
|
||||
"h-5 w-5 text-primary transition-all hover:text-accent-foreground",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
{addRow && (
|
||||
<div>
|
||||
<ShadTooltip content={"Add a new row"}>
|
||||
<Button unstyled onClick={addRow}>
|
||||
<IconComponent
|
||||
name="Plus"
|
||||
className={cn("h-5 w-5 text-primary transition-all")}
|
||||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
)}
|
||||
{duplicateRow && (
|
||||
<div>
|
||||
<ShadTooltip
|
||||
|
|
@ -52,7 +47,10 @@ export default function TableOptions({
|
|||
<Button unstyled onClick={duplicateRow} disabled={!hasSelection}>
|
||||
<IconComponent
|
||||
name="Copy"
|
||||
className={cn("h-5 w-5 text-primary transition-all")}
|
||||
className={cn(
|
||||
"h-5 w-5 transition-all",
|
||||
hasSelection ? "text-primary" : "text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
|
|
@ -73,14 +71,35 @@ export default function TableOptions({
|
|||
<IconComponent
|
||||
name="Trash2"
|
||||
className={cn(
|
||||
"h-5 w-5 text-primary transition-all",
|
||||
!hasSelection ? "" : "hover:text-status-red",
|
||||
"h-5 w-5 transition-all",
|
||||
!hasSelection
|
||||
? "text-muted-foreground"
|
||||
: "text-primary hover:text-status-red",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
)}{" "}
|
||||
<div>
|
||||
<ShadTooltip content="Reset Columns">
|
||||
<Button
|
||||
unstyled
|
||||
onClick={() => {
|
||||
resetGrid();
|
||||
}}
|
||||
disabled={!stateChange}
|
||||
>
|
||||
<IconComponent
|
||||
name="RotateCcw"
|
||||
strokeWidth={2}
|
||||
className={cn(
|
||||
"h-5 w-5 text-primary transition-all hover:text-accent-foreground",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,21 +6,31 @@ import ObjectRender from "../../../objectRender";
|
|||
import StringReader from "../../../stringReaderComponent";
|
||||
import { Badge } from "../../../ui/badge";
|
||||
|
||||
interface CustomCellRender extends CustomCellRendererProps {
|
||||
formatter?: "json" | "text";
|
||||
}
|
||||
|
||||
export default function TableAutoCellRender({
|
||||
value,
|
||||
setValue,
|
||||
colDef,
|
||||
}: CustomCellRendererProps) {
|
||||
formatter,
|
||||
}: CustomCellRender) {
|
||||
function getCellType() {
|
||||
switch (typeof value) {
|
||||
let format: string = formatter ? formatter : typeof value;
|
||||
//convert text to string to bind to the string reader
|
||||
format = format === "text" ? "string" : format;
|
||||
format = format === "json" ? "object" : format;
|
||||
|
||||
switch (format) {
|
||||
case "object":
|
||||
if (value === null) {
|
||||
return String(value);
|
||||
} else if (Array.isArray(value)) {
|
||||
return <ObjectRender object={value} />;
|
||||
} else {
|
||||
return <ObjectRender object={value} />;
|
||||
}
|
||||
return (
|
||||
<ObjectRender
|
||||
setValue={!!colDef?.onCellValueChanged ? setValue : undefined}
|
||||
object={value}
|
||||
/>
|
||||
);
|
||||
|
||||
case "string":
|
||||
if (isTimeStampString(value)) {
|
||||
return <DateReader date={value} />;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import TableNodeComponent from "@/components/TableNodeComponent";
|
||||
import { CustomCellRendererProps } from "ag-grid-react";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { useState } from "react";
|
||||
|
|
@ -138,6 +139,19 @@ export default function TableNodeCellRender({
|
|||
editNode={true}
|
||||
/>
|
||||
);
|
||||
case "table":
|
||||
return (
|
||||
<TableNodeComponent
|
||||
description={templateData.info || "Add or edit data"}
|
||||
columns={templateData.table_schema?.columns}
|
||||
onChange={(value) => {
|
||||
handleOnNewValue(value, templateData.key);
|
||||
}}
|
||||
editNode
|
||||
tableTitle={templateData.display_name ?? "Table"}
|
||||
value={templateValue}
|
||||
/>
|
||||
);
|
||||
|
||||
case "float":
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { ColDef } from "ag-grid-community";
|
||||
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
|
||||
import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme applied to the grid
|
||||
import { AgGridReact, AgGridReactProps } from "ag-grid-react";
|
||||
|
|
@ -16,9 +17,10 @@ import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
|||
import TableOptions from "./components/TableOptions";
|
||||
import resetGrid from "./utils/reset-grid-columns";
|
||||
|
||||
interface TableComponentProps extends AgGridReactProps {
|
||||
columnDefs: NonNullable<AgGridReactProps["columnDefs"]>;
|
||||
export interface TableComponentProps extends AgGridReactProps {
|
||||
columnDefs: NonNullable<ColDef<any, any>[]>;
|
||||
rowData: NonNullable<AgGridReactProps["rowData"]>;
|
||||
displayEmptyAlert?: boolean;
|
||||
alertTitle?: string;
|
||||
alertDescription?: string;
|
||||
editable?:
|
||||
|
|
@ -32,6 +34,7 @@ interface TableComponentProps extends AgGridReactProps {
|
|||
pagination?: boolean;
|
||||
onDelete?: () => void;
|
||||
onDuplicate?: () => void;
|
||||
addRow?: () => void;
|
||||
}
|
||||
|
||||
const TableComponent = forwardRef<
|
||||
|
|
@ -42,6 +45,7 @@ const TableComponent = forwardRef<
|
|||
{
|
||||
alertTitle = DEFAULT_TABLE_ALERT_TITLE,
|
||||
alertDescription = DEFAULT_TABLE_ALERT_MSG,
|
||||
displayEmptyAlert = true,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
|
|
@ -62,7 +66,7 @@ const TableComponent = forwardRef<
|
|||
(typeof props.editable === "boolean" && props.editable) ||
|
||||
(Array.isArray(props.editable) &&
|
||||
props.editable.every((field) => typeof field === "string") &&
|
||||
(props.editable as Array<string>).includes(newCol.headerName ?? ""))
|
||||
(props.editable as Array<string>).includes(newCol.field ?? ""))
|
||||
) {
|
||||
newCol = {
|
||||
...newCol,
|
||||
|
|
@ -79,7 +83,7 @@ const TableComponent = forwardRef<
|
|||
onUpdate: (value: any) => void;
|
||||
editableCell: boolean;
|
||||
}>
|
||||
).find((field) => field.field === newCol.headerName);
|
||||
).find((field) => field.field === newCol.field);
|
||||
if (field) {
|
||||
newCol = {
|
||||
...newCol,
|
||||
|
|
@ -90,11 +94,9 @@ const TableComponent = forwardRef<
|
|||
}
|
||||
return newCol;
|
||||
});
|
||||
const gridRef = useRef(null);
|
||||
// @ts-ignore
|
||||
const realRef: React.MutableRefObject<AgGridReact> = ref?.current
|
||||
? ref
|
||||
: gridRef;
|
||||
const realRef: React.MutableRefObject<AgGridReact> =
|
||||
useRef<AgGridReact | null>(null);
|
||||
const dark = useDarkStore((state) => state.dark);
|
||||
const initialColumnDefs = useRef(colDef);
|
||||
const [columnStateChange, setColumnStateChange] = useState(false);
|
||||
|
|
@ -132,7 +134,7 @@ const TableComponent = forwardRef<
|
|||
params.api.setGridOption("columnDefs", updatedColumnDefs);
|
||||
if (props.onColumnMoved) props.onColumnMoved(params);
|
||||
};
|
||||
if (props.rowData.length === 0) {
|
||||
if (props.rowData.length === 0 && displayEmptyAlert) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center rounded-md border">
|
||||
<Alert variant={"default"} className="w-fit">
|
||||
|
|
@ -161,7 +163,15 @@ const TableComponent = forwardRef<
|
|||
}}
|
||||
animateRows={false}
|
||||
columnDefs={colDef}
|
||||
ref={realRef}
|
||||
ref={(node) => {
|
||||
if (!node) return;
|
||||
realRef.current = node;
|
||||
if (typeof ref === "function") {
|
||||
ref(node);
|
||||
} else if (ref) {
|
||||
ref.current = node;
|
||||
}
|
||||
}}
|
||||
onGridReady={onGridReady}
|
||||
onColumnMoved={onColumnMoved}
|
||||
onStateUpdated={(e) => {
|
||||
|
|
@ -180,6 +190,7 @@ const TableComponent = forwardRef<
|
|||
hasSelection={realRef.current?.api?.getSelectedRows().length > 0}
|
||||
duplicateRow={props.onDuplicate ? props.onDuplicate : undefined}
|
||||
deleteRow={props.onDelete ? props.onDelete : undefined}
|
||||
addRow={props.addRow ? props.addRow : undefined}
|
||||
resetGrid={() => {
|
||||
resetGrid(realRef, initialColumnDefs);
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
45
src/frontend/src/modals/tableModal/index.tsx
Normal file
45
src/frontend/src/modals/tableModal/index.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import ForwardedIconComponent from "@/components/genericIconComponent";
|
||||
import TableComponent, {
|
||||
TableComponentProps,
|
||||
} from "@/components/tableComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DialogClose } from "@radix-ui/react-dialog";
|
||||
import { ElementRef, forwardRef, useState } from "react";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
interface TableModalProps extends TableComponentProps {
|
||||
tableTitle: string;
|
||||
description: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const TableModal = forwardRef<
|
||||
ElementRef<typeof TableComponent>,
|
||||
TableModalProps
|
||||
>(({ tableTitle, description, children, ...props }: TableModalProps, ref) => {
|
||||
return (
|
||||
<BaseModal>
|
||||
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={description}>
|
||||
<span className="pr-2">{tableTitle}</span>
|
||||
<ForwardedIconComponent name="Table" className="mr-2 h-4 w-4" />
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<TableComponent
|
||||
className="h-full w-full"
|
||||
ref={ref}
|
||||
{...props}
|
||||
></TableComponent>
|
||||
</BaseModal.Content>
|
||||
<BaseModal.Footer>
|
||||
<DialogClose>
|
||||
<div className="flex w-full justify-end gap-2 pt-2">
|
||||
<Button>Close</Button>
|
||||
</div>
|
||||
</DialogClose>
|
||||
</BaseModal.Footer>
|
||||
</BaseModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default TableModal;
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { ColDef } from "ag-grid-community";
|
||||
import { ReactElement, ReactNode, SetStateAction } from "react";
|
||||
import { ReactFlowJsonObject } from "reactflow";
|
||||
import { InputOutput } from "../../constants/enums";
|
||||
|
|
@ -9,6 +10,7 @@ import {
|
|||
} from "../api";
|
||||
import { ChatMessageType } from "../chat";
|
||||
import { FlowStyleType, FlowType, NodeDataType, NodeType } from "../flow/index";
|
||||
import { ColumnField } from "../utils/functions";
|
||||
import { sourceHandleType, targetHandleType } from "./../flow/index";
|
||||
export type InputComponentType = {
|
||||
name?: string;
|
||||
|
|
@ -134,6 +136,16 @@ export type TextAreaComponentType = {
|
|||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export type TableComponentType = {
|
||||
description: string;
|
||||
tableTitle: string;
|
||||
onChange: (value: any[]) => void;
|
||||
value: any[];
|
||||
editNode?: boolean;
|
||||
id?: string;
|
||||
columns?: ColumnField[];
|
||||
};
|
||||
|
||||
export type outputComponentType = {
|
||||
types: string[];
|
||||
selected: string;
|
||||
|
|
|
|||
|
|
@ -8,3 +8,18 @@ export type getCodesObjProps = {
|
|||
};
|
||||
|
||||
export type getCodesObjReturn = Array<{ name: string; code: string }>;
|
||||
|
||||
export enum FormatterType {
|
||||
date = "date",
|
||||
text = "text",
|
||||
number = "number",
|
||||
json = "json",
|
||||
}
|
||||
|
||||
export type ColumnField = {
|
||||
display_name: string;
|
||||
name: string;
|
||||
sortable: boolean;
|
||||
filterable: boolean;
|
||||
formatter?: FormatterType;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ import {
|
|||
SquarePen,
|
||||
Store,
|
||||
SunIcon,
|
||||
Table,
|
||||
TerminalIcon,
|
||||
TerminalSquare,
|
||||
TextCursorInput,
|
||||
|
|
@ -582,6 +583,7 @@ export const nodeIconsLucide: iconsType = {
|
|||
PGVector: CpuIcon,
|
||||
Confluence: ConfluenceIcon,
|
||||
FreezeAll: freezeAllIcon,
|
||||
Table: Table,
|
||||
AIML: AIMLIcon,
|
||||
"AI/ML": AIMLIcon,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { ColumnField, FormatterType } from "@/types/utils/functions";
|
||||
import { ColDef, ColGroupDef } from "ag-grid-community";
|
||||
import clsx, { ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
|
@ -357,7 +358,7 @@ export function extractColumnsFromRows(
|
|||
rows: object[],
|
||||
mode: "intersection" | "union",
|
||||
excludeColumns?: Array<string>,
|
||||
): (ColDef<any> | ColGroupDef<any>)[] {
|
||||
): ColDef<any>[] {
|
||||
let columnsKeys: { [key: string]: ColDef<any> | ColGroupDef<any> } = {};
|
||||
if (rows.length === 0) {
|
||||
return [];
|
||||
|
|
@ -471,3 +472,62 @@ export function isEndpointNameValid(name: string, maxLength: number): boolean {
|
|||
name.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
export function FormatColumns(columns: ColumnField[]): ColDef<any>[] {
|
||||
if (!columns) return [];
|
||||
const basic_types = new Set(["date", "number"]);
|
||||
const colDefs = columns.map((col, index) => {
|
||||
let newCol: ColDef = {
|
||||
headerName: col.display_name,
|
||||
field: col.name,
|
||||
sortable: col.sortable,
|
||||
filter: col.filterable,
|
||||
};
|
||||
if (col.formatter) {
|
||||
if (basic_types.has(col.formatter)) {
|
||||
newCol.cellDataType = col.formatter;
|
||||
} else {
|
||||
newCol.cellRendererParams = {
|
||||
formatter: col.formatter,
|
||||
};
|
||||
newCol.cellRenderer = TableAutoCellRender;
|
||||
}
|
||||
}
|
||||
return newCol;
|
||||
});
|
||||
|
||||
return colDefs;
|
||||
}
|
||||
|
||||
export function generateBackendColumnsFromValue(rows: Object[]): ColumnField[] {
|
||||
const columns = extractColumnsFromRows(rows, "union");
|
||||
return columns.map((column) => {
|
||||
const newColumn: ColumnField = {
|
||||
name: column.field ?? "",
|
||||
display_name: column.headerName ?? "",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
};
|
||||
if (rows[0] && rows[0][column.field ?? ""]) {
|
||||
const value = rows[0][column.field ?? ""] as any;
|
||||
if (typeof value === "string") {
|
||||
if (isTimeStampString(value)) {
|
||||
newColumn.formatter = FormatterType.date;
|
||||
} else {
|
||||
newColumn.formatter = FormatterType.text;
|
||||
}
|
||||
} else if (typeof value === "object" && value !== null) {
|
||||
// Check if the object is a Date object
|
||||
if (
|
||||
Object.prototype.toString.call(value) === "[object Date]" ||
|
||||
value instanceof Date
|
||||
) {
|
||||
newColumn.formatter = FormatterType.date;
|
||||
} else {
|
||||
newColumn.formatter = FormatterType.json;
|
||||
}
|
||||
}
|
||||
}
|
||||
return newColumn;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue