edit sources

This commit is contained in:
Abel Luck 2026-03-30 13:49:00 +02:00
parent 847aeae772
commit 328a70ff9b
7 changed files with 512 additions and 38 deletions

View file

@ -1,11 +1,12 @@
from repub.pages.dashboard import dashboard_page
from repub.pages.runs import execution_logs_page, runs_page
from repub.pages.shim import shim_page
from repub.pages.sources import create_source_page, sources_page
from repub.pages.sources import create_source_page, edit_source_page, sources_page
__all__ = [
"create_source_page",
"dashboard_page",
"edit_source_page",
"execution_logs_page",
"runs_page",
"shim_page",

View file

@ -41,6 +41,19 @@ PANGEA_CONTENT_TYPES = (
)
def _value(source: Mapping[str, object] | None, key: str, default: str = "") -> str:
if source is None:
return default
return str(source.get(key, default))
def _checked(source: Mapping[str, object] | None, key: str, default: bool) -> bool:
if source is None:
return default
value = source.get(key, default)
return bool(value)
def _source_row(source: Mapping[str, object]) -> tuple[Node, ...]:
return (
h.div[
@ -64,7 +77,9 @@ def _source_row(source: Mapping[str, object]) -> tuple[Node, ...]:
h.p(class_="mt-2 text-xs text-slate-500")[str(source["last_run"])],
],
h.div(class_="flex flex-nowrap items-center gap-3")[
inline_link(href="/sources/create", label="Edit", tone="amber"),
inline_link(
href=f"/sources/{source['slug']}/edit", label="Edit", tone="amber"
),
inline_link(href="/runs", label="View runs"),
],
)
@ -97,7 +112,27 @@ def sources_page(
)
def create_source_form(*, action_path: str = "/actions/sources/create") -> Renderable:
def source_form(
*,
mode: str,
action_path: str,
source: Mapping[str, object] | None = None,
) -> Renderable:
source_type = _value(source, "source_type", "pangea")
slug = _value(source, "slug")
title = "Source and job setup" if mode == "create" else "Edit source"
eyebrow = "Create" if mode == "create" else "Edit"
description = (
"Create the source and its paired job record."
if mode == "create"
else "Update the existing source and its paired job record."
)
status_label = "New source" if mode == "create" else "Existing source"
submit_label = "Create source" if mode == "create" else "Save changes"
initial_signals = "{sourceType: 'pangea'}"
if mode == "edit":
initial_signals = f"{{sourceType: '{source_type}', sourceSlug: '{slug}'}}"
return section_card(
content=(
h.div(
@ -106,20 +141,16 @@ def create_source_form(*, action_path: str = "/actions/sources/create") -> Rende
h.div[
h.p(
class_="text-xs font-semibold uppercase tracking-[0.22em] text-amber-600"
)["Create"],
h.h2(class_="mt-2 text-xl font-semibold text-slate-950")[
"Source and job setup"
],
h.p(class_="mt-2 max-w-3xl text-sm text-slate-600")[
"The create flow lives on its own page and creates the source plus its paired job record. This pass is visual only, but the fields already reflect the intended shape."
],
)[eyebrow],
h.h2(class_="mt-2 text-xl font-semibold text-slate-950")[title],
h.p(class_="mt-2 max-w-3xl text-sm text-slate-600")[description],
],
status_badge(label="New source", tone="scheduled"),
status_badge(label=status_label, tone="scheduled"),
],
h.form(
{
"data-signals": "{_formError: '', _formSuccess: ''}",
"data-signals__ifmissing": "{sourceType: 'pangea'}",
"data-signals__ifmissing": initial_signals,
"data-on:submit": f"@post('{action_path}')",
},
class_="mt-5 space-y-6",
@ -142,13 +173,16 @@ def create_source_form(*, action_path: str = "/actions/sources/create") -> Rende
input_field(
label="Source name",
field_id="source-name",
value=_value(source, "name"),
signal_name="sourceName",
),
input_field(
label="Slug",
field_id="source-slug",
value=slug,
help_text="Immutable after creation.",
signal_name="sourceSlug",
disabled=mode == "edit",
),
h.div[
h.label(
@ -161,8 +195,12 @@ def create_source_form(*, action_path: str = "/actions/sources/create") -> Rende
name="source-type",
class_="mt-2 block w-full rounded-2xl border-0 bg-white px-3.5 py-2.5 text-sm text-slate-900 shadow-sm ring-1 ring-slate-200 focus:outline-hidden focus:ring-2 focus:ring-amber-500",
)[
h.option(value="feed")["feed"],
h.option(value="pangea", selected=True)["pangea"],
h.option(value="feed", selected=source_type == "feed")[
"feed"
],
h.option(value="pangea", selected=source_type == "pangea")[
"pangea"
],
],
],
],
@ -185,6 +223,7 @@ def create_source_form(*, action_path: str = "/actions/sources/create") -> Rende
input_field(
label="Feed URL",
field_id="feed-url",
value=_value(source, "feed_url"),
placeholder="https://example.com/feed.xml",
signal_name="feedUrl",
),
@ -209,37 +248,39 @@ def create_source_form(*, action_path: str = "/actions/sources/create") -> Rende
input_field(
label="Pangea domain",
field_id="pangea-domain",
value=_value(source, "pangea_domain"),
signal_name="pangeaDomain",
),
input_field(
label="Category name",
field_id="pangea-category",
value=_value(source, "pangea_category"),
signal_name="pangeaCategory",
),
select_field(
label="Content format",
field_id="content-format",
options=PANGEA_CONTENT_FORMATS,
selected="MOBILE_3",
selected=_value(source, "content_format", "MOBILE_3"),
signal_name="contentFormat",
),
select_field(
label="Content type",
field_id="content-type",
options=PANGEA_CONTENT_TYPES,
selected="articles",
selected=_value(source, "content_type", "articles"),
signal_name="contentType",
),
input_field(
label="Max articles",
field_id="max-articles",
value="10",
value=_value(source, "max_articles", "10"),
signal_name="maxArticles",
),
input_field(
label="Oldest article (days)",
field_id="oldest-article",
value="3",
value=_value(source, "oldest_article", "3"),
signal_name="oldestArticle",
),
],
@ -248,25 +289,25 @@ def create_source_form(*, action_path: str = "/actions/sources/create") -> Rende
label="Only newest",
description="Limit Pangea syncs to the newest material available in the selected category.",
signal_name="onlyNewest",
checked=True,
checked=_checked(source, "only_newest", True),
),
toggle_field(
label="Include authors",
description="Carry author bylines into mirrored output where upstream data exists.",
signal_name="includeAuthors",
checked=True,
checked=_checked(source, "include_authors", True),
),
toggle_field(
label="Exclude media",
description="Skip image and media attachment mirroring for this source.",
signal_name="excludeMedia",
checked=False,
checked=_checked(source, "exclude_media", False),
),
toggle_field(
label="Include content",
description="Store article body content in mirrored output when the upstream provides it.",
signal_name="includeContent",
checked=True,
checked=_checked(source, "include_content", True),
),
],
],
@ -274,13 +315,17 @@ def create_source_form(*, action_path: str = "/actions/sources/create") -> Rende
textarea_field(
label="Notes",
field_id="source-notes",
value="",
value=_value(source, "notes"),
signal_name="sourceNotes",
),
textarea_field(
label="Spider arguments",
field_id="spider-arguments",
value="language=en\ndownload_media=true",
value=_value(
source,
"spider_arguments",
"language=en\ndownload_media=true",
),
signal_name="spiderArguments",
),
],
@ -300,31 +345,31 @@ def create_source_form(*, action_path: str = "/actions/sources/create") -> Rende
input_field(
label="Minute",
field_id="cron-minute",
value="*/30",
value=_value(source, "cron_minute", "*/30"),
signal_name="cronMinute",
),
input_field(
label="Hour",
field_id="cron-hour",
value="*",
value=_value(source, "cron_hour", "*"),
signal_name="cronHour",
),
input_field(
label="Day of month",
field_id="cron-day-of-month",
value="*",
value=_value(source, "cron_day_of_month", "*"),
signal_name="cronDayOfMonth",
),
input_field(
label="Day of week",
field_id="cron-day-of-week",
value="*",
value=_value(source, "cron_day_of_week", "*"),
signal_name="cronDayOfWeek",
),
input_field(
label="Month",
field_id="cron-month",
value="*",
value=_value(source, "cron_month", "*"),
signal_name="cronMonth",
),
],
@ -341,7 +386,7 @@ def create_source_form(*, action_path: str = "/actions/sources/create") -> Rende
label="Job enabled",
description="Scheduler will consider the new job immediately after creation.",
signal_name="jobEnabled",
checked=True,
checked=_checked(source, "enabled", True),
),
],
],
@ -353,7 +398,7 @@ def create_source_form(*, action_path: str = "/actions/sources/create") -> Rende
h.button(
type="submit",
class_="rounded-full bg-slate-950 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-slate-800",
)["Create source"],
)[submit_label],
],
],
)
@ -369,7 +414,27 @@ def create_source_page(*, action_path: str = "/actions/sources/create") -> Rende
current_path="/sources/create",
eyebrow="Source creation",
title="Create source",
description="Dedicated create page for the source form. The list page stays focused on scanning existing sources, while this page handles the new source and job configuration flow.",
description="Create a new source and its paired job configuration.",
actions=actions,
content=create_source_form(action_path=action_path),
content=source_form(mode="create", action_path=action_path),
)
def edit_source_page(
*,
slug: str,
source: Mapping[str, object],
action_path: str,
) -> Renderable:
actions = (
muted_action_link(href="/sources", label="Back to sources"),
header_action_link(href="/runs", label="View runs"),
)
return page_shell(
current_path=f"/sources/{slug}/edit",
eyebrow="Source editing",
title="Edit source",
description="Update an existing source and its paired job configuration.",
actions=actions,
content=source_form(mode="edit", action_path=action_path, source=source),
)