create sources in memory
This commit is contained in:
parent
9e826fcee8
commit
06066c2394
4 changed files with 392 additions and 46 deletions
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
import htpy as h
|
||||
from htpy import Node, Renderable
|
||||
|
||||
|
|
@ -17,7 +19,28 @@ from repub.components import (
|
|||
toggle_field,
|
||||
)
|
||||
|
||||
SOURCES: tuple[dict[str, str], ...] = (
|
||||
PANGEA_CONTENT_FORMATS = (
|
||||
"WTF_0",
|
||||
"TEXT_ONLY",
|
||||
"WTF_1",
|
||||
"MOBILE_1",
|
||||
"MOBILE_2",
|
||||
"MOBILE_3",
|
||||
"WTF_2",
|
||||
"XML_TX",
|
||||
"JSON",
|
||||
)
|
||||
|
||||
PANGEA_CONTENT_TYPES = (
|
||||
"articles",
|
||||
"audioclips",
|
||||
"videoclips",
|
||||
"breakingnews",
|
||||
"mostpopular",
|
||||
"topstories",
|
||||
)
|
||||
|
||||
DEFAULT_SOURCES: tuple[dict[str, str], ...] = (
|
||||
{
|
||||
"name": "Guardian feed mirror",
|
||||
"slug": "guardian-feed",
|
||||
|
|
@ -51,22 +74,27 @@ SOURCES: tuple[dict[str, str], ...] = (
|
|||
)
|
||||
|
||||
|
||||
def _source_row(source: dict[str, str]) -> tuple[Node, ...]:
|
||||
def _source_row(source: Mapping[str, object]) -> tuple[Node, ...]:
|
||||
return (
|
||||
h.div[
|
||||
h.div(class_="font-semibold text-slate-950")[source["name"]],
|
||||
h.p(class_="mt-1 font-mono text-xs text-slate-500")[source["slug"]],
|
||||
h.div(class_="font-semibold text-slate-950")[str(source["name"])],
|
||||
h.p(class_="mt-1 font-mono text-xs text-slate-500")[str(source["slug"])],
|
||||
],
|
||||
h.p(class_="font-medium whitespace-nowrap text-slate-900")[
|
||||
source["source_type"]
|
||||
str(source["source_type"])
|
||||
],
|
||||
h.p(class_="max-w-sm truncate font-mono text-xs text-slate-600")[
|
||||
source["upstream"]
|
||||
str(source["upstream"])
|
||||
],
|
||||
h.p(class_="font-medium whitespace-nowrap text-slate-900")[
|
||||
str(source["schedule"])
|
||||
],
|
||||
h.p(class_="font-medium whitespace-nowrap text-slate-900")[source["schedule"]],
|
||||
h.div(class_="min-w-32 whitespace-normal")[
|
||||
status_badge(label=source["state"], tone=source["state_tone"]),
|
||||
h.p(class_="mt-2 text-xs text-slate-500")[source["last_run"]],
|
||||
status_badge(
|
||||
label=str(source["state"]),
|
||||
tone=str(source["state_tone"]),
|
||||
),
|
||||
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"),
|
||||
|
|
@ -75,8 +103,10 @@ def _source_row(source: dict[str, str]) -> tuple[Node, ...]:
|
|||
)
|
||||
|
||||
|
||||
def sources_table() -> Renderable:
|
||||
rows = tuple(_source_row(source) for source in SOURCES)
|
||||
def sources_table(
|
||||
*, sources: tuple[Mapping[str, object], ...] | None = None
|
||||
) -> Renderable:
|
||||
rows = tuple(_source_row(source) for source in (sources or DEFAULT_SOURCES))
|
||||
return table_section(
|
||||
eyebrow="Inventory",
|
||||
title="Sources",
|
||||
|
|
@ -87,18 +117,20 @@ def sources_table() -> Renderable:
|
|||
)
|
||||
|
||||
|
||||
def sources_page() -> Renderable:
|
||||
def sources_page(
|
||||
*, sources: tuple[Mapping[str, object], ...] | None = None
|
||||
) -> Renderable:
|
||||
return page_shell(
|
||||
current_path="/sources",
|
||||
eyebrow="Source management",
|
||||
title="Sources",
|
||||
description="Configured feed and Pangea sources live here as tables, with clear schedule and job state visibility instead of card-based CRUD.",
|
||||
actions=header_action_link(href="/sources/create", label="Create source"),
|
||||
content=sources_table(),
|
||||
content=sources_table(sources=sources),
|
||||
)
|
||||
|
||||
|
||||
def create_source_form() -> Renderable:
|
||||
def create_source_form(*, action_path: str = "/actions/sources/create") -> Renderable:
|
||||
return section_card(
|
||||
content=(
|
||||
h.div(
|
||||
|
|
@ -118,20 +150,40 @@ def create_source_form() -> Renderable:
|
|||
status_badge(label="New source", tone="scheduled"),
|
||||
],
|
||||
h.form(
|
||||
{"data-signals__ifmissing": "{sourceType: 'pangea'}"},
|
||||
{
|
||||
"data-signals": "{_formError: '', _formSuccess: ''}",
|
||||
"data-signals__ifmissing": "{sourceType: 'pangea'}",
|
||||
"data-on:submit": f"@post('{action_path}')",
|
||||
},
|
||||
class_="mt-5 space-y-6",
|
||||
)[
|
||||
h.div(
|
||||
{
|
||||
"data-show": "$_formError !== ''",
|
||||
"data-text": "$_formError",
|
||||
},
|
||||
class_="rounded-2xl bg-rose-50 px-4 py-3 text-sm font-medium text-rose-800",
|
||||
),
|
||||
h.div(
|
||||
{
|
||||
"data-show": "$_formSuccess !== ''",
|
||||
"data-text": "$_formSuccess",
|
||||
},
|
||||
class_="rounded-2xl bg-emerald-100 px-4 py-3 text-sm font-medium text-emerald-800",
|
||||
),
|
||||
h.div(class_="grid gap-4 md:grid-cols-2")[
|
||||
input_field(
|
||||
label="Source name",
|
||||
field_id="source-name",
|
||||
value="Pangea mobile articles",
|
||||
signal_name="sourceName",
|
||||
),
|
||||
input_field(
|
||||
label="Slug",
|
||||
field_id="source-slug",
|
||||
value="pangea-mobile",
|
||||
help_text="Immutable after creation.",
|
||||
signal_name="sourceSlug",
|
||||
),
|
||||
h.div[
|
||||
h.label(
|
||||
|
|
@ -169,6 +221,7 @@ def create_source_form() -> Renderable:
|
|||
label="Feed URL",
|
||||
field_id="feed-url",
|
||||
placeholder="https://example.com/feed.xml",
|
||||
signal_name="feedUrl",
|
||||
),
|
||||
],
|
||||
],
|
||||
|
|
@ -192,32 +245,59 @@ def create_source_form() -> Renderable:
|
|||
label="Pangea domain",
|
||||
field_id="pangea-domain",
|
||||
value="guardianproject.info",
|
||||
signal_name="pangeaDomain",
|
||||
),
|
||||
input_field(
|
||||
label="Category name",
|
||||
field_id="pangea-category",
|
||||
value="News",
|
||||
signal_name="pangeaCategory",
|
||||
),
|
||||
select_field(
|
||||
label="Content format",
|
||||
field_id="content-format",
|
||||
options=("MOBILE_3", "MOBILE_2", "WEB"),
|
||||
options=PANGEA_CONTENT_FORMATS,
|
||||
selected="MOBILE_3",
|
||||
signal_name="contentFormat",
|
||||
),
|
||||
input_field(
|
||||
select_field(
|
||||
label="Content type",
|
||||
field_id="content-type",
|
||||
value="articles",
|
||||
options=PANGEA_CONTENT_TYPES,
|
||||
selected="articles",
|
||||
signal_name="contentType",
|
||||
),
|
||||
input_field(
|
||||
label="Max articles",
|
||||
field_id="max-articles",
|
||||
value="10",
|
||||
signal_name="maxArticles",
|
||||
),
|
||||
input_field(
|
||||
label="Oldest article (days)",
|
||||
field_id="oldest-article",
|
||||
value="3",
|
||||
signal_name="oldestArticle",
|
||||
),
|
||||
],
|
||||
h.div(class_="grid gap-4 lg:grid-cols-3")[
|
||||
toggle_field(
|
||||
label="Only newest",
|
||||
description="Limit Pangea syncs to the newest material available in the selected category.",
|
||||
signal_name="onlyNewest",
|
||||
checked=True,
|
||||
),
|
||||
toggle_field(
|
||||
label="Include authors",
|
||||
description="Carry author bylines into mirrored output where upstream data exists.",
|
||||
signal_name="includeAuthors",
|
||||
checked=True,
|
||||
),
|
||||
toggle_field(
|
||||
label="Exclude media",
|
||||
description="Skip image and media attachment mirroring for this source.",
|
||||
signal_name="excludeMedia",
|
||||
checked=False,
|
||||
),
|
||||
],
|
||||
],
|
||||
|
|
@ -226,11 +306,13 @@ def create_source_form() -> Renderable:
|
|||
label="Notes",
|
||||
field_id="source-notes",
|
||||
value="Primary Pangea mobile article mirror for the operator landing page.",
|
||||
signal_name="sourceNotes",
|
||||
),
|
||||
textarea_field(
|
||||
label="Spider arguments",
|
||||
field_id="spider-arguments",
|
||||
value="language=en,download_media=true",
|
||||
signal_name="spiderArguments",
|
||||
),
|
||||
],
|
||||
h.div(
|
||||
|
|
@ -250,26 +332,31 @@ def create_source_form() -> Renderable:
|
|||
label="Minute",
|
||||
field_id="cron-minute",
|
||||
value="15",
|
||||
signal_name="cronMinute",
|
||||
),
|
||||
input_field(
|
||||
label="Hour",
|
||||
field_id="cron-hour",
|
||||
value="*/4",
|
||||
signal_name="cronHour",
|
||||
),
|
||||
input_field(
|
||||
label="Day of month",
|
||||
field_id="cron-day-of-month",
|
||||
value="*",
|
||||
signal_name="cronDayOfMonth",
|
||||
),
|
||||
input_field(
|
||||
label="Day of week",
|
||||
field_id="cron-day-of-week",
|
||||
value="1-6",
|
||||
signal_name="cronDayOfWeek",
|
||||
),
|
||||
input_field(
|
||||
label="Month",
|
||||
field_id="cron-month",
|
||||
value="*",
|
||||
signal_name="cronMonth",
|
||||
),
|
||||
],
|
||||
],
|
||||
|
|
@ -287,24 +374,6 @@ def create_source_form() -> Renderable:
|
|||
signal_name="jobEnabled",
|
||||
checked=True,
|
||||
),
|
||||
toggle_field(
|
||||
label="Only newest",
|
||||
description="Limit Pangea syncs to the newest material available in the selected category.",
|
||||
signal_name="onlyNewest",
|
||||
checked=True,
|
||||
),
|
||||
toggle_field(
|
||||
label="Include authors",
|
||||
description="Carry author bylines into mirrored output where upstream data exists.",
|
||||
signal_name="includeAuthors",
|
||||
checked=True,
|
||||
),
|
||||
toggle_field(
|
||||
label="Exclude media",
|
||||
description="Skip image and media attachment mirroring for this source.",
|
||||
signal_name="excludeMedia",
|
||||
checked=False,
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
|
|
@ -313,7 +382,7 @@ def create_source_form() -> Renderable:
|
|||
)[
|
||||
muted_action_link(href="/sources", label="Cancel"),
|
||||
h.button(
|
||||
type="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"],
|
||||
],
|
||||
|
|
@ -322,7 +391,7 @@ def create_source_form() -> Renderable:
|
|||
)
|
||||
|
||||
|
||||
def create_source_page() -> Renderable:
|
||||
def create_source_page(*, action_path: str = "/actions/sources/create") -> Renderable:
|
||||
actions = (
|
||||
muted_action_link(href="/sources", label="Back to sources"),
|
||||
header_action_link(href="/runs", label="View runs"),
|
||||
|
|
@ -333,5 +402,5 @@ def create_source_page() -> Renderable:
|
|||
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.",
|
||||
actions=actions,
|
||||
content=create_source_form(),
|
||||
content=create_source_form(action_path=action_path),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue