Skip to content

Commit 672acef

Browse files
committed
feat(web)
- add help pages - refactor page footer generation - expand custom colors
1 parent 4f9fb34 commit 672acef

File tree

8 files changed

+221
-13
lines changed

8 files changed

+221
-13
lines changed

packages/cm-web/src/lsst/cmservice/web/pages/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .campaign_edit import CampaignClonePage, CampaignEditPage
2121
from .campaign_overview import CampaignOverviewPage
2222
from .canvas import CanvasScratchPage
23+
from .help import HelpPage
2324
from .node_detail import NodeDetailPage
2425

2526

@@ -32,6 +33,16 @@ async def campaign_overview_page(
3233
await page.render()
3334

3435

36+
@ui.page("/help", response_timeout=settings.timeout)
37+
@ui.page("/help/{_:path}", response_timeout=settings.timeout)
38+
async def cm_help_page(
39+
client_: Annotated[AsyncClient, Depends(CLIENT_FACTORY.get_aclient)],
40+
) -> None:
41+
await ui.context.client.connected()
42+
if page := await HelpPage(title="CM Service Help").setup(client_):
43+
await page.render()
44+
45+
3546
@ui.page("/campaign/{campaign_id}", response_timeout=settings.timeout)
3647
async def campaign_detail_page(
3748
campaign_id: str, client_: Annotated[AsyncClient, Depends(CLIENT_FACTORY.get_aclient)]

packages/cm-web/src/lsst/cmservice/web/pages/campaign_detail.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ def drawer_contents(self) -> None:
187187
{"cards": "Cards", "table": "Table"}, value="cards", on_change=self.handle_node_display_toggle
188188
).classes("flex-1").bind_value(self, "nodes_view", strict=False)
189189

190+
async def footer_contents(self) -> None:
191+
ui.label().classes("text-xs").bind_text_from(self, "campaign_id", strict=False)
192+
190193
async def setup(self, client_: AsyncClient | None = None, *, campaign_id: str = "") -> Self:
191194
"""Async method called at page creation. Subpages can override this
192195
method to perform data loading/prep, etc., before calling render().
@@ -260,9 +263,6 @@ async def create_content(self) -> None:
260263
ui.fab_action("ios_share", label="export", on_click=export_navigator).disable()
261264
ui.fab_action("copy_all", label="clone", on_click=clone_navigator)
262265

263-
with self.footer:
264-
ui.label().classes("text-xs").bind_text_from(self, "campaign_id", strict=False)
265-
266266
self.hide_spinner()
267267

268268
@ui.refreshable_method

packages/cm-web/src/lsst/cmservice/web/pages/campaign_edit.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,15 @@ async def create_content(self) -> None:
130130
});
131131
""")
132132

133-
with self.footer:
134-
ui.label().classes("text-xs").bind_text_from(self, "campaign_id", strict=False)
135-
136133
def drawer_contents(self) -> None:
137134
"""Right-side menu drawer contents rendered in a ui.column."""
138135
ui.button("Save", icon="save", on_click=self.handle_upload).classes("w-50")
139136
ui.button("Export", icon="save_alt", on_click=self.handle_export).classes("w-50")
140137
ui.button("Import", icon="file_upload").classes("w-50").disable()
141138

139+
async def footer_contents(self) -> None:
140+
ui.label().classes("text-xs").bind_text_from(self, "campaign_id", strict=False)
141+
142142
async def handle_node_edit(self, data: GenericEventArguments) -> None:
143143
"""Callback target for "edit" events emitted by nodes on the canvas."""
144144
step_id, step_name = data.args

