dataeditor.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. """Data Editor component from glide-data-grid."""
  2. from __future__ import annotations
  3. from enum import Enum
  4. from typing import Any, Callable, Dict, List, Literal, Optional, Union
  5. from reflex.base import Base
  6. from reflex.components.component import Component, NoSSRComponent
  7. from reflex.components.literals import LiteralRowMarker
  8. from reflex.utils import console, format, imports, types
  9. from reflex.utils.imports import ImportVar
  10. from reflex.utils.serializers import serializer
  11. from reflex.vars import Var, get_unique_variable_name
  12. # TODO: Fix the serialization issue for custom types.
  13. class GridColumnIcons(Enum):
  14. """An Enum for the available icons in DataEditor."""
  15. Array = "array"
  16. AudioUri = "audio_uri"
  17. Boolean = "boolean"
  18. HeaderCode = "code"
  19. Date = "date"
  20. Email = "email"
  21. Emoji = "emoji"
  22. GeoDistance = "geo_distance"
  23. IfThenElse = "if_then_else"
  24. Image = "image"
  25. JoinStrings = "join_strings"
  26. Lookup = "lookup"
  27. Markdown = "markdown"
  28. Math = "math"
  29. Number = "number"
  30. Phone = "phone"
  31. Reference = "reference"
  32. Rollup = "rollup"
  33. RowID = "row_id"
  34. SingleValue = "single_value"
  35. SplitString = "split_string"
  36. String = "string"
  37. TextTemplate = "text_template"
  38. Time = "time"
  39. Uri = "uri"
  40. VideoUri = "video_uri"
  41. # @serializer
  42. # def serialize_gridcolumn_icon(icon: GridColumnIcons) -> str:
  43. # """Serialize grid column icon.
  44. # Args:
  45. # icon: the Icon to serialize.
  46. # Returns:
  47. # The serialized value.
  48. # """
  49. # return "prefix" + str(icon)
  50. # class DataEditorColumn(Base):
  51. # """Column."""
  52. # title: str
  53. # id: Optional[str] = None
  54. # type_: str = "str"
  55. class DataEditorTheme(Base):
  56. """The theme for the DataEditor component."""
  57. accent_color: Optional[str] = None
  58. accent_fg: Optional[str] = None
  59. accent_light: Optional[str] = None
  60. base_font_style: Optional[str] = None
  61. bg_bubble: Optional[str] = None
  62. bg_bubble_selected: Optional[str] = None
  63. bg_cell: Optional[str] = None
  64. bg_cell_medium: Optional[str] = None
  65. bg_header: Optional[str] = None
  66. bg_header_has_focus: Optional[str] = None
  67. bg_header_hovered: Optional[str] = None
  68. bg_icon_header: Optional[str] = None
  69. bg_search_result: Optional[str] = None
  70. border_color: Optional[str] = None
  71. cell_horizontal_padding: Optional[int] = None
  72. cell_vertical_padding: Optional[int] = None
  73. drilldown_border: Optional[str] = None
  74. editor_font_size: Optional[str] = None
  75. fg_icon_header: Optional[str] = None
  76. font_family: Optional[str] = None
  77. header_bottom_border_color: Optional[str] = None
  78. header_font_style: Optional[str] = None
  79. horizontal_border_color: Optional[str] = None
  80. line_height: Optional[int] = None
  81. link_color: Optional[str] = None
  82. text_bubble: Optional[str] = None
  83. text_dark: Optional[str] = None
  84. text_group_header: Optional[str] = None
  85. text_header: Optional[str] = None
  86. text_header_selected: Optional[str] = None
  87. text_light: Optional[str] = None
  88. text_medium: Optional[str] = None
  89. class DataEditor(NoSSRComponent):
  90. """The DataEditor Component."""
  91. tag: str = "DataEditor"
  92. is_default: bool = True
  93. library: str = "@glideapps/glide-data-grid@^5.3.0"
  94. lib_dependencies: List[str] = [
  95. "lodash@^4.17.21",
  96. "marked@^4.0.10",
  97. "react-responsive-carousel@^3.2.7",
  98. ]
  99. # Number of rows.
  100. rows: Optional[Var[int]] = None
  101. # Headers of the columns for the data grid.
  102. columns: Optional[Var[List[Dict[str, Any]]]] = None
  103. # The data.
  104. data: Optional[Var[List[List[Any]]]] = None
  105. # The name of the callback used to find the data to display.
  106. get_cell_content: Optional[Var[str]] = None
  107. # Allow selection for copying.
  108. get_cell_for_selection: Optional[Var[bool]] = None
  109. # Allow paste.
  110. on_paste: Optional[Var[bool]] = None
  111. # Controls the drawing of the focus ring.
  112. draw_focus_ring: Optional[Var[bool]] = None
  113. # Enables or disables the overlay shadow when scrolling horizontally.
  114. fixed_shadow_x: Optional[Var[bool]] = None
  115. # Enables or disables the overlay shadow when scrolling vertically.
  116. fixed_shadow_y: Optional[Var[bool]] = None
  117. # The number of columns which should remain in place when scrolling horizontally. Doesn't include rowMarkers.
  118. freeze_columns: Optional[Var[int]] = None
  119. # Controls the header of the group header row.
  120. group_header_height: Optional[Var[int]] = None
  121. # Controls the height of the header row.
  122. header_height: Optional[Var[int]] = None
  123. # Additional header icons:
  124. # header_icons: Var[Any] # (TODO: must be a map of name: svg)
  125. # The maximum width a column can be automatically sized to.
  126. max_column_auto_width: Optional[Var[int]] = None
  127. # The maximum width a column can be resized to.
  128. max_column_width: Optional[Var[int]] = None
  129. # The minimum width a column can be resized to.
  130. min_column_width: Optional[Var[int]] = None
  131. # Determins the height of each row.
  132. row_height: Optional[Var[int]] = None
  133. # Kind of row markers.
  134. row_markers: Optional[Var[LiteralRowMarker]] = None
  135. # Changes the starting index for row markers.
  136. row_marker_start_index: Optional[Var[int]] = None
  137. # Sets the width of row markers in pixels, if unset row markers will automatically size.
  138. row_marker_width: Optional[Var[int]] = None
  139. # Enable horizontal smooth scrolling.
  140. smooth_scroll_x: Optional[Var[bool]] = None
  141. # Enable vertical smooth scrolling.
  142. smooth_scroll_y: Optional[Var[bool]] = None
  143. # Controls the drawing of the left hand vertical border of a column. If set to a boolean value it controls all borders.
  144. vertical_border: Var[bool] # TODO: support a mapping (dict[int, bool])
  145. # Allow columns selections. ("none", "single", "multi")
  146. column_select: Optional[Var[Literal["none", "single", "multi"]]] = None
  147. # Prevent diagonal scrolling.
  148. prevent_diagonal_scrolling: Optional[Var[bool]] = None
  149. # Allow to scroll past the limit of the actual content on the horizontal axis.
  150. overscroll_x: Optional[Var[int]] = None
  151. # Allow to scroll past the limit of the actual content on the vertical axis.
  152. overscroll_y: Optional[Var[int]] = None
  153. # Initial scroll offset on the horizontal axis.
  154. scroll_offset_x: Optional[Var[int]] = None
  155. # Initial scroll offset on the vertical axis.
  156. scroll_offset_y: Optional[Var[int]] = None
  157. # global theme
  158. theme: Optional[Var[Union[DataEditorTheme, Dict]]] = None
  159. def _get_imports(self):
  160. return imports.merge_imports(
  161. super()._get_imports(),
  162. {
  163. "": {
  164. ImportVar(
  165. tag=f"{format.format_library_name(self.library)}/dist/index.css"
  166. )
  167. },
  168. self.library: {ImportVar(tag="GridCellKind")},
  169. "/utils/helpers/dataeditor.js": {
  170. ImportVar(
  171. tag=f"formatDataEditorCells", is_default=False, install=False
  172. ),
  173. },
  174. },
  175. )
  176. def get_event_triggers(self) -> Dict[str, Callable]:
  177. """The event triggers of the component.
  178. Returns:
  179. The dict describing the event triggers.
  180. """
  181. def edit_sig(pos, data: dict[str, Any]):
  182. return [pos, data]
  183. return {
  184. "on_cell_activated": lambda pos: [pos],
  185. "on_cell_clicked": lambda pos: [pos],
  186. "on_cell_context_menu": lambda pos: [pos],
  187. "on_cell_edited": edit_sig,
  188. "on_group_header_clicked": edit_sig,
  189. "on_group_header_context_menu": lambda grp_idx, data: [grp_idx, data],
  190. "on_group_header_renamed": lambda idx, val: [idx, val],
  191. "on_header_clicked": lambda pos: [pos],
  192. "on_header_context_menu": lambda pos: [pos],
  193. "on_header_menu_click": lambda col, pos: [col, pos],
  194. "on_item_hovered": lambda pos: [pos],
  195. "on_delete": lambda selection: [selection],
  196. "on_finished_editing": lambda new_value, movement: [new_value, movement],
  197. "on_row_appended": lambda: [],
  198. "on_selection_cleared": lambda: [],
  199. "on_column_resize": lambda col, width: [col, width],
  200. }
  201. def _get_hooks(self) -> str | None:
  202. # Define the id of the component in case multiple are used in the same page.
  203. editor_id = get_unique_variable_name()
  204. # Define the name of the getData callback associated with this component and assign to get_cell_content.
  205. data_callback = f"getData_{editor_id}"
  206. self.get_cell_content = Var.create(data_callback, _var_is_local=False) # type: ignore
  207. code = [f"function {data_callback}([col, row])" "{"]
  208. columns_path = f"{self.columns._var_full_name}"
  209. data_path = f"{self.data._var_full_name}"
  210. code.extend(
  211. [
  212. f" return formatDataEditorCells(col, row, {columns_path}, {data_path});",
  213. " }",
  214. ]
  215. )
  216. return "\n".join(code)
  217. @classmethod
  218. def create(cls, *children, **props) -> Component:
  219. """Create the DataEditor component.
  220. Args:
  221. *children: The children of the data editor.
  222. **props: The props of the data editor.
  223. Raises:
  224. ValueError: invalid input.
  225. Returns:
  226. The DataEditor component.&
  227. """
  228. from reflex.components.el import Div
  229. columns = props.get("columns", [])
  230. data = props.get("data", [])
  231. rows = props.get("rows", None)
  232. # If rows is not provided, determine from data.
  233. if rows is None:
  234. props["rows"] = (
  235. data.length() # BaseVar.create(value=f"{data}.length()", is_local=False)
  236. if isinstance(data, Var)
  237. else len(data)
  238. )
  239. if not isinstance(columns, Var) and len(columns):
  240. if (
  241. types.is_dataframe(type(data))
  242. or isinstance(data, Var)
  243. and types.is_dataframe(data._var_type)
  244. ):
  245. raise ValueError(
  246. "Cannot pass in both a pandas dataframe and columns to the data_editor component."
  247. )
  248. else:
  249. props["columns"] = [
  250. format.format_data_editor_column(col) for col in columns
  251. ]
  252. if "theme" in props:
  253. theme = props.get("theme")
  254. if isinstance(theme, Dict):
  255. props["theme"] = DataEditorTheme(**theme)
  256. # Allow by default to select a region of cells in the grid.
  257. props.setdefault("get_cell_for_selection", True)
  258. # Disable on_paste by default if not provided.
  259. props.setdefault("on_paste", False)
  260. if props.pop("get_cell_content", None) is not None:
  261. console.warn(
  262. "get_cell_content is not user configurable, the provided value will be discarded"
  263. )
  264. grid = super().create(*children, **props)
  265. return Div.create(
  266. grid,
  267. width=props.pop("width", "100%"),
  268. height=props.pop("height", "100%"),
  269. )
  270. @staticmethod
  271. def _get_app_wrap_components() -> dict[tuple[int, str], Component]:
  272. """Get the app wrap components for the component.
  273. Returns:
  274. The app wrap components.
  275. """
  276. from reflex.components.el import Div
  277. class Portal(Div):
  278. def get_ref(self):
  279. return None
  280. return {
  281. (-1, "DataEditorPortal"): Portal.create(
  282. id="portal",
  283. position="fixed",
  284. top=0,
  285. )
  286. }
  287. # try:
  288. # pass
  289. # # def format_dataframe_values(df: DataFrame) -> list[list[Any]]:
  290. # # """Format dataframe values to a list of lists.
  291. # # Args:
  292. # # df: The dataframe to format.
  293. # # Returns:
  294. # # The dataframe as a list of lists.
  295. # # """
  296. # # return [
  297. # # [str(d) if isinstance(d, (list, tuple)) else d for d in data]
  298. # # for data in list(df.values.tolist())
  299. # # ]
  300. # # ...
  301. # # @serializer
  302. # # def serialize_dataframe(df: DataFrame) -> dict:
  303. # # """Serialize a pandas dataframe.
  304. # # Args:
  305. # # df: The dataframe to serialize.
  306. # # Returns:
  307. # # The serialized dataframe.
  308. # # """
  309. # # return {
  310. # # "columns": df.columns.tolist(),
  311. # # "data": format_dataframe_values(df),
  312. # # }
  313. # except ImportError:
  314. # pass
  315. @serializer
  316. def serialize_dataeditortheme(theme: DataEditorTheme):
  317. """The serializer for the data editor theme.
  318. Args:
  319. theme: The theme to serialize.
  320. Returns:
  321. The serialized theme.
  322. """
  323. return format.json_dumps(
  324. {format.to_camel_case(k): v for k, v in theme.__dict__.items() if v is not None}
  325. )