dhxpyt.cardpanel

1from .cardpanel import CardPanel
2from .cardpanel_config import CardPanelConfig, CardPanelCardConfig
3
4__all__ = ["CardPanel", "CardPanelConfig", "CardPanelCardConfig"]
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.
config
cardpanel
@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 on_add(self, handler: Callable[[], Any]) -> None:
154    def on_add(self, handler: Callable[[], Any]) -> None:
155        self._bind_event("add", handler)
def on_view(self, handler: Callable[[Any], Any]) -> None:
157    def on_view(self, handler: Callable[[Any], Any]) -> None:
158        self._bind_event("view", handler)
def on_card_click(self, handler: Callable[[Any], Any]) -> None:
160    def on_card_click(self, handler: Callable[[Any], Any]) -> None:
161        self._bind_event("cardClick", handler)
def load( self, cards: Iterable[Union[CardPanelCardConfig, Dict[str, Any]]]) -> None:
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)))
def add_card( self, card: Union[CardPanelCardConfig, Dict[str, Any]]) -> None:
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)))
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 (default div)
    • class/className/classes: string or list of classes
    • text or html: content strings. Placeholders like {title} or {card.subtitle} are interpolated from the card/context.
    • attrs/dataset/style: mapping of attributes with placeholders
    • children: 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)
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]
add_button_text: Optional[str] = None
search_button_text: Optional[str] = None
search_placeholder: Optional[str] = None
search_aria_label: Optional[str] = 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
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>)
id: str
title: str
subtitle: Optional[str] = None
pill: Optional[str] = None
icon: Optional[str] = None
extra: Dict[str, Any]
def to_dict(self) -> Dict[str, Any]:
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}