packages/cm-web/src/lsst/cmservice/web/pages/common.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,21 @@ def apply_style() -> None:
6565
white=Palette.WHITE.light,
6666
dark=Palette.BLACK.light,
6767
dark_page=Palette.BLACK.dark,
68+
# Custom, Alt, and Deep Colors
69+
red=Palette.RED.light,
70+
deepred=Palette.RED.dark,
71+
orange=Palette.ORANGE.light,
72+
deeporange=Palette.ORANGE.dark,
73+
yellow=Palette.YELLOW.light,
74+
deepyellow=Palette.YELLOW.dark,
75+
green=Palette.GREEN.light,
76+
deepgreen=Palette.GREEN.dark,
77+
blue=Palette.BLUE.light,
78+
deepblue=Palette.BLUE.dark,
79+
indigo=Palette.INDIGO.light,
80+
deepindigo=Palette.INDIGO.dark,
81+
violet=Palette.VIOLET.light,
82+
deepviolet=Palette.VIOLET.dark,
6883
# Custom Colors for Statuses
6984
**{status.name: status.hex for status in StatusDecorators},
7085
)
@@ -82,6 +97,10 @@ def create_header(self) -> None:
8297
ui.space()
8398
ui.button(icon="menu", on_click=lambda: self.toggle_drawer()).props("flat color=white")
8499

100+
async def footer_contents(self) -> None:
101+
"""Hook method for subclasses to implement their own footer objects"""
102+
pass
103+
85104
def page_layout(self) -> None:
86105
self.create_drawer()
87106
with ui.header(elevated=True).classes(
@@ -96,11 +115,9 @@ def page_layout(self) -> None:
96115
) as self.content:
97116
pass
98117

99-
with ui.footer(bordered=False, elevated=True).classes(
118+
self.footer = ui.footer(bordered=False, elevated=True).classes(
100119
"h-[2rem] min-h-[2rem] items-center justify-between px-4 p-0"
101-
) as self.footer:
102-
for footer in self.footers:
103-
ui.label(footer).classes("text-xs m-0")
120+
)
104121

105122
self.overlay_div = ui.element("div").classes("hidden")
106123

