Share live runs table between dashboard and runs
This commit is contained in:
parent
c04efeb189
commit
f3f4badaa2
5 changed files with 159 additions and 181 deletions
|
|
@ -816,6 +816,7 @@ def load_dashboard_view(
|
||||||
footprint_bytes = _directory_size(output_dir)
|
footprint_bytes = _directory_size(output_dir)
|
||||||
return {
|
return {
|
||||||
"running": runs_view["running"],
|
"running": runs_view["running"],
|
||||||
|
"queued": runs_view["queued"],
|
||||||
"source_feeds": tuple(
|
"source_feeds": tuple(
|
||||||
_project_source_feed(source, output_dir, reference_time)
|
_project_source_feed(source, output_dir, reference_time)
|
||||||
for source in sources
|
for source in sources
|
||||||
|
|
|
||||||
|
|
@ -8,56 +8,13 @@ from htpy import Node, Renderable
|
||||||
from repub.components import (
|
from repub.components import (
|
||||||
app_shell,
|
app_shell,
|
||||||
header_action_link,
|
header_action_link,
|
||||||
inline_button,
|
|
||||||
inline_link,
|
inline_link,
|
||||||
muted_action_link,
|
muted_action_link,
|
||||||
stat_card,
|
stat_card,
|
||||||
status_badge,
|
status_badge,
|
||||||
table_section,
|
table_section,
|
||||||
)
|
)
|
||||||
|
from repub.pages.runs import live_work_section, relative_time_formatter_script
|
||||||
|
|
||||||
def _text(values: Mapping[str, object], key: str) -> str:
|
|
||||||
return str(values[key])
|
|
||||||
|
|
||||||
|
|
||||||
def _running_execution_row(execution: Mapping[str, object]) -> tuple[Node, ...]:
|
|
||||||
status_tone = "running" if _text(execution, "status") != "Succeeded" else "done"
|
|
||||||
return (
|
|
||||||
h.div[
|
|
||||||
h.div(class_="font-semibold text-slate-950")[_text(execution, "source")],
|
|
||||||
h.p(class_="mt-0.5 font-mono text-[11px] text-slate-500")[
|
|
||||||
_text(execution, "slug")
|
|
||||||
],
|
|
||||||
],
|
|
||||||
h.div[
|
|
||||||
h.p(class_="font-medium text-slate-900")[
|
|
||||||
f"#{_text(execution, 'execution_id')}"
|
|
||||||
],
|
|
||||||
h.p(class_="mt-0.5 text-[11px] text-slate-500")[
|
|
||||||
f"job {_text(execution, 'job_id')}"
|
|
||||||
],
|
|
||||||
],
|
|
||||||
h.div[
|
|
||||||
h.p(class_="font-medium text-slate-900")[_text(execution, "started_at")],
|
|
||||||
h.p(class_="mt-0.5 text-[11px] text-slate-500")[
|
|
||||||
_text(execution, "runtime")
|
|
||||||
],
|
|
||||||
],
|
|
||||||
status_badge(label=_text(execution, "status"), tone=status_tone),
|
|
||||||
h.div(class_="min-w-56 whitespace-normal")[
|
|
||||||
h.p(class_="font-medium text-slate-900")[_text(execution, "stats")],
|
|
||||||
h.p(class_="mt-0.5 text-[11px] text-slate-500")[_text(execution, "worker")],
|
|
||||||
],
|
|
||||||
h.div(class_="flex flex-nowrap items-center gap-3")[
|
|
||||||
inline_link(
|
|
||||||
href=_text(execution, "log_href"),
|
|
||||||
label="View log",
|
|
||||||
tone="amber",
|
|
||||||
),
|
|
||||||
inline_button(label="Stop", tone="danger"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def dashboard_header() -> Renderable:
|
def dashboard_header() -> Renderable:
|
||||||
|
|
@ -121,76 +78,6 @@ def operational_snapshot(*, snapshot: Mapping[str, str] | None = None) -> Render
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def running_executions_table(
|
|
||||||
*, running_executions: tuple[Mapping[str, object], ...] | None = None
|
|
||||||
) -> Renderable:
|
|
||||||
rows = tuple(
|
|
||||||
_running_execution_row(execution) for execution in (running_executions or ())
|
|
||||||
)
|
|
||||||
headers = ("Source", "Execution", "Started", "Status", "Stats", "Actions")
|
|
||||||
|
|
||||||
def render_row(row: tuple[Node, ...]) -> Renderable:
|
|
||||||
first_cell, *other_cells = row
|
|
||||||
return h.tr(class_="align-top")[
|
|
||||||
h.td(class_="py-3 pr-6 pl-4 text-sm font-medium text-slate-950 sm:pl-4")[
|
|
||||||
first_cell
|
|
||||||
],
|
|
||||||
(
|
|
||||||
h.td(
|
|
||||||
class_="px-3 py-3 align-top text-sm whitespace-nowrap text-slate-600"
|
|
||||||
)[cell]
|
|
||||||
for cell in other_cells
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
body_rows: Node
|
|
||||||
if rows:
|
|
||||||
body_rows = (render_row(row) for row in rows)
|
|
||||||
else:
|
|
||||||
body_rows = h.tr[
|
|
||||||
h.td(
|
|
||||||
colspan=str(len(headers)),
|
|
||||||
class_="px-4 py-8 text-center text-sm text-slate-500",
|
|
||||||
)["No job executions are running."]
|
|
||||||
]
|
|
||||||
|
|
||||||
return h.section[
|
|
||||||
h.div(class_="mb-3 flex items-end justify-between gap-4")[
|
|
||||||
h.div[
|
|
||||||
h.p(
|
|
||||||
class_="text-xs font-semibold uppercase tracking-[0.22em] text-amber-600"
|
|
||||||
)["Live work"],
|
|
||||||
h.h2(class_="mt-1 text-xl font-semibold text-slate-950")[
|
|
||||||
"Running executions"
|
|
||||||
],
|
|
||||||
],
|
|
||||||
muted_action_link(href="/runs", label="Open runs"),
|
|
||||||
],
|
|
||||||
h.div(
|
|
||||||
class_="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-slate-200"
|
|
||||||
)[
|
|
||||||
h.div(class_="overflow-x-auto")[
|
|
||||||
h.table(
|
|
||||||
class_="w-full min-w-[70rem] divide-y divide-slate-200 table-auto"
|
|
||||||
)[
|
|
||||||
h.thead(class_="bg-stone-50")[
|
|
||||||
h.tr[
|
|
||||||
(
|
|
||||||
h.th(
|
|
||||||
scope="col",
|
|
||||||
class_="px-3 py-2.5 text-left text-[11px] font-semibold uppercase tracking-[0.18em] whitespace-nowrap text-slate-500 first:pl-4",
|
|
||||||
)[header]
|
|
||||||
for header in headers
|
|
||||||
)
|
|
||||||
]
|
|
||||||
],
|
|
||||||
h.tbody(class_="divide-y divide-slate-200 bg-white")[body_rows],
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _source_feed_row(source_feed: Mapping[str, object]) -> tuple[Node, ...]:
|
def _source_feed_row(source_feed: Mapping[str, object]) -> tuple[Node, ...]:
|
||||||
last_updated_iso = source_feed.get("last_updated_iso")
|
last_updated_iso = source_feed.get("last_updated_iso")
|
||||||
last_updated = (
|
last_updated = (
|
||||||
|
|
@ -249,9 +136,11 @@ def dashboard_page_with_data(
|
||||||
*,
|
*,
|
||||||
snapshot: Mapping[str, str] | None = None,
|
snapshot: Mapping[str, str] | None = None,
|
||||||
running_executions: tuple[Mapping[str, object], ...] | None = None,
|
running_executions: tuple[Mapping[str, object], ...] | None = None,
|
||||||
|
queued_executions: tuple[Mapping[str, object], ...] | None = None,
|
||||||
source_feeds: tuple[Mapping[str, object], ...] | None = None,
|
source_feeds: tuple[Mapping[str, object], ...] | None = None,
|
||||||
) -> Renderable:
|
) -> Renderable:
|
||||||
running_items = running_executions or ()
|
running_items = running_executions or ()
|
||||||
|
queued_items = queued_executions or ()
|
||||||
source_items = source_feeds or ()
|
source_items = source_feeds or ()
|
||||||
return app_shell(
|
return app_shell(
|
||||||
current_path="/",
|
current_path="/",
|
||||||
|
|
@ -260,7 +149,11 @@ def dashboard_page_with_data(
|
||||||
content=(
|
content=(
|
||||||
dashboard_header(),
|
dashboard_header(),
|
||||||
operational_snapshot(snapshot=snapshot),
|
operational_snapshot(snapshot=snapshot),
|
||||||
running_executions_table(running_executions=running_items),
|
live_work_section(
|
||||||
|
running_executions=running_items,
|
||||||
|
queued_executions=queued_items,
|
||||||
|
),
|
||||||
published_feeds_table(source_feeds=source_items),
|
published_feeds_table(source_feeds=source_items),
|
||||||
|
relative_time_formatter_script(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -496,81 +496,39 @@ def _completed_history_section(
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def runs_page(
|
def live_work_section(
|
||||||
*,
|
*,
|
||||||
running_executions: tuple[Mapping[str, object], ...] | None = None,
|
running_executions: tuple[Mapping[str, object], ...] | None = None,
|
||||||
queued_executions: tuple[Mapping[str, object], ...] | None = None,
|
queued_executions: tuple[Mapping[str, object], ...] | None = None,
|
||||||
upcoming_jobs: tuple[Mapping[str, object], ...] | None = None,
|
actions: Node | None = None,
|
||||||
completed_executions: tuple[Mapping[str, object], ...] | None = None,
|
|
||||||
completed_page: int = 1,
|
|
||||||
completed_page_size: int = 20,
|
|
||||||
completed_total_count: int | None = None,
|
|
||||||
completed_total_pages: int | None = None,
|
|
||||||
source_count: int = 0,
|
|
||||||
) -> Renderable:
|
) -> Renderable:
|
||||||
running_items = running_executions or ()
|
running_items = running_executions or ()
|
||||||
queued_items = queued_executions or ()
|
queued_items = queued_executions or ()
|
||||||
upcoming_items = upcoming_jobs or ()
|
|
||||||
completed_items = completed_executions or ()
|
|
||||||
running_rows = tuple(_running_row(execution) for execution in running_items)
|
running_rows = tuple(_running_row(execution) for execution in running_items)
|
||||||
queued_rows = tuple(_queued_row(execution) for execution in queued_items)
|
queued_rows = tuple(_queued_row(execution) for execution in queued_items)
|
||||||
live_rows = running_rows + queued_rows
|
live_rows = running_rows + queued_rows
|
||||||
live_row_attrs = tuple(
|
live_row_attrs = tuple(
|
||||||
_queue_row_attrs(execution) for execution in running_items + queued_items
|
_queue_row_attrs(execution) for execution in running_items + queued_items
|
||||||
)
|
)
|
||||||
upcoming_rows = tuple(_upcoming_row(job) for job in upcoming_items)
|
return table_section(
|
||||||
completed_rows = tuple(_completed_row(execution) for execution in completed_items)
|
eyebrow="Live work",
|
||||||
resolved_completed_total_count = (
|
title="Running jobs",
|
||||||
len(completed_items) if completed_total_count is None else completed_total_count
|
empty_message="No jobs are running or queued.",
|
||||||
)
|
headers=(
|
||||||
resolved_completed_total_pages = (
|
"State",
|
||||||
1 if completed_total_pages is None else completed_total_pages
|
"Source",
|
||||||
|
"Details",
|
||||||
|
"Actions",
|
||||||
|
),
|
||||||
|
rows=live_rows,
|
||||||
|
row_attrs=live_row_attrs,
|
||||||
|
actions=actions,
|
||||||
)
|
)
|
||||||
|
|
||||||
return page_shell(
|
|
||||||
current_path="/runs",
|
def relative_time_formatter_script() -> Renderable:
|
||||||
eyebrow="Execution control",
|
return h.script[
|
||||||
title="Runs",
|
"""
|
||||||
actions=muted_action_link(href="/sources", label="Back to sources"),
|
|
||||||
source_count=source_count,
|
|
||||||
running_count=len(running_items),
|
|
||||||
content=(
|
|
||||||
table_section(
|
|
||||||
eyebrow="Live work",
|
|
||||||
title="Running jobs",
|
|
||||||
empty_message="No jobs are running or queued.",
|
|
||||||
headers=(
|
|
||||||
"State",
|
|
||||||
"Source",
|
|
||||||
"Details",
|
|
||||||
"Actions",
|
|
||||||
),
|
|
||||||
rows=live_rows,
|
|
||||||
row_attrs=live_row_attrs,
|
|
||||||
),
|
|
||||||
table_section(
|
|
||||||
eyebrow="Schedule",
|
|
||||||
title="Scheduled jobs",
|
|
||||||
empty_message="No jobs are scheduled.",
|
|
||||||
headers=(
|
|
||||||
"Source",
|
|
||||||
"State",
|
|
||||||
"Next run",
|
|
||||||
"Cron",
|
|
||||||
"Run now",
|
|
||||||
"Actions",
|
|
||||||
),
|
|
||||||
rows=upcoming_rows,
|
|
||||||
),
|
|
||||||
_completed_history_section(
|
|
||||||
completed_rows=completed_rows,
|
|
||||||
completed_page=completed_page,
|
|
||||||
completed_page_size=completed_page_size,
|
|
||||||
completed_total_count=resolved_completed_total_count,
|
|
||||||
completed_total_pages=resolved_completed_total_pages,
|
|
||||||
),
|
|
||||||
h.script[
|
|
||||||
"""
|
|
||||||
window.repubFormatNextRuns = window.repubFormatNextRuns || (() => {
|
window.repubFormatNextRuns = window.repubFormatNextRuns || (() => {
|
||||||
const relativeFormatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
|
const relativeFormatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
|
||||||
const absoluteFormatter = new Intl.DateTimeFormat(undefined, {
|
const absoluteFormatter = new Intl.DateTimeFormat(undefined, {
|
||||||
|
|
@ -612,8 +570,67 @@ window.repubFormatNextRuns = window.repubFormatNextRuns || (() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
window.repubFormatNextRuns();
|
window.repubFormatNextRuns();
|
||||||
"""
|
"""
|
||||||
],
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def runs_page(
|
||||||
|
*,
|
||||||
|
running_executions: tuple[Mapping[str, object], ...] | None = None,
|
||||||
|
queued_executions: tuple[Mapping[str, object], ...] | None = None,
|
||||||
|
upcoming_jobs: tuple[Mapping[str, object], ...] | None = None,
|
||||||
|
completed_executions: tuple[Mapping[str, object], ...] | None = None,
|
||||||
|
completed_page: int = 1,
|
||||||
|
completed_page_size: int = 20,
|
||||||
|
completed_total_count: int | None = None,
|
||||||
|
completed_total_pages: int | None = None,
|
||||||
|
source_count: int = 0,
|
||||||
|
) -> Renderable:
|
||||||
|
upcoming_items = upcoming_jobs or ()
|
||||||
|
completed_items = completed_executions or ()
|
||||||
|
upcoming_rows = tuple(_upcoming_row(job) for job in upcoming_items)
|
||||||
|
completed_rows = tuple(_completed_row(execution) for execution in completed_items)
|
||||||
|
resolved_completed_total_count = (
|
||||||
|
len(completed_items) if completed_total_count is None else completed_total_count
|
||||||
|
)
|
||||||
|
resolved_completed_total_pages = (
|
||||||
|
1 if completed_total_pages is None else completed_total_pages
|
||||||
|
)
|
||||||
|
|
||||||
|
return page_shell(
|
||||||
|
current_path="/runs",
|
||||||
|
eyebrow="Execution control",
|
||||||
|
title="Runs",
|
||||||
|
actions=muted_action_link(href="/sources", label="Back to sources"),
|
||||||
|
source_count=source_count,
|
||||||
|
running_count=len(running_executions or ()),
|
||||||
|
content=(
|
||||||
|
live_work_section(
|
||||||
|
running_executions=running_executions,
|
||||||
|
queued_executions=queued_executions,
|
||||||
|
),
|
||||||
|
table_section(
|
||||||
|
eyebrow="Schedule",
|
||||||
|
title="Scheduled jobs",
|
||||||
|
empty_message="No jobs are scheduled.",
|
||||||
|
headers=(
|
||||||
|
"Source",
|
||||||
|
"State",
|
||||||
|
"Next run",
|
||||||
|
"Cron",
|
||||||
|
"Run now",
|
||||||
|
"Actions",
|
||||||
|
),
|
||||||
|
rows=upcoming_rows,
|
||||||
|
),
|
||||||
|
_completed_history_section(
|
||||||
|
completed_rows=completed_rows,
|
||||||
|
completed_page=completed_page,
|
||||||
|
completed_page_size=completed_page_size,
|
||||||
|
completed_total_count=resolved_completed_total_count,
|
||||||
|
completed_total_pages=resolved_completed_total_pages,
|
||||||
|
),
|
||||||
|
relative_time_formatter_script(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -437,6 +437,7 @@ async def render_dashboard(app: Quart | None = None) -> Renderable:
|
||||||
return dashboard_page_with_data(
|
return dashboard_page_with_data(
|
||||||
snapshot=cast(dict[str, str], view["snapshot"]),
|
snapshot=cast(dict[str, str], view["snapshot"]),
|
||||||
running_executions=cast(tuple[dict[str, object], ...], view["running"]),
|
running_executions=cast(tuple[dict[str, object], ...], view["running"]),
|
||||||
|
queued_executions=cast(tuple[dict[str, object], ...], view["queued"]),
|
||||||
source_feeds=cast(tuple[dict[str, object], ...], view["source_feeds"]),
|
source_feeds=cast(tuple[dict[str, object], ...], view["source_feeds"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from repub.model import (
|
||||||
load_settings_form,
|
load_settings_form,
|
||||||
save_setting,
|
save_setting,
|
||||||
)
|
)
|
||||||
|
from repub.pages import dashboard_page_with_data
|
||||||
from repub.pages.runs import runs_page
|
from repub.pages.runs import runs_page
|
||||||
from repub.pages.sources import sources_page
|
from repub.pages.sources import sources_page
|
||||||
from repub.web import (
|
from repub.web import (
|
||||||
|
|
@ -471,7 +472,7 @@ def test_dashboard_post_serves_morph_component() -> None:
|
||||||
assert b"id: " in chunk
|
assert b"id: " in chunk
|
||||||
assert b'<main id="morph"' in chunk
|
assert b'<main id="morph"' in chunk
|
||||||
assert b"Operational snapshot" in chunk
|
assert b"Operational snapshot" in chunk
|
||||||
assert b"Running executions" in chunk
|
assert b"Running jobs" in chunk
|
||||||
await connection.disconnect()
|
await connection.disconnect()
|
||||||
|
|
||||||
asyncio.run(run())
|
asyncio.run(run())
|
||||||
|
|
@ -572,6 +573,8 @@ def test_render_stream_stops_when_shutdown_is_requested() -> None:
|
||||||
await stream.aclose()
|
await stream.aclose()
|
||||||
|
|
||||||
asyncio.run(run())
|
asyncio.run(run())
|
||||||
|
|
||||||
|
|
||||||
def test_render_dashboard_shows_dashboard_information_architecture(
|
def test_render_dashboard_shows_dashboard_information_architecture(
|
||||||
monkeypatch, tmp_path: Path
|
monkeypatch, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -583,7 +586,7 @@ def test_render_dashboard_shows_dashboard_information_architecture(
|
||||||
body = str(await render_dashboard(app))
|
body = str(await render_dashboard(app))
|
||||||
|
|
||||||
assert "Operational snapshot" in body
|
assert "Operational snapshot" in body
|
||||||
assert "Running executions" in body
|
assert "Running jobs" in body
|
||||||
assert "Published feeds" in body
|
assert "Published feeds" in body
|
||||||
assert 'href="/sources"' in body
|
assert 'href="/sources"' in body
|
||||||
assert 'href="/runs"' in body
|
assert 'href="/runs"' in body
|
||||||
|
|
@ -602,12 +605,75 @@ def test_render_dashboard_shows_empty_state_rows(monkeypatch, tmp_path: Path) ->
|
||||||
app = create_app()
|
app = create_app()
|
||||||
body = str(await render_dashboard(app))
|
body = str(await render_dashboard(app))
|
||||||
|
|
||||||
assert "No job executions are running." in body
|
assert "No jobs are running or queued." in body
|
||||||
assert "No feeds have been published yet." in body
|
assert "No feeds have been published yet." in body
|
||||||
|
|
||||||
asyncio.run(run())
|
asyncio.run(run())
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_running_table_matches_runs_page_live_table_markup() -> None:
|
||||||
|
running_executions = (
|
||||||
|
{
|
||||||
|
"source": "Running source",
|
||||||
|
"slug": "running-source",
|
||||||
|
"job_id": 1,
|
||||||
|
"execution_id": 11,
|
||||||
|
"started_at": "2 minutes ago",
|
||||||
|
"started_at_iso": "2026-03-30T12:00:00+00:00",
|
||||||
|
"duration": "00:00:10",
|
||||||
|
"runtime": "running for 10s",
|
||||||
|
"status": "Running",
|
||||||
|
"stats": "1 requests • 1 items • 1 byte",
|
||||||
|
"worker": "streaming stats from worker",
|
||||||
|
"log_href": "/job/1/execution/11/logs",
|
||||||
|
"cancel_label": "Stop",
|
||||||
|
"cancel_post_path": "/actions/executions/11/cancel",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
queued_executions = (
|
||||||
|
{
|
||||||
|
"source": "Queued source",
|
||||||
|
"slug": "queued-source",
|
||||||
|
"job_id": 2,
|
||||||
|
"execution_id": 22,
|
||||||
|
"queued_at": "2 minutes ago",
|
||||||
|
"queued_at_iso": "2026-03-30T12:28:00+00:00",
|
||||||
|
"queue_position": 1,
|
||||||
|
"status": "Queued",
|
||||||
|
"status_tone": "idle",
|
||||||
|
"run_label": "Queued",
|
||||||
|
"run_disabled": True,
|
||||||
|
"run_post_path": "/actions/jobs/2/run-now",
|
||||||
|
"cancel_post_path": "/actions/queued-executions/22/cancel",
|
||||||
|
"move_up_disabled": True,
|
||||||
|
"move_up_post_path": None,
|
||||||
|
"move_down_disabled": True,
|
||||||
|
"move_down_post_path": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
runs_body = str(
|
||||||
|
runs_page(
|
||||||
|
running_executions=running_executions,
|
||||||
|
queued_executions=queued_executions,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
dashboard_body = str(
|
||||||
|
dashboard_page_with_data(
|
||||||
|
running_executions=running_executions,
|
||||||
|
queued_executions=queued_executions,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "Running jobs" in dashboard_body
|
||||||
|
assert "Running executions" not in dashboard_body
|
||||||
|
assert "Running source" in dashboard_body
|
||||||
|
assert "running-source" in dashboard_body
|
||||||
|
assert "queued-source" in dashboard_body
|
||||||
|
assert "bg-sky-100 text-sky-800" in dashboard_body
|
||||||
|
assert "/job/1/execution/11/logs" in dashboard_body
|
||||||
|
assert runs_body.count(">State<") >= 1
|
||||||
|
|
||||||
|
|
||||||
def test_load_dashboard_view_measures_log_artifact_path(
|
def test_load_dashboard_view_measures_log_artifact_path(
|
||||||
monkeypatch, tmp_path: Path
|
monkeypatch, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue