import base64
import json
import os
from dash import dcc, Dash, Input, Output, State
from visualization_toolkit.constants import (
LIBRARY_PATH,
JINJA_ENVIRONMENT,
ATLAS_USER_STORE_ID,
ATLAS_VISIBILITY_STORE_ID,
ATLAS_LOCATION_ID,
ATLAS_DEBUG,
)
DASHBOARD_INTERACTION = "dashboard interaction"
def location_store(
refresh: str = "callback-nav",
id: str = ATLAS_LOCATION_ID,
**kwargs,
) -> dcc.Location:
"""
Returns a dcc.Location object for the dash app to navigate between pages and access query parameters in callbacks
:param refresh: Refresh behavior of ``dcc.Location`` when URLs change. Set to ``callback-nav`` which is most suitable for multi-page apps.
:param id: HTML ID of the location element, defaults to ``ATLAS_LOCATION_ID``.
:param kwargs: Optional kwargs to pass to the underlying dcc.Location component.
:return:
"""
return dcc.Location(
id=id,
refresh=refresh,
**kwargs,
)
[docs]
def user_store(
user_id: str = "NOT_SET",
account_id: str = "NOT_SET",
user_metadata: dict[str, str] = None,
account_metadata: dict[str, str] = None,
storage_type: str = "memory",
id: str = ATLAS_USER_STORE_ID,
) -> dcc.Store:
"""
Standard object to store user information as it relates to analytics services (ex: pendo).
Requires passing in a User ID and Account ID that will then be annotated with any analytics events fired by the dash app.
To keep this data available in a dash app session it will be store clientside using the browsers memory or session
:param user_id: User ID associated with this browser session, typically an email
:param account_id: Account ID associated with the user_id
:param user_metadata: Optional metadata for the user in key/value pairs of a dictionary
:param account_metadata: Optional metadata for the account in key/value pairs of a dictionary
:param storage_type: Optionally control browser side storage behavior. This is passed to the ``dcc.Store`` ``storage_type`` parameter.
:param id: Optionally set an HTML ID for this instead of the default. Note when setting this, all other callbacks connected to this ID must be updated to reference the new/non-default value.
:return:
"""
account_metadata = account_metadata or {"type": "NOT_SET"}
user_metadata = user_metadata or {}
return dcc.Store(
id=id,
data={
"user": {"id": user_id} | user_metadata,
"account": {"id": account_id, "name": None} | account_metadata,
},
storage_type=storage_type,
)
[docs]
def visibility_store(
html_ids: list[str] = None,
storage_type: str = "memory",
id: str = ATLAS_VISIBILITY_STORE_ID,
):
"""
Initialize a store to keep track of which elements should emit events when they are visible or not visible on the screen.
This should be used with ``pendo_visible_elements_callback`` which will access this store to monitor the associated HTML IDs.
:param html_ids: List of HTML IDs to initialize the store with. This list should be the list of HTML elements to be tracked for visibility events
:param storage_type:
:param id:
:return:
"""
html_ids = html_ids or []
return dcc.Store(
id=id,
data={
"html_ids": html_ids,
},
storage_type=storage_type,
)
[docs]
def control_store(
storage_type: str = "memory",
id: str = ATLAS_VISIBILITY_STORE_ID,
initial_data: list | dict = None,
):
"""
Initialize a store to keep track of controls and their current values on a page.
A separate callback should be implemented to update the control store when any control value changes
:param storage_type:
:param id: HTML ID to identify store in dash callbacks
:param initial_data: Initial state of store, typically a list
:return:
"""
data = initial_data or {}
return dcc.Store(
id=id,
data=data,
storage_type=storage_type,
)
[docs]
def pendo_initialize_callback(
app: Dash, user_store_id: str = ATLAS_USER_STORE_ID, debug: bool = ATLAS_DEBUG
):
"""
Client Side callback to initialize a Pendo session. Once the session is initialized other client-side callbacks
to send Pendo events can be executed. This should be used in conjunction with the ``user_store`` component. The data from
that component will be used in initializing the Pendo session
:param app: Dash app to register the client side callback
:param user_store_id: HTML ID of the user store that contains user/account information to initialize Pendo client. Must be associated with a ``dcc.Store`` component.
:param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development.
:return:
"""
with open(
os.path.join(LIBRARY_PATH, "templates/pendo/initializePendoCallback.js")
) as f:
clientside_script = (
JINJA_ENVIRONMENT.from_string(f.read())
.render(parameters={"debug": debug})
.strip()
)
app.clientside_callback(
clientside_script,
Output(user_store_id, "modified_timestamp"),
Input(user_store_id, "data"),
)
[docs]
def pendo_track_click_callback(
app: Dash,
button_id: str | dict,
page: str,
feature: str,
event: str,
additional_properties: dict = None,
store_ids: list[str | dict] = None,
data_context: str = DASHBOARD_INTERACTION,
debug: bool = ATLAS_DEBUG,
prevent_initial_call: bool = True,
):
"""
Client Side callback to emit a click event every time a ``ymc.Button` is clicked.
:param app: Dash app to register the client side callback
:param button_id: HTML ID of the corresponding button that is interacted with.
:param page: Pendo Page ID for the click action
:param feature: Pendo Feature ID for the click action
:param event: Pendo Event ID for the click action
:param additional_properties: Optional key value metadata pairs to pass into the Pendo event
:param store_ids: List of optional ``dcc.Store`` IDs to store metadata from in track events
:param data_context: Optional metadata to pass into the ``data_context`` field of pendo track events
:param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development.
:param prevent_initial_call: When ``True``, the client side callback will not be fired when associated component initially loads. Defaults to ``True``.
:return:
"""
additional_properties = additional_properties or {}
store_ids = store_ids or []
with open(os.path.join(LIBRARY_PATH, "templates/pendo/trackClick.js")) as f:
clientside_script = (
JINJA_ENVIRONMENT.from_string(f.read())
.render(
parameters={
"debug": debug,
"page": page,
"feature": feature,
"event": event,
"additional_properties": json.dumps(
additional_properties | {"html_id": button_id}
),
"store_ids": _map_stores_to_store_names(store_ids),
"data_context": data_context,
}
)
.strip()
)
app.clientside_callback(
clientside_script,
Input(button_id, "n_clicks"),
State(button_id, "id"),
*_map_stores_to_callback_state(store_ids),
prevent_initial_call=prevent_initial_call,
)
[docs]
def pendo_track_select_callback(
app: Dash,
select_id: str | dict,
page: str,
feature: str,
event: str,
additional_properties: dict = None,
store_ids: list[str | dict] = None,
is_multi_select: bool = False,
data_context: str = DASHBOARD_INTERACTION,
debug: bool = ATLAS_DEBUG,
prevent_initial_call: bool = True,
):
"""
Client Side callback to emit a click event every time a ``ymc.Select` or `ymc.MultiSelect` or ``ymc.DatePicker`` options are chosen.
:param app: Dash app to register the client side callback
:param select_id: HTML ID of the corresponding dropdown component that is interacted with.
:param page: Pendo Page ID for the click action
:param feature: Pendo Feature ID for the click action
:param event: Pendo Event ID for the click action
:param additional_properties: Optional key value metadata pairs to pass into the Pendo event
:param is_multi_select: Set to ``True`` if a ``ymc.MultiSelect`` is used
:param store_ids: List of optional ``dcc.Store`` IDs to store metadata from in track events
:param data_context: Optional metadata to pass into the ``data_context`` field of pendo track events
:param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development.
:param prevent_initial_call: When ``True``, the client side callback will not be fired when associated component initially loads. Defaults to ``True``.
:return:
"""
additional_properties = additional_properties or {}
store_ids = store_ids or []
with open(os.path.join(LIBRARY_PATH, "templates/pendo/trackSelect.js")) as f:
clientside_script = (
JINJA_ENVIRONMENT.from_string(f.read())
.render(
parameters={
"debug": debug,
"is_multi_select": is_multi_select,
"page": page,
"feature": feature,
"event": event,
"additional_properties": json.dumps(
additional_properties | {"html_id": select_id}
),
"store_ids": _map_stores_to_store_names(store_ids),
"data_context": data_context,
}
)
.strip()
)
app.clientside_callback(
clientside_script,
Input(select_id, "selectedValues" if is_multi_select else "value"),
State(select_id, "id"),
*_map_stores_to_callback_state(store_ids),
prevent_initial_call=prevent_initial_call,
)
[docs]
def pendo_track_select_range_callback(
app: Dash,
select_range_id: str | dict,
page: str,
feature: str,
event: str,
additional_properties: dict = None,
store_ids: list[str | dict] = None,
data_context: str = DASHBOARD_INTERACTION,
debug: bool = ATLAS_DEBUG,
prevent_initial_call: bool = True,
):
"""
Client Side callback to emit a click event every time a ``ymc.DateRangePicker` options are chosen.
:param app: Dash app to register the client side callback
:param select_range_id: HTML ID of the corresponding date range or range component that is interacted with.
:param page: Pendo Page ID for the click action
:param feature: Pendo Feature ID for the click action
:param event: Pendo Event ID for the click action
:param additional_properties: Optional key value metadata pairs to pass into the Pendo event
:param store_ids: List of optional ``dcc.Store`` IDs to store metadata from in track events
:param data_context: Optional metadata to pass into the ``data_context`` field of pendo track events
:param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development.
:param prevent_initial_call: When ``True``, the client side callback will not be fired when associated component initially loads. Defaults to ``True``.
:return:
"""
additional_properties = additional_properties or {}
store_ids = store_ids or []
with open(os.path.join(LIBRARY_PATH, "templates/pendo/trackRangeSelect.js")) as f:
clientside_script = (
JINJA_ENVIRONMENT.from_string(f.read())
.render(
parameters={
"debug": debug,
"page": page,
"feature": feature,
"event": event,
"additional_properties": json.dumps(
additional_properties | {"html_id": select_range_id}
),
"store_ids": _map_stores_to_store_names(store_ids),
"data_context": data_context,
}
)
.strip()
)
app.clientside_callback(
clientside_script,
[Input(select_range_id, "startDate"), Input(select_range_id, "endDate")],
State(select_range_id, "id"),
*_map_stores_to_callback_state(store_ids),
prevent_initial_call=prevent_initial_call,
)
[docs]
def pendo_track_visible_elements_callback(
app: Dash,
page: str,
feature: str,
event: str,
additional_properties: dict = None,
visibility_store_id: str = ATLAS_VISIBILITY_STORE_ID,
store_ids: list[str | dict] = None,
data_context: str = DASHBOARD_INTERACTION,
feature_mapping: dict = None,
debug: bool = ATLAS_DEBUG,
prevent_initial_call: bool = None,
):
"""
Client Side callback to emit an event every time the given html element(s) are visible on screen
and also when it exits the viewport
:param app: Dash app to register the client side callback
:param page: Pendo Page ID for the click action
:param feature: Pendo Feature ID for the click action
:param event: Pendo Event ID for the click action
:param additional_properties: Optional key value metadata pairs to pass into the Pendo event
:param visibility_store_id: Store ID that contains the list of HTML IDs to be observing.
:param store_ids: List of optional ``dcc.Store`` IDs to store metadata from in track events
:param data_context: Optional metadata to pass into the ``data_context`` field of pendo track events
:param feature_mapping: Optional mapping if different html IDs that are tracked are mapped to different pendo features. Keys are HTML IDs and the values are the corresponding feature(s). If not specified, the ``feature`` argument is used.
:param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development.
:param prevent_initial_call: When ``True``, the client side callback will not be fired when associated component initially loads. Defaults to ``None``.
:return:
"""
additional_properties = additional_properties or {}
store_ids = store_ids or []
feature_mapping = feature_mapping or {}
# Use b64 encoding to hash HTML IDs into JS safe values.
# This allows for nested dictionaries for pattern-matching callbacks to be passed client-side
encoded_feature_mapping = {}
for key, value in feature_mapping.items():
b64_encoded = base64.b64encode(key.encode("utf-8")).decode("utf-8")
encoded_feature_mapping[b64_encoded] = value
with open(os.path.join(LIBRARY_PATH, "templates/pendo/trackVisibility.js")) as f:
clientside_script = (
JINJA_ENVIRONMENT.from_string(f.read())
.render(
parameters={
"debug": debug,
"page": page,
"feature": feature,
"event": event,
"additional_properties": json.dumps(additional_properties),
"store_ids": _map_stores_to_store_names(store_ids),
"data_context": data_context,
"feature_mapping": json.dumps(encoded_feature_mapping),
}
)
.strip()
)
app.clientside_callback(
clientside_script,
Output(visibility_store_id, "modified_timestamp"),
Input(visibility_store_id, "data"),
*_map_stores_to_callback_state(store_ids),
prevent_initial_call=prevent_initial_call,
)
[docs]
def pendo_track_table_sort_callback(
app: Dash,
table_id: str | dict,
page: str,
feature: str,
event: str,
additional_properties: dict = None,
store_ids: list[str | dict] = None,
data_context: str = DASHBOARD_INTERACTION,
debug: bool = ATLAS_DEBUG,
prevent_initial_call: bool = True,
):
"""
Client Side callback to emit a click event every time a ``ymc.DataGrid` is sorted on a column.
:param app: Dash app to register the client side callback
:param table_id: HTML ID of the corresponding ``ymc.DataGrid`` that is interacted with.
:param page: Pendo Page ID for the click action
:param feature: Pendo Feature ID for the click action
:param event: Pendo Event ID for the click action
:param additional_properties: Optional key value metadata pairs to pass into the Pendo event
:param store_ids: List of optional ``dcc.Store`` IDs to store metadata from in track events
:param data_context: Optional metadata to pass into the ``data_context`` field of pendo track events
:param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development.
:param prevent_initial_call: When ``True``, the client side callback will not be fired when associated component initially loads. Defaults to ``True``.
:return:
"""
additional_properties = additional_properties or {}
store_ids = store_ids or []
with open(os.path.join(LIBRARY_PATH, "templates/pendo/trackTableSort.js")) as f:
clientside_script = (
JINJA_ENVIRONMENT.from_string(f.read())
.render(
parameters={
"debug": debug,
"page": page,
"feature": feature,
"event": event,
"additional_properties": json.dumps(
additional_properties | {"html_id": table_id}
),
"store_ids": _map_stores_to_store_names(store_ids),
"data_context": data_context,
}
)
.strip()
)
app.clientside_callback(
clientside_script,
Input(table_id, "sortModel"),
State(table_id, "id"),
*_map_stores_to_callback_state(store_ids),
prevent_initial_call=prevent_initial_call,
)
[docs]
def pendo_track_table_filter_callback(
app: Dash,
table_id: str | dict,
page: str,
feature: str,
event: str,
additional_properties: dict = None,
store_ids: list[str | dict] = None,
data_context: str = DASHBOARD_INTERACTION,
debug: bool = ATLAS_DEBUG,
prevent_initial_call: bool = True,
):
"""
Client Side callback to emit a click event every time a ``ymc.DataGrid` is filtered on a column.
:param app: Dash app to register the client side callback
:param table_id: HTML ID of the corresponding ``ymc.DataGrid`` that is interacted with.
:param page: Pendo Page ID for the click action
:param feature: Pendo Feature ID for the click action
:param event: Pendo Event ID for the click action
:param additional_properties: Optional key value metadata pairs to pass into the Pendo event
:param store_ids: List of optional ``dcc.Store`` IDs to store metadata from in track events
:param data_context: Optional metadata to pass into the ``data_context`` field of pendo track events
:param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development.
:param prevent_initial_call: When ``True``, the client side callback will not be fired when associated component initially loads. Defaults to ``True``.
:return:
"""
additional_properties = additional_properties or {}
store_ids = store_ids or []
with open(os.path.join(LIBRARY_PATH, "templates/pendo/trackTableFilter.js")) as f:
clientside_script = (
JINJA_ENVIRONMENT.from_string(f.read())
.render(
parameters={
"debug": debug,
"page": page,
"feature": feature,
"event": event,
"additional_properties": json.dumps(
additional_properties | {"html_id": table_id}
),
"store_ids": _map_stores_to_store_names(store_ids),
"data_context": data_context,
}
)
.strip()
)
app.clientside_callback(
clientside_script,
Input(table_id, "filterModel"),
State(table_id, "id"),
*_map_stores_to_callback_state(store_ids),
prevent_initial_call=prevent_initial_call,
)
[docs]
def pendo_track_page_load_callback(
app: Dash,
html_id: str | dict,
page: str,
feature: str,
event: str,
additional_properties: dict = None,
store_ids: list[str | dict] = None,
data_context: str = DASHBOARD_INTERACTION,
debug: bool = ATLAS_DEBUG,
prevent_initial_call: bool = None,
):
"""
Client Side callback to emit a load event once the application is fully loaded. This should generally be called once for a data app.
:param app: Dash app to register the client side callback
:param html_id: HTML ID of the corresponding ``element`` that triggers this callback
:param page: Pendo Page ID for the click action
:param feature: Pendo Feature ID for the click action
:param event: Pendo Event ID for the click action
:param additional_properties: Optional key value metadata pairs to pass into the Pendo event
:param store_ids: List of optional ``dcc.Store`` IDs to store metadata from in track events
:param data_context: Optional metadata to pass into the ``data_context`` field of pendo track events
:param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development.
:param prevent_initial_call: When ``True``, the client side callback will not be fired when associated component initially loads. Defaults to ``None``.
:return:
"""
additional_properties = additional_properties or {}
store_ids = store_ids or []
with open(os.path.join(LIBRARY_PATH, "templates/pendo/trackPageLoad.js")) as f:
clientside_script = (
JINJA_ENVIRONMENT.from_string(f.read())
.render(
parameters={
"debug": debug,
"page": page,
"feature": feature,
"event": event,
"additional_properties": json.dumps(
additional_properties | {"html_id": html_id}
),
"store_ids": _map_stores_to_store_names(store_ids),
"data_context": data_context,
}
)
.strip()
)
app.clientside_callback(
clientside_script,
Output(html_id, "id"),
Input(html_id, "children"),
State(html_id, "id"),
*_map_stores_to_callback_state(store_ids),
prevent_initial_call=prevent_initial_call,
)
def _map_stores_to_callback_state(store_ids: list[str]):
return [State(_id, "data") for _id in store_ids]
def _map_stores_to_store_names(store_ids: list[str | dict]):
return json.dumps(
[
f"{_id['type']}-{_id['index']}" if isinstance(_id, dict) else _id
for _id in store_ids
]
)