@@ -117,6 +134,17 @@ async def render(self) -> None:
117134
Subclasses should use the `create_content()` method, which is called
118135
from within the content column's context manager.
119136
"""
137+
with self.footer:
138+
with ui.row().classes("items-center gap-2"):
139+
for footer in self.footers:
140+
ui.label(footer).classes("text-xs m-0")
141+
await self.footer_contents()
142+
143+
with ui.row().classes("items-center gap-2"):
144+
ui.button(icon="help_outline", on_click=lambda: ui.navigate.to("/help")).classes(
145+
"text-white bg-info m-0"
146+
).props("flat round size=sm")
147+
120148
with self.content:
121149
await self.create_content()
122150

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
from nicegui import ui
2+
3+
from ..settings import settings
4+
from .common import CMPage
5+
6+
7+
class HelpPage(CMPage):
8+
"""A page for general help and documentation. Each section is rendered
9+
as a subpage. Any nested help route not associated with a specific subpage
10+
is served by the main help landing page instead of producing a 404 error.
11+
"""
12+
13+
def drawer_contents(self) -> None: ...
14+
15+
async def create_content(self) -> None:
16+
with ui.button_group().classes("w-full h-[4rem]"):
17+
ui.button("Help", on_click=lambda: ui.navigate.to("/help"), color="secondary").classes("flex-1")
18+
ui.button("Step", on_click=lambda: ui.navigate.to("/help/step"), color="secondary").classes(
19+
"flex-1"
20+
)
21+
ui.button("BPS", on_click=lambda: ui.navigate.to("/help/bps"), color="secondary").classes(
22+
"flex-1"
23+
)
24+
ui.button("Butler", on_click=lambda: ui.navigate.to("/help/butler"), color="secondary").classes(
25+
"flex-1"
26+
)
27+
ui.button("LSST", on_click=lambda: ui.navigate.to("/help/lsst"), color="secondary").classes(
28+
"flex-1"
29+
)
30+
ui.button("WMS", on_click=lambda: ui.navigate.to("/help/wms"), color="secondary").classes(
31+
"flex-1"
32+
)
33+
ui.button("Site", on_click=lambda: ui.navigate.to("/help/site"), color="secondary").classes(
34+
"flex-1"
35+
)
36+
37+
with ui.element("div").classes("w-full h-full overflow-x-hidden overflow-y-auto"):
38+
ui.sub_pages(
39+
{
40+
"/help": self.landing_help,
41+
"/help/step": self.step_help,
42+
"/help/bps": self.bps_help,
43+
"/help/butler": self.butler_help,
44+
"/help/lsst": self.lsst_help,
45+
"/help/wms": self.wms_help,
46+
"/help/site": self.site_help,
47+
},
48+
show_404=False,
49+
).classes("w-full h-full")
50+
51+
async def landing_help(self) -> None:
52+
"""The main landing page for the help page network. Served by the
53+
`/help` route and any `/help/*` route not otherwise associated with a
54+
subpage.
55+
"""
56+
ui.markdown("""
57+
# Main Help
58+
Choose a help section from the options above.
59+
""")
60+
61+
with ui.expansion(
62+
text="Glossary", caption="A brief glossary of terms used throughout CM Service."
63+
).classes("w-full"):
64+
with (
65+
ui.element("div")
66+
.classes("w-full overflow-x-auto")
67+
.style("column-count: 2; column-gap: 2rem;")
68+
):
69+
await self.markdown_help_section("cmglossary.md")
70+
71+
with ui.expansion(
72+
text="Page Overview", caption="An overview of Pages available in CM Service."
73+
).classes("w-full"):
74+
with (
75+
ui.element("div")
76+
.classes("w-full overflow-x-auto")
77+
.style("column-count: 2; column-gap: 2rem;")
78+
):
79+
await self.markdown_help_section("cmpages.md")
80+
81+
async def bps_help(self) -> None:
82+
ui.markdown("""
83+
# BPS Help
84+
""")
85+
86+
await self.manifest_spec_iframe("bps")
87+
88+
async def step_help(self) -> None:
89+
ui.markdown("""
90+
# Step Help
91+
""")
92+
93+
await self.manifest_spec_iframe("step")
94+
95+
async def butler_help(self) -> None:
96+
ui.markdown("""
97+
# Butler Help
98+
""")
99+
100+
await self.manifest_spec_iframe("butler")
101+
102+
async def lsst_help(self) -> None:
103+
ui.markdown("""
104+
# LSST Help
105+
""")
106+
107+
await self.manifest_spec_iframe("lsst")
108+
109+
async def wms_help(self) -> None:
110+
ui.markdown("""
111+
# WMS Help
112+
""")
113+
114+
await self.manifest_spec_iframe("wms")
115+
116+
async def site_help(self) -> None:
117+
ui.markdown("""
118+
# Site Help
119+
""")
120+
121+
await self.manifest_spec_iframe("site")
122+
123+
async def manifest_spec_iframe(self, kind: str) -> None:
124+
"""Adds an iframe element into which is loaded the Manifest Spec html
125+
reference page. This page is generated from pydantic models as
126+
jsonschema then HTML is generated from that.
127+
"""
128+
ui.element("iframe").props(f"src='/static/docs/{kind}_spec.html'").classes("w-full h-full")
129+
130+
async def markdown_help_section(self, md: str) -> None:
131+
"""Adds a markdown element with the contents of the markdown file
132+
referenced by the `md` argument. This file must exist in the "markdown"
133+
directory relative to the static content location indicated by the
134+
`settings.static_dir` Path.
135+
"""
136+
137+
markdown_content = (settings.static_dir / "markdown" / md).read_text()
138+
ui.markdown(markdown_content, sanitize=True).classes("w-full h-full")

packages/cm-web/src/lsst/cmservice/web/pages/node_detail.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ class NodeDetailPage(CMPage[NodeDetailPageModel]):
4444

4545
def drawer_contents(self) -> None: ...
4646

47+
async def footer_contents(self) -> None:
48+
ui.label().classes("text-xs").bind_text_from(self, "node_id", strict=False)
49+
4750
async def setup(self, client_: AsyncClient | None = None, *, node_id: str = "") -> Self:
4851
"""Data intensive setup method that fetches the full node description
4952
from CM API. Run at page setup to populate the page `model` with the
@@ -62,9 +65,6 @@ async def setup(self, client_: AsyncClient | None = None, *, node_id: str = "")
6265
"logs": data["logs"],
6366
}
6467

