-
Notifications
You must be signed in to change notification settings - Fork 3
Highlight selected page #91
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
Changes from 18 commits
130250e
8e90389
2a309c4
67d03df
97ed155
1d1d5c5
85f6b4a
5f68432
2125048
6c5d216
f51148c
440da0e
5a3e539
430aad7
d09ed56
2956f34
da842f7
08e412d
8b84b07
14d3982
8a96460
e36f320
0c6b95d
ab85ae3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ | |
import os | ||
import pathlib | ||
import weakref | ||
from dataclasses import dataclass, field | ||
from json import JSONDecodeError | ||
from typing import Dict, Iterable, Union, List, Set, Any, Optional, Tuple, Generic, Type, Callable | ||
|
||
|
@@ -42,6 +43,26 @@ | |
LastWillT = Tuple["WebApiObject[T]", T] | ||
|
||
|
||
@dataclass | ||
class MenuEntrySpec: | ||
"""Specification of one menu entry within the :class:`WebServer`""" | ||
# The name of the page (link target) | ||
page_name: Optional[str] = None | ||
# The label of the entry in the main menu | ||
label: Optional[str] = None | ||
# If given, the menu entry is prepended with the named icon | ||
icon: Optional[str] = None | ||
# Flag whether this menu item is active | ||
is_active: bool = False | ||
|
||
|
||
@dataclass | ||
class SubMenuEntrySpec(MenuEntrySpec): | ||
"""Specification of a sub menu entry within the :class:`WebServer`""" | ||
# List of submenus if any | ||
submenus: List[MenuEntrySpec] = field(default_factory=list) | ||
|
||
|
||
class WebServer(AbstractInterface): | ||
""" | ||
A SHC interface to provide the web user interface and a REST+websocket API for interacting with Connectable objects. | ||
|
@@ -81,14 +102,14 @@ def __init__(self, host: str, port: int, index_name: Optional[str] = None, root_ | |
self._associated_tasks: weakref.WeakSet[asyncio.Task] = weakref.WeakSet() | ||
# last will (object, value) per API websocket client (if set) | ||
self._api_ws_last_will: Dict[aiohttp.web.WebSocketResponse, LastWillT] = {} | ||
# data structure of the user interface's main menu | ||
# using class `MenuEntrySpec` and `SubMenuEntrySpec` as data structure for the user interface's main menu | ||
# The structure looks as follows: | ||
# [('Label', 'icon', 'page_name'), | ||
# ('Submenu label', None, [ | ||
# ('Label 2', 'icon', 'page_name2'), ... | ||
# [('Label', 'icon', 'page_name', 'is_active'), | ||
# ('Submenu label', 'icon', None, 'is_active' [ | ||
# ('Label 2', 'icon', 'page_name2', 'is_active'), ... | ||
# ]), | ||
# ...] | ||
self.ui_menu_entries: List[Tuple[str, Optional[str], Union[str, List[Tuple[str, Optional[str], str]]]]] = [] | ||
self.ui_menu_entries: List[MenuEntrySpec] = [] | ||
# List of all static js URLs to be included in the user interface pages | ||
self._js_files = [ | ||
"/static/pack/main.js", | ||
|
@@ -164,6 +185,9 @@ def page(self, name: str, title: Optional[str] = None, menu_entry: Union[bool, s | |
Create a new WebPage with a given name. | ||
|
||
If there is already a page with that name existing, it will be returned. | ||
For convenience you may create a menu entry pointing to the new page by specifying the | ||
`menu_entry` attribute. See :meth:`add_menu_entry` for creating plain menu entries. | ||
|
||
|
||
:param name: The `name` of the page, which is used in the page's URL to identify it. | ||
:param title: The title/heading of the page. If not given, the name is used. | ||
|
@@ -197,6 +221,7 @@ def add_menu_entry(self, page_name: str, label: str, icon: Optional[str] = None, | |
Create an entry for a named web UI page in the web UI's main navigation menu. | ||
|
||
The existence of the page is not checked, so menu entries can be created before the page has been created. | ||
See :meth:`page` for creating pages. | ||
|
||
:param page_name: The name of the page (link target) | ||
:param label: The label of the entry (or the submenu to place the entry in) in the main menu | ||
|
@@ -207,21 +232,26 @@ def add_menu_entry(self, page_name: str, label: str, icon: Optional[str] = None, | |
:raises ValueError: If there is already a menu entry with the same label (or a submenu entry with the same two | ||
labels) | ||
""" | ||
existing_entry = next((e for e in self.ui_menu_entries if e[0] == label), None) | ||
existing_entry: Optional[MenuEntrySpec] = next((e for e in self.ui_menu_entries if e.label == label), None) | ||
if not sub_label: | ||
if existing_entry: | ||
raise ValueError("UI main menu entry with label {} exists already. Contents: {}" | ||
.format(label, existing_entry[2])) | ||
self.ui_menu_entries.append((label, icon, page_name)) | ||
.format(label, existing_entry.page_name)) | ||
self.ui_menu_entries.append(MenuEntrySpec(label=label, icon=icon, page_name=page_name)) | ||
|
||
elif existing_entry: | ||
if not isinstance(existing_entry[2], list): | ||
if not isinstance(existing_entry, SubMenuEntrySpec): | ||
raise ValueError("Existing UI main menu entry with label {} is not a submenu but a link to page {}" | ||
.format(label, existing_entry[2])) | ||
existing_entry[2].append((sub_label, sub_icon, page_name)) | ||
|
||
.format(label, existing_entry.page_name)) | ||
existing_entry.submenus.append(MenuEntrySpec(label=sub_label, icon=sub_icon, page_name=page_name)) | ||
else: | ||
self.ui_menu_entries.append((label, icon, [(sub_label, sub_icon, page_name)])) | ||
self.ui_menu_entries.append( | ||
SubMenuEntrySpec( | ||
label=label, | ||
icon=icon, | ||
submenus=[MenuEntrySpec(label=sub_label, icon=sub_icon, page_name=page_name)], | ||
) | ||
) | ||
|
||
def api(self, type_: Type, name: str) -> "WebApiObject": | ||
""" | ||
|
@@ -291,11 +321,34 @@ async def _page_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Respo | |
|
||
html_title = self.title_formatter(page.title) | ||
template = jinja_env.get_template('page.htm') | ||
body = await template.render_async(title=page.title, segments=page.segments, menu=self.ui_menu_entries, | ||
root_url=self.root_url, js_files=self._js_files, css_files=self._css_files, | ||
server_token=id(self), html_title=html_title) | ||
self._mark_active_menu_items(page.name) | ||
body = await template.render_async( | ||
title=page.title, | ||
segments=page.segments, | ||
menu=self.ui_menu_entries, | ||
root_url=self.root_url, | ||
js_files=self._js_files, | ||
css_files=self._css_files, | ||
server_token=id(self), | ||
html_title=html_title, | ||
SubMenuEntrySpec=SubMenuEntrySpec, | ||
) | ||
return aiohttp.web.Response(body=body, content_type="text/html", charset='utf-8') | ||
|
||
def _mark_active_menu_items(self, page_name: str): | ||
"""Set menu items is_active flag if current page matches page_name/target link.""" | ||
for item in self.ui_menu_entries: | ||
if isinstance(item, SubMenuEntrySpec): | ||
item.is_active = False | ||
for sub_item in item.submenus: | ||
if page_name == sub_item.page_name: | ||
item.is_active = True | ||
sub_item.is_active = True | ||
else: | ||
sub_item.is_active = False | ||
else: | ||
item.is_active = page_name == item.page_name | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really don't like that this method changes the This may even result in concurrency issues, considering, that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, understood & fixed. |
||
|
||
async def _ui_websocket_handler(self, request: aiohttp.web.Request) -> aiohttp.web.WebSocketResponse: | ||
ws = aiohttp.web.WebSocketResponse() | ||
await ws.prepare(request) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment should be adjusted for the new classes, which replace the old tuple-based structure. I.f.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you're so right. Fixed.