Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor the dynamic sidebar links (menuLinks) to be extendable to control all dashboard links #130

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# See LICENSE file for licensing details.

options:
additional-sidebar-links:
additional-menu-links:
type: string
default: ""
description: >
Expand All @@ -11,9 +11,9 @@ options:
{text: <string>, link: <string>, type: item, icon: <string>}. For example:
'[{text: Another runs, link: /runs, type: item, icon: assessment}]'.
This is typically easiest to configure from a file - define a YAML or JSON file, then pass to Juju via
`juju config kubeflow-dashboard additional-sidebar-links=@myfile.yaml`.
`juju config kubeflow-dashboard additional-menu-links=@myfile.yaml`.
See [upstream docs](https://www.kubeflow.org/docs/components/central-dash/customizing-menu/) for more details on
the sidebar and link format.
the menu and link format.
dashboard-configmap:
type: string
default: centraldashboard-config
Expand All @@ -26,7 +26,7 @@ options:
type: boolean
default: true
description: Whether to enable the registration flow on sign-in
sidebar-link-order:
menu-link-order:
type: string
default: '["Notebooks", "Experiments (AutoML)", "Experiments (KFP)", "Pipelines", "Runs", "Recurring Runs", "Volumes", "Tensorboards"]'
description: >
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,57 @@
"""# KubeflowDashboardSidebar Library
This library implements data transfer for the kubeflow_dashboard_sidebar
interface used by Kubeflow Dashboard to implement the sidebar relation. This
"""KubeflowDashboardLinks Library
This library implements data transfer for the kubeflow_dashboard_links
interface used by Kubeflow Dashboard to implement the links relation. This
relation enables applications to request a link on the Kubeflow Dashboard
sidebar dynamically.
dynamically.

To enable an application to add a link to Kubeflow Dashboard's sidebar, use
the KubeflowDashboardSidebarRequirer and SidebarItem classes included here as
To enable an application to add a link to Kubeflow Dashboard, use
the KubeflowDashboardLinksRequirer and DashboardLink classes included here as
shown below. No additional action is required within the charm. On
establishing the relation, the data will be sent to Kubeflow Dashboard to add
the link. The link will be removed if the relation is broken.
the links. The links will be removed if the relation is broken.

## Getting Started

To get started using the library, fetch the library with `charmcraft`.

```shell
cd some-charm
charmcraft fetch-lib charms.kubeflow_dashboard.v0.kubeflow_dashboard_sidebar
charmcraft fetch-lib charms.kubeflow_dashboard.v0.kubeflow_dashboard_links
```

Then in your charm, do:

```python
from charms.kubeflow_dashboard.v0.kubeflow_dashboard_sidebar import (
KubeflowDashboardSidebarRequirer,
SidebarItem,
from charms.kubeflow_dashboard.v0.kubeflow_dashboard_links import (
KubeflowDashboardLinksRequirer,
DashboardLink,
)
# ...

SIDEBAR_ITEMS = [
SidebarItem(
DASHBOARD_LINKS = [
DashboardLink(
text="Example Relative Link",
link="/relative-link",
type="item",
icon="assessment"
icon="assessment",
location="sidebar",
),
SidebarItem(
DashboardLink(
text="Example External Link",
link="https://charmed-kubeflow.io/docs",
type="item",
icon="assessment"
icon="assessment",
location="sidebar-external"
),
]

class SomeCharm(CharmBase):
def __init__(self, *args):
# ...
self.kubeflow_dashboard_sidebar = KubeflowDashboardSidebarRequirer(
self.kubeflow_dashboard_links = KubeflowDashboardLinksRequirer(
charm=self,
relation_name="sidebar", # use whatever you call the relation in your metadata.yaml
SIDEBAR_ITEMS
relation_name="links", # use whatever you call the relation in your metadata.yaml
DASHBOARD_LINKS
)
# ...
```
Expand All @@ -73,59 +75,63 @@ def __init__(self, *args):

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 1
LIBPATCH = 2


SIDEBAR_ITEMS_FIELD = "sidebar_items"
DASHBOARD_LINKS_FIELD = "dashboard_links"


@dataclass
class SidebarItem:
"""Representation of a Kubeflow Dashboard sidebar entry.
class DashboardLink:
"""Representation of a Kubeflow Dashboard Link entry.

See https://www.kubeflow.org/docs/components/central-dash/customizing-menu/ for more details.

Args:
text: The text shown in the sidebar
link: The relative link within the host (eg: /runs, not http://.../runs)
text: The text shown for the link
link: The link (a relative link for `location=sidebar` or `location=quick`, eg: `/mlflow`, or a full URL for
other locations, eg: http://my-website.com)
type: A type of sidebar entry (typically, "item")
icon: An icon for the link, from
https://kevingleason.me/Polymer-Todo/bower_components/iron-icons/demo/index.html
location: Link's location on the dashboard. One of `sidebar`, `sidebar_external`, `quick`,
and `documentation`.
"""
text: str
link: str
type: str # noqa: A003
icon: str
# location: str # To be implemented


class KubeflowDashboardSidebarDataUpdatedEvent(RelationEvent):
"""Indicates the Kubeflow Dashboard Sidebar data was updated."""
class KubeflowDashboardLinksUpdatedEvent(RelationEvent):
"""Indicates the Kubeflow Dashboard link data was updated."""


class KubeflowDashboardidebarEvents(ObjectEvents):
"""Events for the Kubeflow Dashboard Sidebar library."""
class KubeflowDashboardLinksEvents(ObjectEvents):
"""Events for the Kubeflow Dashboard Links library."""

data_updated = EventSource(KubeflowDashboardSidebarDataUpdatedEvent)
updated = EventSource(KubeflowDashboardLinksUpdatedEvent)


class KubeflowDashboardSidebarProvider(Object):
class KubeflowDashboardLinksProvider(Object):
"""Relation manager for the Provider side of the Kubeflow Dashboard Sidebar relation.."""
on = KubeflowDashboardidebarEvents()
on = KubeflowDashboardLinksEvents()

def __init__(
self,
charm: CharmBase,
relation_name: str,
refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = None,
):
"""Relation manager for the Provider side of the Kubeflow Dashboard Sidebar relation.
"""Relation manager for the Provider side of the Kubeflow Dashboard Links relation.

This relation manager subscribes to:
* on[relation_name].relation_changed
* any events provided in refresh_event

This library emits:
* KubeflowDashboardSidebarDataUpdatedEvent:
* KubeflowDashboardLinksUpdatedEvent:
when data received on the relation is updated

TODO: Should this class automatically subscribe to events, or should it optionally do that.
Expand Down Expand Up @@ -158,16 +164,16 @@ def __init__(
for evt in refresh_event:
self.framework.observe(evt, self._on_relation_changed)

def get_sidebar_items(self, omit_breaking_app: bool = True) -> List[SidebarItem]:
"""Returns a list of all SidebarItems from related Applications.
def get_dashboard_links(self, omit_breaking_app: bool = True) -> List[DashboardLink]:
"""Returns a list of all DashboardItems from related Applications.

Args:
omit_breaking_app: If True and this is called during a sidebar-relation-broken event,
omit_breaking_app: If True and this is called during a link-relation-broken event,
the remote app's data will be omitted. For more context, see:
https://github.com/canonical/kubeflow-dashboard-operator/issues/124

Returns:
List of SidebarItems defining the dashboard sidebar for all related applications.
List of DashboardLinks defining the dashboard links for all related applications.
"""
# If this is a relation-broken event, remove the departing app from the relation data if
# it exists. See: https://github.com/canonical/kubeflow-dashboard-operator/issues/124
Expand All @@ -178,56 +184,56 @@ def get_sidebar_items(self, omit_breaking_app: bool = True) -> List[SidebarItem]

if other_app_to_skip:
logger.debug(
f"get_sidebar_items executed during a relation-broken event. Return will"
f"exclude sidebar_items from other app named '{other_app_to_skip}'. "
f"get_dashboard_links executed during a relation-broken event. Return will"
f"exclude dashboard_links from other app named '{other_app_to_skip}'. "
)

sidebar_items = []
sidebar_relation = self.model.relations[self._relation_name]
for relation in sidebar_relation:
dashboard_links = []
dashboard_link_relation = self.model.relations[self._relation_name]
for relation in dashboard_link_relation:
other_app = relation.app
if other_app.name == other_app_to_skip:
# Skip this app because it is leaving a broken relation
continue
json_data = relation.data[other_app].get(SIDEBAR_ITEMS_FIELD, "{}")
json_data = relation.data[other_app].get(DASHBOARD_LINKS_FIELD, "{}")
dict_data = json.loads(json_data)
sidebar_items.extend([SidebarItem(**item) for item in dict_data])
dashboard_links.extend([DashboardLink(**item) for item in dict_data])

return sidebar_items
return dashboard_links

def get_sidebar_items_as_json(self, omit_breaking_app: bool = True) -> str:
"""Returns a JSON string of all SidebarItems from related Applications.
def get_dashboard_links_as_json(self, omit_breaking_app: bool = True) -> str:
"""Returns a JSON string of all DashboardItems from related Applications.

Args:
omit_breaking_app: If True and this is called during a sidebar-relation-broken event,
omit_breaking_app: If True and this is called during a links-relation-broken event,
the remote app's data will be omitted. For more context, see:
https://github.com/canonical/kubeflow-dashboard-operator/issues/124

Returns:
JSON string of all SidebarItems for all related applications, each as dicts.
JSON string of all DashboardLinks for all related applications, each as dicts.
"""
return sidebar_items_to_json(self.get_sidebar_items(omit_breaking_app=omit_breaking_app))
return dashboard_links_to_json(self.get_dashboard_links(omit_breaking_app=omit_breaking_app))

def _on_relation_changed(self, event):
"""Handler for relation-changed event for this relation."""
self.on.data_updated.emit(event.relation)
self.on.updated.emit(event.relation)

def _on_relation_broken(self, event: BoundEvent):
"""Handler for relation-broken event for this relation."""
self.on.data_updated.emit(event.relation)
self.on.updated.emit(event.relation)


class KubeflowDashboardSidebarRequirer(Object):
"""Relation manager for the Requirer side of the Kubeflow Dashboard Sidebar relation."""
class KubeflowDashboardLinksRequirer(Object):
"""Relation manager for the Requirer side of the Kubeflow Dashboard Links relation."""
def __init__(
self,
charm: CharmBase,
relation_name: str,
sidebar_items: List[SidebarItem],
dashboard_links: List[DashboardLink],
refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = None,
):
"""
Relation manager for the Requirer side of the Kubeflow Dashboard Sidebar relation.
Relation manager for the Requirer side of the Kubeflow Dashboard Link relation.

This relation manager subscribes to:
* on.leader_elected: because only the leader is allowed to provide this data, and
Expand All @@ -246,14 +252,14 @@ def __init__(
Args:
charm: Charm this relation is being used by
relation_name: Name of this relation (from metadata.yaml)
sidebar_items: List of SidebarItem objects to send over the relation
dashboard_links: List of DashboardLink objects to send over the relation
refresh_event: List of BoundEvents that this manager should handle. Use this to update
the data sent on this relation on demand.
"""
super().__init__(charm, relation_name)
self._charm = charm
self._relation_name = relation_name
self._sidebar_items = sidebar_items
self._dashboard_links = dashboard_links

self.framework.observe(self._charm.on.leader_elected, self._on_send_data)

Expand All @@ -273,7 +279,7 @@ def _on_send_data(self, event: EventBase):
"""Handles any event where we should send data to the relation."""
if not self._charm.model.unit.is_leader():
logger.info(
"KubeflowDashboardSidebarRequirer handled send_data event when it is not the "
"KubeflowDashboardLinksRequirer handled send_data event when it is not the "
"leader. Skipping event - no data sent."
)
return
Expand All @@ -282,8 +288,8 @@ def _on_send_data(self, event: EventBase):

for relation in relations:
relation_data = relation.data[self._charm.app]
sidebar_items_as_json = json.dumps([asdict(item) for item in self._sidebar_items])
relation_data.update({SIDEBAR_ITEMS_FIELD: sidebar_items_as_json})
dashboard_links_as_json = json.dumps([asdict(item) for item in self._dashboard_links])
relation_data.update({DASHBOARD_LINKS_FIELD: dashboard_links_as_json})


def get_name_of_breaking_app(relation_name: str) -> Optional[str]:
Expand All @@ -306,8 +312,8 @@ def get_name_of_breaking_app(relation_name: str) -> Optional[str]:
return os.environ.get("JUJU_REMOTE_APP", None)


def sidebar_items_to_json(sidebar_items: List[SidebarItem]) -> str:
def dashboard_links_to_json(dashboard_links: List[DashboardLink]) -> str:
"""Returns a list of SidebarItems as a JSON string."""
return json.dumps(
[asdict(sidebar_item) for sidebar_item in sidebar_items]
[asdict(dashboard_link) for dashboard_link in dashboard_links]
)
4 changes: 2 additions & 2 deletions metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ resources:
auto-fetch: true
upstream-source: docker.io/kubeflownotebookswg/centraldashboard:v1.7.0
provides:
sidebar:
interface: kubeflow_dashboard_sidebar
links:
interface: kubeflow_dashboard_links
requires:
ingress:
interface: ingress
Expand Down
Loading
Loading