65-
with self.footer:
66-
ui.label().classes("text-xs").bind_text_from(self, "node_id", strict=False)
67-
6868
self.breadcrumbs.append(data["node"]["name"])
6969
self.create_header.refresh()
7070
return self
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
- `campaign`: A complete end-to-end data processing workflow.
2+
- `manifest`: A kind-specific configuration document that defines the parameters available to a campaign node, step, or group.
3+
Each kind of manifest supports difference configuration parameters and controls the generation of artifacts used by BPS and other launchers. A manifest may be configured at the group, step, campaign, or library level. See `configuration chain`.
4+
- `library`: A generic, "global" manifest of a particular kind used as a fallback of last resort in a configuration chain.
5+
A Library manifest is always "version 0" and are read-only.
6+
- `graph`
7+
- `node`: A Node is a single atomic unit in a campaign graph.
8+
A Node is usually a step or a group, but can also be a breakpoint or start/end marker.
9+
Additional types of nodes may be introduced to CM Service to perform more specific operations as a campaign progresses.
10+
- `step`: A Step is a graph node that specifically represents a unit of LSST Pipeline work -- which could be a pipeline task, step, or stage -- any unit of pipeline work representable by a single "Pipeline YAML" declaration can be a `step`.
11+
- `group`: Every `step` in a campaign graph is broken out into one or more `group`s.
12+
A group inherits all its configuration from the step that generated it, including the "Pipeline YAML" declaration, and differs only in the Butler data query that constrains the data IDs used by the group.
13+
Groups are the unit of parallelization for executing campaign graphs.
14+
- `collect`: Since every `step` "fans out" into one or more `groups`, the `collect` Node is the "fan in" operation that builds Butler collections that gather together all the output run collections of its ancestor groups. A `collect` node also serves as a checkpoint in the campaign graph, since every ancestor group must be completed before the `collect` node can start.
15+
- `configuration chain`: Every group's configuration is generated by a "chain" of manifests. For every configuration parameter a group uses from a manifest, each manifest in the "chain" is checked until the parameter is found. The ordering of this "chain" is from most specific to most general: group -> step -> campaign -> library.
16+
- `launcher`: A launcher is the mechanism CM Service uses to perform work. For example, to run a "bps submit" command for a group, CM Service first writes a BASH script artifact that calls "bps submit" and then uses a `launcher` to run that script. A common launcher is the HTCondor launcher, which submits the script as a job to an HTCondor Schedd host in the local universe.
17+
- `version`: Many objects in CM Service are versioned, especially manifests and nodes. Versions provide a history and provenance for tracking changes made to a campaign's elements. CM Service does not support branching versions, so the latest version of any object is always the newest, but nodes track in their metadata which version of a manifest was used to produce the node's configuration.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
- Every page in CM Service has a header, a side panel menu, and a footer.
2+
Each page may display different information in these sections, but a "help" button is always present in the footer, and will bring you back to this help section.
3+
Similarly, every page header has a "Campaign Management" label that is a link back to the Campaign Overview page.
4+
5+
- *Campaign Overview*. The main home page of CM Service.
6+
This page shows a card for each campaign known to CM Service.
7+
On each card is an Avatar, the campaign name and status, a histogram of node-statuses, and a pause/resume switch.
8+
Additional buttons allow you to "favorite" or "hide" a campaign.
9+
You may also "clone" a campaign to make a copy of its graph and configuration, or "export" the campaign for offline use.
10+
11+
- *Campaign Detail*. Each card on the *campaign overview* page is linked to a *Campaign Detail* page for that campaign.
12+
On this detail page, you can access a Mermaid rendering of the campaign graph, review gauges that display the campaign progress, review and edit any Campaign Manifests, and review and edit any Nodes in the campaign.
13+
The collection of campaign nodes is displayed as a set of Node Detail cards or a simplified Table, with a switch between the two views available in the side panel menu.
14+
Node Detail cards have richer content but are consequently more visually complex; the table view is simpler but has a restricted set of available operations.

0 commit comments

Comments
 (0)