dhxpyt.cardpanel
class
CardPanel:
18class CardPanel: 19 """ 20 Python wrapper for the custom CardPanel widget. 21 """ 22 23 _template_proxies: Dict[str, List[Any]] = {} 24 25 def __init__( 26 self, 27 config: Optional[CardPanelConfig] = None, 28 *, 29 container: Any = None, 30 root: Optional[Union[str, Any]] = None, 31 ) -> None: 32 """ 33 :param config: Optional CardPanelConfig object describing initial options. 34 :param container: Optional DHTMLX layout cell (or similar) used to mount the widget. 35 :param root: Optional CSS selector string or DOM element where the widget should render. 36 """ 37 if container is None and root is None: 38 raise ValueError("CardPanel requires either a container or a root element.") 39 40 self.config = config or CardPanelConfig() 41 self._event_proxies: Dict[str, List[Any]] = {} 42 self._container = container 43 44 root_element = self._resolve_root(container=container, root=root) 45 if root_element is None: 46 raise RuntimeError("Unable to resolve root element for CardPanel.") 47 48 config_payload = self.config.to_dict() 49 self.cardpanel = js.customdhx.CardPanel.new( 50 root_element, 51 js.JSON.parse(json.dumps(config_payload)) 52 ) 53 54 # --------------------------------------------------------------------- 55 # Template registration helpers 56 # --------------------------------------------------------------------- 57 58 @staticmethod 59 def _js_class() -> Any: 60 """ 61 Access the underlying JavaScript constructor, raising a helpful error if unavailable. 62 """ 63 try: 64 return js.customdhx.CardPanel 65 except AttributeError as exc: 66 raise RuntimeError( 67 "customdhx.CardPanel is not available. Ensure the cardpanel JavaScript has been loaded." 68 ) from exc 69 70 @classmethod 71 def register_template(cls, name: str, factory: Any) -> None: 72 """ 73 Register a template factory with the underlying JavaScript widget. 74 75 The `factory` argument may be: 76 * a string containing JS code that evaluates to a factory function 77 * a JsProxy/Function returned from js.eval/js.Function 78 * a Python callable that returns a renderer. The callable will be proxied to JS. 79 """ 80 if not name or not isinstance(name, str): 81 raise ValueError("Template name must be a non-empty string.") 82 83 cardpanel_cls = cls._js_class() 84 register = getattr(cardpanel_cls, "registerTemplate", None) 85 if register is None: 86 raise RuntimeError( 87 "CardPanel.registerTemplate is not available. Rebuild your front-end bundle to include the latest widget code." 88 ) 89 90 factory_ref = factory 91 if isinstance(factory, str): 92 factory_ref = js.eval(factory) 93 elif callable(factory) and not hasattr(factory, "to_py"): 94 # Proxy the Python callable into JS and keep the proxy alive. 95 proxied = create_proxy(factory) 96 cls._template_proxies.setdefault(name, []).append(proxied) 97 factory_ref = proxied 98 else: 99 print(f"[CardPanel] register_template received factory of type {type(factory)}") 100 101 logger.info( 102 "CardPanel.register_template called for '%s' (%s)", name, WRAPPER_REVISION 103 ) 104 print(f"[CardPanel] register_template -> '{name}' ({WRAPPER_REVISION})") 105 register(name, factory_ref) 106 107 @classmethod 108 def get_template(cls, name: str) -> Any: 109 """ 110 Retrieve a previously registered template factory from the JS layer (if available). 111 """ 112 cardpanel_cls = cls._js_class() 113 getter = getattr(cardpanel_cls, "getTemplate", None) 114 if getter is None: 115 return None 116 return getter(name) 117 118 def _resolve_root(self, *, container: Any, root: Optional[Union[str, Any]]) -> Any: 119 """ 120 Resolve the DOM element that will host the CardPanel. 121 """ 122 if container is not None: 123 return container 124 125 if isinstance(root, str): 126 return js.document.querySelector(root) 127 128 return root 129 130 def _bind_event(self, event_name: str, handler: Callable) -> None: 131 """ 132 Helper that registers an event handler and keeps a proxy alive. 133 """ 134 def wrapped(*args, **kwargs): 135 if args: 136 converted = [ 137 arg.to_py() if hasattr(arg, "to_py") else arg 138 for arg in args 139 ] 140 else: 141 converted = [] 142 return handler(*converted, **kwargs) 143 144 proxy = create_proxy(wrapped) 145 bucket = self._event_proxies.setdefault(event_name, []) 146 bucket.append(proxy) 147 self.cardpanel.on(event_name, proxy) 148 149 # Event bindings ------------------------------------------------------- 150 151 def on_search(self, handler: Callable[[str], Any]) -> None: 152 self._bind_event("search", handler) 153 154 def on_add(self, handler: Callable[[], Any]) -> None: 155 self._bind_event("add", handler) 156 157 def on_view(self, handler: Callable[[Any], Any]) -> None: 158 self._bind_event("view", handler) 159 160 def on_card_click(self, handler: Callable[[Any], Any]) -> None: 161 self._bind_event("cardClick", handler) 162 163 # Data API ------------------------------------------------------------- 164 165 def load(self, cards: Iterable[Union[CardPanelCardConfig, Dict[str, Any]]]) -> None: 166 payload = [card.to_dict() if hasattr(card, "to_dict") else card for card in cards] 167 self.cardpanel.load(js.JSON.parse(json.dumps(payload))) 168 169 def add_card(self, card: Union[CardPanelCardConfig, Dict[str, Any]]) -> None: 170 card_payload = card.to_dict() if hasattr(card, "to_dict") else card 171 self.cardpanel.add(js.JSON.parse(json.dumps(card_payload))) 172 173 def filter(self, query: str) -> None: 174 """ 175 Access the underlying filter logic (mirrors the JS helper). 176 """ 177 if hasattr(self.cardpanel, "_filter"): 178 self.cardpanel._filter(query) 179 180 def destroy(self) -> None: 181 """ 182 Tears down DOM content created for the widget (best-effort). 183 """ 184 try: 185 if hasattr(self.cardpanel, "destroy"): 186 self.cardpanel.destroy() 187 elif hasattr(self.cardpanel, "destructor"): 188 self.cardpanel.destructor() 189 finally: 190 self._event_proxies.clear()
Python wrapper for the custom CardPanel widget.
CardPanel( config: Optional[CardPanelConfig] = None, *, container: Any = None, root: Union[str, Any, NoneType] = None)
25 def __init__( 26 self, 27 config: Optional[CardPanelConfig] = None, 28 *, 29 container: Any = None, 30 root: Optional[Union[str, Any]] = None, 31 ) -> None: 32 """ 33 :param config: Optional CardPanelConfig object describing initial options. 34 :param container: Optional DHTMLX layout cell (or similar) used to mount the widget. 35 :param root: Optional CSS selector string or DOM element where the widget should render. 36 """ 37 if container is None and root is None: 38 raise ValueError("CardPanel requires either a container or a root element.") 39 40 self.config = config or CardPanelConfig() 41 self._event_proxies: Dict[str, List[Any]] = {} 42 self._container = container 43 44 root_element = self._resolve_root(container=container, root=root) 45 if root_element is None: 46 raise RuntimeError("Unable to resolve root element for CardPanel.") 47 48 config_payload = self.config.to_dict() 49 self.cardpanel = js.customdhx.CardPanel.new( 50 root_element, 51 js.JSON.parse(json.dumps(config_payload)) 52 )
Parameters
- config: Optional CardPanelConfig object describing initial options.
- container: Optional DHTMLX layout cell (or similar) used to mount the widget.
- root: Optional CSS selector string or DOM element where the widget should render.
@classmethod
def
register_template(cls, name: str, factory: Any) -> None:
70 @classmethod 71 def register_template(cls, name: str, factory: Any) -> None: 72 """ 73 Register a template factory with the underlying JavaScript widget. 74 75 The `factory` argument may be: 76 * a string containing JS code that evaluates to a factory function 77 * a JsProxy/Function returned from js.eval/js.Function 78 * a Python callable that returns a renderer. The callable will be proxied to JS. 79 """ 80 if not name or not isinstance(name, str): 81 raise ValueError("Template name must be a non-empty string.") 82 83 cardpanel_cls = cls._js_class() 84 register = getattr(cardpanel_cls, "registerTemplate", None) 85 if register is None: 86 raise RuntimeError( 87 "CardPanel.registerTemplate is not available. Rebuild your front-end bundle to include the latest widget code." 88 ) 89 90 factory_ref = factory 91 if isinstance(factory, str): 92 factory_ref = js.eval(factory) 93 elif callable(factory) and not hasattr(factory, "to_py"): 94 # Proxy the Python callable into JS and keep the proxy alive. 95 proxied = create_proxy(factory) 96 cls._template_proxies.setdefault(name, []).append(proxied) 97 factory_ref = proxied 98 else: 99 print(f"[CardPanel] register_template received factory of type {type(factory)}") 100 101 logger.info( 102 "CardPanel.register_template called for '%s' (%s)", name, WRAPPER_REVISION 103 ) 104 print(f"[CardPanel] register_template -> '{name}' ({WRAPPER_REVISION})") 105 register(name, factory_ref)
Register a template factory with the underlying JavaScript widget.
The factory argument may be:
* a string containing JS code that evaluates to a factory function
* a JsProxy/Function returned from js.eval/js.Function
* a Python callable that returns a renderer. The callable will be proxied to JS.
@classmethod
def
get_template(cls, name: str) -> Any:
107 @classmethod 108 def get_template(cls, name: str) -> Any: 109 """ 110 Retrieve a previously registered template factory from the JS layer (if available). 111 """ 112 cardpanel_cls = cls._js_class() 113 getter = getattr(cardpanel_cls, "getTemplate", None) 114 if getter is None: 115 return None 116 return getter(name)
Retrieve a previously registered template factory from the JS layer (if available).
def
filter(self, query: str) -> None:
173 def filter(self, query: str) -> None: 174 """ 175 Access the underlying filter logic (mirrors the JS helper). 176 """ 177 if hasattr(self.cardpanel, "_filter"): 178 self.cardpanel._filter(query)
Access the underlying filter logic (mirrors the JS helper).
def
destroy(self) -> None:
180 def destroy(self) -> None: 181 """ 182 Tears down DOM content created for the widget (best-effort). 183 """ 184 try: 185 if hasattr(self.cardpanel, "destroy"): 186 self.cardpanel.destroy() 187 elif hasattr(self.cardpanel, "destructor"): 188 self.cardpanel.destructor() 189 finally: 190 self._event_proxies.clear()
Tears down DOM content created for the widget (best-effort).
@dataclass
class
CardPanelConfig:
30@dataclass 31class CardPanelConfig: 32 """ 33 High-level configuration for the CardPanel widget. 34 35 The ``card_template`` option may be: 36 37 * the name of a registered template (string) 38 * a callable/JS function handle 39 * a nested dictionary describing DOM structure. The descriptor supports keys: 40 - ``tag``: element tag name (default ``div``) 41 - ``class``/``className``/``classes``: string or list of classes 42 - ``text`` or ``html``: content strings. Placeholders like ``{title}`` 43 or ``{card.subtitle}`` are interpolated from the card/context. 44 - ``attrs``/``dataset``/``style``: mapping of attributes with placeholders 45 - ``children``: list of nested descriptor nodes 46 """ 47 title: str = "Data Sources" 48 description: str = "Manage and connect to various data sources with intelligent profiling and lineage tracking." 49 searchable: bool = True 50 auto_filter: bool = True 51 cards: List[Any] = field(default_factory=list) 52 add_button_text: Optional[str] = None 53 search_button_text: Optional[str] = None 54 search_placeholder: Optional[str] = None 55 search_aria_label: Optional[str] = None 56 show_search: Optional[bool] = None 57 view_button_text: Optional[str] = None 58 card_min_width: Optional[Union[int, float, str]] = None 59 card_min_height: Optional[Union[int, float, str]] = None 60 card_gap: Optional[Union[int, float, str]] = None 61 card_columns: Optional[int] = None 62 card_icon_size: Optional[Union[int, float, str]] = None 63 card_template: Optional[Any] = None # accepts string name, callable, or descriptor dict 64 65 def to_dict(self) -> Dict[str, Any]: 66 cards_payload: List[Dict[str, Any]] = [] 67 for card in self.cards: 68 if hasattr(card, "to_dict"): 69 cards_payload.append(card.to_dict()) 70 elif isinstance(card, dict): 71 cards_payload.append(card) 72 else: 73 raise TypeError(f"Unsupported card configuration type: {type(card)!r}") 74 75 config_dict = { 76 "title": self.title, 77 "description": self.description, 78 "searchable": self.searchable, 79 "autoFilter": self.auto_filter, 80 "cards": cards_payload, 81 } 82 83 if self.add_button_text is not None: 84 config_dict["addButtonText"] = self.add_button_text 85 if self.search_button_text is not None: 86 config_dict["searchButtonText"] = self.search_button_text 87 if self.search_placeholder is not None: 88 config_dict["searchPlaceholder"] = self.search_placeholder 89 if self.search_aria_label is not None: 90 config_dict["searchAriaLabel"] = self.search_aria_label 91 if self.show_search is not None: 92 config_dict["showSearch"] = self.show_search 93 if self.view_button_text is not None: 94 config_dict["viewButtonText"] = self.view_button_text 95 if self.card_min_width is not None: 96 config_dict["cardMinWidth"] = self.card_min_width 97 if self.card_min_height is not None: 98 config_dict["cardMinHeight"] = self.card_min_height 99 if self.card_gap is not None: 100 config_dict["cardGap"] = self.card_gap 101 if self.card_columns is not None: 102 config_dict["cardColumns"] = self.card_columns 103 if self.card_icon_size is not None: 104 config_dict["cardIconSize"] = self.card_icon_size 105 if self.card_template is not None: 106 config_dict["cardTemplate"] = self.card_template 107 108 return config_dict
High-level configuration for the CardPanel widget.
The card_template option may be:
- the name of a registered template (string)
- a callable/JS function handle
- a nested dictionary describing DOM structure. The descriptor supports keys:
tag: element tag name (defaultdiv)class/className/classes: string or list of classestextorhtml: content strings. Placeholders like{title}or{card.subtitle}are interpolated from the card/context.attrs/dataset/style: mapping of attributes with placeholderschildren: list of nested descriptor nodes
CardPanelConfig( title: str = 'Data Sources', description: str = 'Manage and connect to various data sources with intelligent profiling and lineage tracking.', searchable: bool = True, auto_filter: bool = True, cards: List[Any] = <factory>, add_button_text: Optional[str] = None, search_button_text: Optional[str] = None, search_placeholder: Optional[str] = None, search_aria_label: Optional[str] = None, show_search: Optional[bool] = None, view_button_text: Optional[str] = None, card_min_width: Union[int, float, str, NoneType] = None, card_min_height: Union[int, float, str, NoneType] = None, card_gap: Union[int, float, str, NoneType] = None, card_columns: Optional[int] = None, card_icon_size: Union[int, float, str, NoneType] = None, card_template: Optional[Any] = None)
description: str =
'Manage and connect to various data sources with intelligent profiling and lineage tracking.'
def
to_dict(self) -> Dict[str, Any]:
65 def to_dict(self) -> Dict[str, Any]: 66 cards_payload: List[Dict[str, Any]] = [] 67 for card in self.cards: 68 if hasattr(card, "to_dict"): 69 cards_payload.append(card.to_dict()) 70 elif isinstance(card, dict): 71 cards_payload.append(card) 72 else: 73 raise TypeError(f"Unsupported card configuration type: {type(card)!r}") 74 75 config_dict = { 76 "title": self.title, 77 "description": self.description, 78 "searchable": self.searchable, 79 "autoFilter": self.auto_filter, 80 "cards": cards_payload, 81 } 82 83 if self.add_button_text is not None: 84 config_dict["addButtonText"] = self.add_button_text 85 if self.search_button_text is not None: 86 config_dict["searchButtonText"] = self.search_button_text 87 if self.search_placeholder is not None: 88 config_dict["searchPlaceholder"] = self.search_placeholder 89 if self.search_aria_label is not None: 90 config_dict["searchAriaLabel"] = self.search_aria_label 91 if self.show_search is not None: 92 config_dict["showSearch"] = self.show_search 93 if self.view_button_text is not None: 94 config_dict["viewButtonText"] = self.view_button_text 95 if self.card_min_width is not None: 96 config_dict["cardMinWidth"] = self.card_min_width 97 if self.card_min_height is not None: 98 config_dict["cardMinHeight"] = self.card_min_height 99 if self.card_gap is not None: 100 config_dict["cardGap"] = self.card_gap 101 if self.card_columns is not None: 102 config_dict["cardColumns"] = self.card_columns 103 if self.card_icon_size is not None: 104 config_dict["cardIconSize"] = self.card_icon_size 105 if self.card_template is not None: 106 config_dict["cardTemplate"] = self.card_template 107 108 return config_dict
@dataclass
class
CardPanelCardConfig:
6@dataclass 7class CardPanelCardConfig: 8 """ 9 Represents an individual card shown inside the CardPanel widget. 10 """ 11 id: str 12 title: str 13 subtitle: Optional[str] = None 14 pill: Optional[str] = None 15 icon: Optional[str] = None 16 extra: Dict[str, Any] = field(default_factory=dict) 17 18 def to_dict(self) -> Dict[str, Any]: 19 data = { 20 "id": self.id, 21 "title": self.title, 22 "subtitle": self.subtitle, 23 "pill": self.pill, 24 "icon": self.icon, 25 } 26 data.update(self.extra or {}) 27 return {key: value for key, value in data.items() if value is not None}
Represents an individual card shown inside the CardPanel widget.
CardPanelCardConfig( id: str, title: str, subtitle: Optional[str] = None, pill: Optional[str] = None, icon: Optional[str] = None, extra: Dict[str, Any] = <factory>)