Refine publisher dashboard layout
This commit is contained in:
parent
2147d9c999
commit
813f19f355
8 changed files with 350 additions and 53 deletions
|
|
@ -291,29 +291,37 @@ def table_section(
|
|||
headers: tuple[str, ...],
|
||||
rows: tuple[tuple[Node, ...], ...],
|
||||
row_attrs: tuple[Mapping[str, str], ...] | None = None,
|
||||
header_classes: tuple[str, ...] | None = None,
|
||||
cell_classes: tuple[str, ...] | None = None,
|
||||
table_class: str = "relative w-full min-w-[64rem] divide-y divide-slate-200 table-auto",
|
||||
first_header_class: str | None = None,
|
||||
first_cell_class: str | None = None,
|
||||
actions: Node | None = None,
|
||||
) -> Renderable:
|
||||
def header_class(index: int) -> str:
|
||||
if header_classes is not None and index < len(header_classes):
|
||||
return header_classes[index]
|
||||
if index == 0 and first_header_class is not None:
|
||||
return first_header_class
|
||||
return "px-2.5 py-2.5 text-left text-xs font-semibold uppercase tracking-[0.18em] whitespace-nowrap text-slate-500 first:pl-3 sm:first:pl-4"
|
||||
|
||||
def cell_class(index: int) -> str:
|
||||
if cell_classes is not None and index < len(cell_classes):
|
||||
return cell_classes[index]
|
||||
if index == 0:
|
||||
return (
|
||||
first_cell_class
|
||||
or "py-3 pr-5 pl-3 text-sm font-medium text-slate-950 sm:pl-4"
|
||||
)
|
||||
return "px-2.5 py-3 align-top text-sm whitespace-nowrap text-slate-600"
|
||||
|
||||
def render_row(
|
||||
row: tuple[Node, ...], attrs: Mapping[str, str] | None = None
|
||||
) -> Renderable:
|
||||
first_cell, *other_cells = row
|
||||
row_attributes = dict(attrs or {})
|
||||
row_attributes["class"] = f"align-top {row_attributes.get('class', '')}".strip()
|
||||
return h.tr(row_attributes)[
|
||||
h.td(
|
||||
class_=(
|
||||
first_cell_class
|
||||
or "py-3 pr-5 pl-3 text-sm font-medium text-slate-950 sm:pl-4"
|
||||
)
|
||||
)[first_cell],
|
||||
(
|
||||
h.td(
|
||||
class_="px-2.5 py-3 align-top text-sm whitespace-nowrap text-slate-600"
|
||||
)[cell]
|
||||
for cell in other_cells
|
||||
),
|
||||
(h.td(class_=cell_class(index))[cell] for index, cell in enumerate(row)),
|
||||
]
|
||||
|
||||
body_rows: Node
|
||||
|
|
@ -362,19 +370,13 @@ def table_section(
|
|||
)
|
||||
)[
|
||||
h.div(class_="overflow-x-auto")[
|
||||
h.table(
|
||||
class_="relative w-full min-w-[64rem] divide-y divide-slate-200 table-auto"
|
||||
)[
|
||||
h.table(class_=table_class)[
|
||||
h.thead(class_="bg-stone-50")[
|
||||
h.tr[
|
||||
(
|
||||
h.th(
|
||||
scope="col",
|
||||
class_=(
|
||||
first_header_class
|
||||
if index == 0 and first_header_class is not None
|
||||
else "px-2.5 py-2.5 text-left text-xs font-semibold uppercase tracking-[0.18em] whitespace-nowrap text-slate-500 first:pl-3 sm:first:pl-4"
|
||||
),
|
||||
class_=header_class(index),
|
||||
)[header]
|
||||
for index, header in enumerate(headers)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -93,31 +93,73 @@ def operational_snapshot(*, snapshot: Mapping[str, str] | None = None) -> Render
|
|||
]
|
||||
|
||||
|
||||
def _source_feed_time(
|
||||
source_feed: Mapping[str, object],
|
||||
*,
|
||||
iso_key: str,
|
||||
label_key: str,
|
||||
class_name: str,
|
||||
data_attr: str | None = None,
|
||||
inline: bool = False,
|
||||
) -> Node:
|
||||
iso_value = source_feed.get(iso_key)
|
||||
label = str(source_feed[label_key])
|
||||
if iso_value is not None:
|
||||
attrs = {
|
||||
"datetime": str(iso_value),
|
||||
"title": str(iso_value),
|
||||
"class": class_name,
|
||||
}
|
||||
if data_attr is not None:
|
||||
attrs[data_attr] = str(iso_value)
|
||||
return h.time(attrs)[label]
|
||||
if inline:
|
||||
return h.span(class_=class_name)[label]
|
||||
return h.p(class_=class_name)[label]
|
||||
|
||||
|
||||
def _source_feed_row(
|
||||
source_feed: Mapping[str, object], *, show_feed_url: bool
|
||||
source_feed: Mapping[str, object], *, show_feed_url: bool, compact_mobile: bool
|
||||
) -> tuple[Node, ...]:
|
||||
last_updated_iso = source_feed.get("last_updated_iso")
|
||||
last_updated = (
|
||||
h.time(
|
||||
datetime=str(last_updated_iso),
|
||||
title=str(last_updated_iso),
|
||||
class_="font-medium text-slate-900",
|
||||
)[str(source_feed["last_updated"])]
|
||||
if last_updated_iso is not None
|
||||
else h.p(class_="font-medium text-slate-900")[str(source_feed["last_updated"])]
|
||||
last_updated = _source_feed_time(
|
||||
source_feed,
|
||||
iso_key="last_updated_iso",
|
||||
label_key="last_updated",
|
||||
class_name="font-medium text-slate-900",
|
||||
)
|
||||
next_run_iso = source_feed.get("next_run_at")
|
||||
next_run = (
|
||||
h.time(
|
||||
{
|
||||
"data-next-run-at": str(next_run_iso),
|
||||
"title": str(next_run_iso),
|
||||
},
|
||||
datetime=str(next_run_iso),
|
||||
class_="font-medium text-slate-900",
|
||||
)[str(source_feed["next_run"])]
|
||||
if next_run_iso is not None
|
||||
else h.p(class_="font-medium text-slate-900")[str(source_feed["next_run"])]
|
||||
next_run = _source_feed_time(
|
||||
source_feed,
|
||||
iso_key="next_run_at",
|
||||
label_key="next_run",
|
||||
class_name="font-medium text-slate-900",
|
||||
data_attr="data-next-run-at",
|
||||
)
|
||||
mobile_meta = (
|
||||
h.div(class_="mt-2 grid gap-1 text-xs text-slate-500 md:hidden")[
|
||||
h.p(class_="flex flex-wrap gap-x-1.5")[
|
||||
h.span(class_="font-medium text-slate-600")["Updated"],
|
||||
_source_feed_time(
|
||||
source_feed,
|
||||
iso_key="last_updated_iso",
|
||||
label_key="last_updated",
|
||||
class_name="font-medium text-slate-700",
|
||||
inline=True,
|
||||
),
|
||||
],
|
||||
h.p(class_="flex flex-wrap gap-x-1.5")[
|
||||
h.span(class_="font-medium text-slate-600")["Next"],
|
||||
_source_feed_time(
|
||||
source_feed,
|
||||
iso_key="next_run_at",
|
||||
label_key="next_run",
|
||||
class_name="font-medium text-slate-700",
|
||||
data_attr="data-next-run-at",
|
||||
inline=True,
|
||||
),
|
||||
],
|
||||
]
|
||||
if compact_mobile
|
||||
else None
|
||||
)
|
||||
feed_url_cells = (
|
||||
(
|
||||
|
|
@ -138,6 +180,7 @@ def _source_feed_row(
|
|||
h.p(class_="mt-0.5 font-mono text-[11px] text-slate-500")[
|
||||
str(source_feed["slug"])
|
||||
],
|
||||
mobile_meta,
|
||||
],
|
||||
*feed_url_cells,
|
||||
status_badge(
|
||||
|
|
@ -160,12 +203,40 @@ def published_feeds_table(
|
|||
manage_sources_href: str | None = "/admin/sources",
|
||||
show_heading: bool = True,
|
||||
show_feed_url: bool = True,
|
||||
compact_mobile: bool = False,
|
||||
) -> Renderable:
|
||||
rows = tuple(
|
||||
_source_feed_row(source_feed, show_feed_url=show_feed_url)
|
||||
_source_feed_row(
|
||||
source_feed,
|
||||
show_feed_url=show_feed_url,
|
||||
compact_mobile=compact_mobile,
|
||||
)
|
||||
for source_feed in (source_feeds or ())
|
||||
)
|
||||
feed_url_headers = ("Feed URL",) if show_feed_url else ()
|
||||
use_compact_columns = compact_mobile and not show_feed_url
|
||||
header_classes = (
|
||||
(
|
||||
"w-[48%] px-3 py-2.5 text-left text-xs font-semibold uppercase tracking-[0.18em] whitespace-nowrap text-slate-500 md:w-[32%] sm:pl-4",
|
||||
"w-[26%] px-2.5 py-2.5 text-left text-xs font-semibold uppercase tracking-[0.18em] whitespace-nowrap text-slate-500 md:w-[16%]",
|
||||
"hidden px-2.5 py-2.5 text-left text-xs font-semibold uppercase tracking-[0.18em] whitespace-nowrap text-slate-500 md:table-cell md:w-[22%]",
|
||||
"hidden px-2.5 py-2.5 text-left text-xs font-semibold uppercase tracking-[0.18em] whitespace-nowrap text-slate-500 md:table-cell md:w-[18%]",
|
||||
"w-[26%] px-2.5 py-2.5 text-right text-xs font-semibold uppercase tracking-[0.18em] whitespace-nowrap text-slate-500 md:w-[12%]",
|
||||
)
|
||||
if use_compact_columns
|
||||
else None
|
||||
)
|
||||
cell_classes = (
|
||||
(
|
||||
"w-[48%] py-3 pr-3 pl-3 text-sm font-medium text-slate-950 md:w-[32%] sm:pl-4",
|
||||
"w-[26%] px-2.5 py-3 align-top text-sm whitespace-nowrap text-slate-600 md:w-[16%]",
|
||||
"hidden px-2.5 py-3 align-top text-sm whitespace-nowrap text-slate-600 md:table-cell md:w-[22%]",
|
||||
"hidden px-2.5 py-3 align-top text-sm whitespace-nowrap text-slate-600 md:table-cell md:w-[18%]",
|
||||
"w-[26%] px-2.5 py-3 text-right align-top text-sm whitespace-nowrap text-slate-600 md:w-[12%]",
|
||||
)
|
||||
if use_compact_columns
|
||||
else None
|
||||
)
|
||||
return table_section(
|
||||
eyebrow="Published feeds" if show_heading else None,
|
||||
title="Published feeds" if show_heading else None,
|
||||
|
|
@ -179,6 +250,13 @@ def published_feeds_table(
|
|||
"Actions",
|
||||
),
|
||||
rows=rows,
|
||||
header_classes=header_classes,
|
||||
cell_classes=cell_classes,
|
||||
table_class=(
|
||||
"relative w-full min-w-0 divide-y divide-slate-200 table-fixed"
|
||||
if use_compact_columns
|
||||
else "relative w-full min-w-[64rem] divide-y divide-slate-200 table-auto"
|
||||
),
|
||||
actions=(
|
||||
muted_action_link(href=manage_sources_href, label="Manage sources")
|
||||
if manage_sources_href is not None
|
||||
|
|
|
|||
|
|
@ -47,11 +47,13 @@ def publisher_page(
|
|||
manage_sources_href=None,
|
||||
show_heading=False,
|
||||
show_feed_url=False,
|
||||
compact_mobile=True,
|
||||
),
|
||||
live_work_section(
|
||||
running_executions=running_executions,
|
||||
queued_executions=queued_executions,
|
||||
show_row_actions=False,
|
||||
compact_mobile=True,
|
||||
),
|
||||
relative_time_formatter_script(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -161,8 +161,11 @@ def _live_status_cell(
|
|||
status_tone: str,
|
||||
clock_label: str,
|
||||
calendar_label: Node,
|
||||
compact_mobile: bool = False,
|
||||
) -> Node:
|
||||
return h.div(class_="min-w-[10rem]")[
|
||||
return h.div(
|
||||
class_=("min-w-0 md:min-w-[10rem]" if compact_mobile else "min-w-[10rem]")
|
||||
)[
|
||||
h.div(class_="flex items-center gap-2")[
|
||||
h.span(class_="font-mono text-xs text-slate-500")[f"#{execution_id}"],
|
||||
h.span(
|
||||
|
|
@ -191,7 +194,10 @@ def _queue_row_attrs(execution: Mapping[str, object]) -> dict[str, str]:
|
|||
|
||||
|
||||
def _running_row(
|
||||
execution: Mapping[str, object], *, show_row_actions: bool = True
|
||||
execution: Mapping[str, object],
|
||||
*,
|
||||
show_row_actions: bool = True,
|
||||
compact_mobile: bool = False,
|
||||
) -> tuple[Node, ...]:
|
||||
started_at = _maybe_text(execution, "started_at_iso")
|
||||
started_at_label: Node = h.p(class_="truncate")[_text(execution, "started_at")]
|
||||
|
|
@ -205,6 +211,20 @@ def _running_row(
|
|||
class_="truncate",
|
||||
)[_text(execution, "started_at")]
|
||||
|
||||
mobile_details = (
|
||||
h.div(class_="mt-2 grid gap-1 text-xs text-slate-500 md:hidden")[
|
||||
h.p(class_="flex flex-wrap gap-x-1.5")[
|
||||
h.span(class_="font-medium text-slate-600")["Stats"],
|
||||
h.span[_text(execution, "stats")],
|
||||
],
|
||||
h.p(class_="flex flex-wrap gap-x-1.5")[
|
||||
h.span(class_="font-medium text-slate-600")["Worker"],
|
||||
h.span[_text(execution, "worker")],
|
||||
],
|
||||
]
|
||||
if compact_mobile
|
||||
else None
|
||||
)
|
||||
cells = (
|
||||
_live_status_cell(
|
||||
execution_id=_text(execution, "execution_id"),
|
||||
|
|
@ -213,12 +233,14 @@ def _running_row(
|
|||
clock_label=_maybe_text(execution, "duration")
|
||||
or _text(execution, "runtime"),
|
||||
calendar_label=started_at_label,
|
||||
compact_mobile=compact_mobile,
|
||||
),
|
||||
h.div[
|
||||
h.div(class_="font-semibold text-slate-950")[_text(execution, "source")],
|
||||
h.p(class_="mt-0.5 font-mono text-xs text-slate-500")[
|
||||
_text(execution, "slug")
|
||||
],
|
||||
mobile_details,
|
||||
],
|
||||
h.div(class_="max-w-xs whitespace-normal")[
|
||||
h.p(class_="font-medium text-slate-900")[_text(execution, "stats")],
|
||||
|
|
@ -245,7 +267,10 @@ def _running_row(
|
|||
|
||||
|
||||
def _queued_row(
|
||||
execution: Mapping[str, object], *, show_row_actions: bool = True
|
||||
execution: Mapping[str, object],
|
||||
*,
|
||||
show_row_actions: bool = True,
|
||||
compact_mobile: bool = False,
|
||||
) -> tuple[Node, ...]:
|
||||
queued_at = _maybe_text(execution, "queued_at_iso")
|
||||
queued_label: Node = h.p(class_="truncate")[_text(execution, "queued_at")]
|
||||
|
|
@ -259,6 +284,20 @@ def _queued_row(
|
|||
class_="truncate",
|
||||
)[_text(execution, "queued_at")]
|
||||
|
||||
mobile_details = (
|
||||
h.div(class_="mt-2 grid gap-1 text-xs text-slate-500 md:hidden")[
|
||||
h.p(class_="flex flex-wrap gap-x-1.5")[
|
||||
h.span(class_="font-medium text-slate-600")["Queued"],
|
||||
h.span[f"Queue position #{_text(execution, 'queue_position')}"],
|
||||
],
|
||||
h.p(class_="flex flex-wrap gap-x-1.5")[
|
||||
h.span(class_="font-medium text-slate-600")["State"],
|
||||
h.span["waiting for capacity"],
|
||||
],
|
||||
]
|
||||
if compact_mobile
|
||||
else None
|
||||
)
|
||||
cells = (
|
||||
_live_status_cell(
|
||||
execution_id=_text(execution, "execution_id"),
|
||||
|
|
@ -266,12 +305,14 @@ def _queued_row(
|
|||
status_tone="queued",
|
||||
clock_label="Waiting",
|
||||
calendar_label=queued_label,
|
||||
compact_mobile=compact_mobile,
|
||||
),
|
||||
h.div[
|
||||
h.div(class_="font-semibold text-slate-950")[_text(execution, "source")],
|
||||
h.p(class_="mt-0.5 font-mono text-xs text-slate-500")[
|
||||
_text(execution, "slug")
|
||||
],
|
||||
mobile_details,
|
||||
],
|
||||
h.div(class_="max-w-xs whitespace-normal")[
|
||||
h.p(class_="font-medium text-slate-900")[
|
||||
|
|
@ -525,21 +566,49 @@ def live_work_section(
|
|||
queued_executions: tuple[Mapping[str, object], ...] | None = None,
|
||||
actions: Node | None = None,
|
||||
show_row_actions: bool = True,
|
||||
compact_mobile: bool = False,
|
||||
) -> Renderable:
|
||||
running_items = running_executions or ()
|
||||
queued_items = queued_executions or ()
|
||||
running_rows = tuple(
|
||||
_running_row(execution, show_row_actions=show_row_actions)
|
||||
_running_row(
|
||||
execution,
|
||||
show_row_actions=show_row_actions,
|
||||
compact_mobile=compact_mobile,
|
||||
)
|
||||
for execution in running_items
|
||||
)
|
||||
queued_rows = tuple(
|
||||
_queued_row(execution, show_row_actions=show_row_actions)
|
||||
_queued_row(
|
||||
execution,
|
||||
show_row_actions=show_row_actions,
|
||||
compact_mobile=compact_mobile,
|
||||
)
|
||||
for execution in queued_items
|
||||
)
|
||||
live_rows = running_rows + queued_rows
|
||||
live_row_attrs = tuple(
|
||||
_queue_row_attrs(execution) for execution in running_items + queued_items
|
||||
)
|
||||
use_compact_columns = compact_mobile and not show_row_actions
|
||||
header_classes = (
|
||||
(
|
||||
"w-[34%] px-3 py-2.5 text-left text-xs font-semibold uppercase tracking-[0.18em] whitespace-nowrap text-slate-500 md:w-[24%] sm:pl-4",
|
||||
"w-[66%] px-2.5 py-2.5 text-left text-xs font-semibold uppercase tracking-[0.18em] whitespace-nowrap text-slate-500 md:w-[34%]",
|
||||
"hidden px-2.5 py-2.5 text-left text-xs font-semibold uppercase tracking-[0.18em] whitespace-nowrap text-slate-500 md:table-cell md:w-[42%]",
|
||||
)
|
||||
if use_compact_columns
|
||||
else None
|
||||
)
|
||||
cell_classes = (
|
||||
(
|
||||
"w-[34%] py-3 pr-3 pl-3 text-sm font-medium text-slate-950 md:w-[24%] sm:pl-4",
|
||||
"w-[66%] px-2.5 py-3 align-top text-sm whitespace-normal text-slate-600 md:w-[34%]",
|
||||
"hidden px-2.5 py-3 align-top text-sm whitespace-normal text-slate-600 md:table-cell md:w-[42%]",
|
||||
)
|
||||
if use_compact_columns
|
||||
else None
|
||||
)
|
||||
return table_section(
|
||||
eyebrow="Live work",
|
||||
title="Running jobs",
|
||||
|
|
@ -551,6 +620,13 @@ def live_work_section(
|
|||
),
|
||||
rows=live_rows,
|
||||
row_attrs=live_row_attrs,
|
||||
header_classes=header_classes,
|
||||
cell_classes=cell_classes,
|
||||
table_class=(
|
||||
"relative w-full min-w-0 divide-y divide-slate-200 table-fixed"
|
||||
if use_compact_columns
|
||||
else "relative w-full min-w-[64rem] divide-y divide-slate-200 table-auto"
|
||||
),
|
||||
actions=actions,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -320,6 +320,9 @@
|
|||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
.inline-flex {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
|
@ -367,6 +370,18 @@
|
|||
.w-40 {
|
||||
width: calc(var(--spacing) * 40);
|
||||
}
|
||||
.w-\[26\%\] {
|
||||
width: 26%;
|
||||
}
|
||||
.w-\[34\%\] {
|
||||
width: 34%;
|
||||
}
|
||||
.w-\[48\%\] {
|
||||
width: 48%;
|
||||
}
|
||||
.w-\[66\%\] {
|
||||
width: 66%;
|
||||
}
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -388,6 +403,9 @@
|
|||
.max-w-xs {
|
||||
max-width: var(--container-xs);
|
||||
}
|
||||
.min-w-0 {
|
||||
min-width: calc(var(--spacing) * 0);
|
||||
}
|
||||
.min-w-32 {
|
||||
min-width: calc(var(--spacing) * 32);
|
||||
}
|
||||
|
|
@ -409,6 +427,9 @@
|
|||
.table-auto {
|
||||
table-layout: auto;
|
||||
}
|
||||
.table-fixed {
|
||||
table-layout: fixed;
|
||||
}
|
||||
.translate-x-5 {
|
||||
--tw-translate-x: calc(var(--spacing) * 5);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
|
|
@ -455,6 +476,9 @@
|
|||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.gap-1 {
|
||||
gap: calc(var(--spacing) * 1);
|
||||
}
|
||||
.gap-1\.5 {
|
||||
gap: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
|
|
@ -501,6 +525,9 @@
|
|||
margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.gap-x-1\.5 {
|
||||
column-gap: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
.-space-x-px {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-x-reverse: 0;
|
||||
|
|
@ -732,6 +759,9 @@
|
|||
.pt-6 {
|
||||
padding-top: calc(var(--spacing) * 6);
|
||||
}
|
||||
.pr-3 {
|
||||
padding-right: calc(var(--spacing) * 3);
|
||||
}
|
||||
.pr-5 {
|
||||
padding-right: calc(var(--spacing) * 5);
|
||||
}
|
||||
|
|
@ -744,6 +774,9 @@
|
|||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
.align-top {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
|
@ -1144,6 +1177,61 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.md\:hidden {
|
||||
@media (width >= 48rem) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.md\:table-cell {
|
||||
@media (width >= 48rem) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
.md\:w-\[12\%\] {
|
||||
@media (width >= 48rem) {
|
||||
width: 12%;
|
||||
}
|
||||
}
|
||||
.md\:w-\[16\%\] {
|
||||
@media (width >= 48rem) {
|
||||
width: 16%;
|
||||
}
|
||||
}
|
||||
.md\:w-\[18\%\] {
|
||||
@media (width >= 48rem) {
|
||||
width: 18%;
|
||||
}
|
||||
}
|
||||
.md\:w-\[22\%\] {
|
||||
@media (width >= 48rem) {
|
||||
width: 22%;
|
||||
}
|
||||
}
|
||||
.md\:w-\[24\%\] {
|
||||
@media (width >= 48rem) {
|
||||
width: 24%;
|
||||
}
|
||||
}
|
||||
.md\:w-\[32\%\] {
|
||||
@media (width >= 48rem) {
|
||||
width: 32%;
|
||||
}
|
||||
}
|
||||
.md\:w-\[34\%\] {
|
||||
@media (width >= 48rem) {
|
||||
width: 34%;
|
||||
}
|
||||
}
|
||||
.md\:w-\[42\%\] {
|
||||
@media (width >= 48rem) {
|
||||
width: 42%;
|
||||
}
|
||||
}
|
||||
.md\:min-w-\[10rem\] {
|
||||
@media (width >= 48rem) {
|
||||
min-width: 10rem;
|
||||
}
|
||||
}
|
||||
.md\:grid-cols-2 {
|
||||
@media (width >= 48rem) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from datastar_py.quart import DatastarResponse
|
||||
from quart import Quart, Response
|
||||
from quart import Quart, Response, redirect
|
||||
|
||||
from repub.web.app import _page_patch_response, _shim_page_response, render_dashboard
|
||||
|
||||
|
|
@ -12,6 +12,11 @@ RouteGuard = Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[A
|
|||
|
||||
|
||||
def register_dashboard_routes(app: Quart, *, admin_required: RouteGuard) -> None:
|
||||
@app.get("/admin/")
|
||||
@admin_required
|
||||
async def admin_dashboard_trailing_slash() -> Response:
|
||||
return cast(Response, redirect("/admin"))
|
||||
|
||||
@app.get("/admin")
|
||||
@admin_required
|
||||
async def admin_dashboard_home() -> Response:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from datastar_py.quart import DatastarResponse
|
||||
from quart import Quart, Response
|
||||
from quart import Quart, Response, redirect
|
||||
|
||||
from repub.web.app import _page_patch_response, _shim_page_response, render_publisher
|
||||
|
||||
|
|
@ -17,6 +17,11 @@ def register_publisher_dashboard_routes(
|
|||
publisher_required: RouteGuard,
|
||||
admin_required: RouteGuard,
|
||||
) -> None:
|
||||
@app.get("/publisher/")
|
||||
@publisher_required
|
||||
async def publisher_trailing_slash() -> Response:
|
||||
return cast(Response, redirect("/publisher"))
|
||||
|
||||
@app.get("/publisher")
|
||||
@publisher_required
|
||||
async def publisher_home() -> Response:
|
||||
|
|
@ -36,6 +41,11 @@ def register_publisher_dashboard_routes(
|
|||
),
|
||||
)
|
||||
|
||||
@app.get("/admin/publisher/")
|
||||
@admin_required
|
||||
async def admin_publisher_trailing_slash() -> Response:
|
||||
return cast(Response, redirect("/admin/publisher"))
|
||||
|
||||
@app.get("/admin/publisher")
|
||||
@admin_required
|
||||
async def admin_publisher_home() -> Response:
|
||||
|
|
|
|||
|
|
@ -414,6 +414,28 @@ def test_root_get_redirects_to_publisher() -> None:
|
|||
asyncio.run(run())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("source_path", "target_path"),
|
||||
(
|
||||
("/admin/", "/admin"),
|
||||
("/publisher/", "/publisher"),
|
||||
("/admin/publisher/", "/admin/publisher"),
|
||||
),
|
||||
)
|
||||
def test_dashboard_trailing_slashes_redirect_to_canonical_paths(
|
||||
source_path: str, target_path: str
|
||||
) -> None:
|
||||
async def run() -> None:
|
||||
client = create_app().test_client()
|
||||
|
||||
response = await client.get(source_path)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.headers["Location"] == target_path
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_admin_get_serves_datastar_shim_with_admin_static_assets() -> None:
|
||||
async def run() -> None:
|
||||
client = create_app().test_client()
|
||||
|
|
@ -1153,6 +1175,12 @@ def test_render_publisher_shows_published_feeds_with_publisher_actions(
|
|||
assert 'href="/feeds/publisher-source/feed.rss"' not in body
|
||||
assert "Available" in body
|
||||
assert "Next run" in body
|
||||
assert "md:w-[32%]" in body
|
||||
assert "table-fixed" in body
|
||||
assert "min-w-0" in body
|
||||
assert "md:hidden" in body
|
||||
assert ">Updated<" in body
|
||||
assert ">Next<" in body
|
||||
assert "Disk usage" not in body
|
||||
assert f"/publisher/actions/jobs/{job.id}/run-now" in body
|
||||
assert f"/admin/actions/jobs/{job.id}/run-now" not in body
|
||||
|
|
@ -1222,6 +1250,14 @@ def test_render_publisher_shows_live_work_without_admin_controls(
|
|||
assert "Publisher running source" in body
|
||||
assert "Publisher queued source" in body
|
||||
assert "Queue position #1" in body
|
||||
assert "w-[34%]" in body
|
||||
assert "w-[66%]" in body
|
||||
assert "hidden" in body
|
||||
assert "md:table-cell" in body
|
||||
assert "md:min-w-[10rem]" in body
|
||||
assert ">Stats<" in body
|
||||
assert ">Worker<" in body
|
||||
assert ">Queued<" in body
|
||||
assert f"/publisher/job/{running_job.id}/execution/" not in body
|
||||
assert (
|
||||
f"/publisher/actions/executions/{int(running_execution.get_id())}/cancel"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue