Current section

Files

Jump to
gust_web lib gust_web live dag_live dashboard.html.heex
Raw

lib/gust_web/live/dag_live/dashboard.html.heex

<Layouts.app flash={@flash}>
<div
:if={@error != {}}
class="alert alert-error shadow-lg mb-4"
role="alert"
id="compilation-error"
>
<% {_lines, error, description} = @error %>
{"#{error} #{description}"}
</div>
<.live_component
module={GustWeb.BreadcrumbsComponent}
dag_def={@dag}
selected_item={@selected_item}
id="breadcrumbs"
/>
<.live_component
module={GustWeb.DagSummaryComponent}
dag={@dag}
dag_def={@dag_def}
id={@dag.id}
/>
<div class="dag-card mb-4">
<section class="dag-card__header">
<div class="dag-card__title-row">
<h2 class="dag-card__title dag-card__title-with-icon">
<.icon name="hero-queue-list" class="h-5 w-5 text-sky-600" />
<span>Run History</span>
</h2>
<div :if={@selected_item} class="dag-card__actions run-history__actions">
<button
:if={restartable?(@selected_item, @item_status)}
id="restart"
phx-click="restart"
class="btn btn-warning"
>
Restart {@item_name}
</button>
<button
:if={cancelable?(@selected_item, @item_status)}
id="cancel"
phx-click="cancel"
class="btn px-3 py-1.5 bg-rose-500 hover:bg-rose-600 text-white text-sm font-medium rounded shadow transition duration-150"
>
Cancel {@item_name}
</button>
</div>
</div>
</section>
<div class="dag-card__body">
<div class="min-w-max">
<!-- Task Grid -->
<div class="flex w-full">
<!-- Task names column -->
<div class="pr-2 border-r border-gray-200 pt-2 w-1/6">
<div class="flex items-center h-6 mb-2 border-b-2">
<span class="text-xs font-medium text-gray-800 ">Run</span>
</div>
<div :for={name <- @dag_def.task_list} class="flex items-center gap-1 h-6 mb-2">
<span class="text-xs font-medium text-gray-800">
{name}
<strong :if={mapped_task?(@dag_def, name)} id={"#{name}-map-badge"}>
[]
</strong>
</span>
</div>
</div>
<!-- Run columns -->
<div
class="flex overflow-x-auto w-5/6"
phx-update="stream"
id="run_history"
>
<%= for {id, %Run{status: status, id: run_id, tasks: ran_tasks} = run} <- @streams.runs do %>
<% tasks_by_name = Enum.group_by(ran_tasks, & &1.name) %>
<div class={"px-1 rounded #{selected_run_class(run_id, @selected_item)}"} id={id}>
<.link navigate={
~g"/dags/#{@dag_def.name}/dashboard?run_id=#{run_id}&page=#{@page}"
}>
<div class="flex justify-center mb-2 pt-2">
<.task_cell id={"run-status-cell-#{run_id}"} status={status} />
</div>
</.link>
<div :for={task_name <- @dag_def.task_list} class="flex justify-center mb-2">
<.interactive_task_cell
task_data={task_cell_data(task_name, tasks_by_name, @selected_item)}
name={task_name}
run_id={run_id}
navigate={
~g"/dags/#{@dag_def.name}/dashboard?run_id=#{run_id}&task_name=#{task_name}&page=#{@page}"
}
data-testid={"#{task_name}-at-run-#{run_id}-link"}
/>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
<div class="dag-card__footer">
<div class="flex gap-2 items-center text-xs font-medium text-slate-600">
<.link
id="next-page"
class="inline-flex items-center gap-1 px-3 py-2 hover:text-slate-900"
navigate={~g"/dags/#{@dag_def.name}/dashboard?page=#{handle_page(@page, :next)}"}
>
<.icon name="hero-arrow-left-solid" class="h-3 w-3" /> Older
</.link>
</div>
<div class="flex items-center gap-3 text-xs font-medium text-slate-600">
<span class="text-xs text-slate-600">
page: {@page}
</span>
</div>
<div class="flex gap-2 items-center">
<.link
id="prev-page"
class="inline-flex items-center gap-1 px-3 py-2 hover:text-slate-900 text-xs font-medium text-slate-600"
navigate={~g"/dags/#{@dag_def.name}/dashboard?page=#{handle_page(@page, :prev)}"}
>
Newer <.icon name="hero-arrow-right-solid" class="h-3 w-3" />
</.link>
</div>
</div>
<div class="flex pb-3 justify-center bg-slate-50">
<.link
id="all-runs"
class="btn btn-xs btn-soft"
navigate={~g"/dags/#{@dag_def.name}/runs?page_size=30&page=1"}
>
<.icon name="hero-queue-list" class="h-3 w-3" />All runs
</.link>
</div>
</div>
<div :if={@selected_item} id="selected-item" class="dag-card mb-4">
<section class="dag-card__header">
<div class="dag-card__title-row">
<div class="flex flex-col gap-1">
<h3 class="dag-card__title text-base">Selected {@item_name}</h3>
<span :if={@item_id} class="text-sm text-slate-600">ID {@item_id}</span>
</div>
<.status_badge :if={@item_status} status={@item_status} data-testid="status-badge" />
</div>
</section>
<section class="dag-card__meta">
<div :if={@item_inserted_at} class="dag-card__meta-item">
<div class="dag-card__meta-label">Started</div>
<div class="dag-card__meta-value" id="inserted-at">
{strftime(@item_inserted_at)}
</div>
</div>
<div :if={@item_updated_at} class="dag-card__meta-item">
<div class="dag-card__meta-label">Updated</div>
<div class="dag-card__meta-value" id="updated-at">
{strftime(@item_updated_at)}
</div>
</div>
</section>
<section :if={@item_params && map_size(@item_params) > 0} class="dag-card__body">
<h4 class="text-sm font-semibold text-slate-700">Params</h4>
<div class="overflow-x-auto overflow-y-auto max-h-96 mt-2">
<pre
id="run-params"
data-testid="run-params"
class="language-json rounded-md overflow-x-auto"
>
<%= pretty_json!(@item_params) %>
</pre>
</div>
</section>
<section :if={@item_error && map_size(@item_error) > 0} class="dag-card__body">
<h4 class="text-sm font-semibold text-rose-600">Error</h4>
<div class="alert overflow-auto mt-2">
<pre
id="task-error"
class="mt-1 whitespace-pre-wrap text-sm"
>
<%= pretty_json!(@item_error) %>
</pre>
</div>
</section>
<section :if={@item_result && map_size(@item_result) > 0} class="dag-card__body">
<h4 class="text-sm font-semibold text-slate-700">Result</h4>
<div class="overflow-x-auto overflow-y-auto max-h-96 mt-2">
<pre
id="task-result"
data-testid="task-result"
class="language-json rounded-md overflow-x-auto"
>
<%= pretty_json!(@item_result) %>
</pre>
</div>
</section>
<section :if={@expanded_item_ids != []} class="dag-card__body">
<div class="border-t border-slate-200/70 -mx-4 mb-3" />
<h4 class="text-sm font-semibold text-slate-700">Task runs</h4>
<div class="overflow-x-auto overflow-y-auto max-h-96 mt-2">
<table id="mapped-task-runs" class="min-w-full divide-y divide-gray-200">
<thead class="bg-slate-50">
<tr>
<th class="px-3 py-2 text-left text-xs font-medium text-slate-600 uppercase tracking-wider">
Index
</th>
<th class="px-3 py-2 text-left text-xs font-medium text-slate-600 uppercase tracking-wider">
Status
</th>
<th class="px-3 py-2 text-left text-xs font-medium text-slate-600 uppercase tracking-wider">
Task ID
</th>
<th class="px-3 py-2 text-left text-xs font-medium text-slate-600 uppercase tracking-wider">
Updated
</th>
<th class="px-3 py-2 text-left text-xs font-medium text-slate-600 uppercase tracking-wider">
Action
</th>
</tr>
</thead>
<tbody
id="mapped-task-run-list"
class="bg-white divide-y divide-gray-200"
phx-update="stream"
>
<tr
:for={{id, task} <- @streams.expanded_items}
id={id}
>
<td class="px-3 py-2 whitespace-nowrap">
<span class="badge badge-xs">{task.map_index}</span>
</td>
<td class="px-3 py-2 whitespace-nowrap">
<.status_badge status={task.status} />
</td>
<td class="px-3 py-2 whitespace-nowrap text-xs text-gray-800 font-mono">
{task.id}
</td>
<td class="px-3 py-2 whitespace-nowrap text-xs text-gray-800">
{strftime(task.updated_at)}
</td>
<td class="px-3 py-2 whitespace-nowrap">
<.link
class="btn btn-xs btn-soft"
navigate={
~g"/dags/#{@dag_def.name}/dashboard?run_id=#{task.run_id}&task_name=#{task.name}&task_index=#{task.map_index}&page=#{@page}"
}
id={"show-mapped-task-#{task.id}"}
>
Show
</.link>
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section :if={@expanded_item_ids == []} class="dag-card__body">
<div class="border-t border-slate-200/70 -mx-4 mb-4" />
<div class="flex flex-wrap items-center justify-between pb-3 gap-3">
<h4 class="text-sm font-semibold text-slate-700">Logs</h4>
<.form for={%{}} id="log-filter" phx-change="filter_logs" class="log-filter">
<.input
id="log-filter-level"
value=""
name="level"
type="select"
options={[
{"All", ""},
{"info", "info"},
{"debug", "debug"},
{"warn", "warn"},
{"error", "error"}
]}
class="select select-bordered select-sm w-28"
/>
</.form>
</div>
<div
:if={@empty_logs}
id="logs-empty-state"
class="flex items-start gap-3 rounded-lg border border-dashed border-slate-300 bg-slate-50 px-4 py-5"
>
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-white text-slate-500 ring-1 ring-slate-200">
<.icon name="hero-document-text" class="h-5 w-5" />
</div>
<div class="min-w-0">
<h4 class="text-sm font-semibold text-slate-900">No logs for this item</h4>
<p class="mt-1 text-sm text-slate-600">
The selected item has not emitted any logs yet.
</p>
</div>
</div>
<div class={[
"overflow-x-auto overflow-y-auto max-h-96",
@empty_logs && "hidden"
]}>
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-slate-50">
<tr>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-medium text-slate-600 uppercase tracking-wider w-32"
>
Timestamp
</th>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-medium text-slate-600 uppercase tracking-wider w-20"
>
Attempt
</th>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-medium text-slate-600 uppercase tracking-wider w-20"
>
Level
</th>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-medium text-slate-600 uppercase tracking-wider"
>
Content
</th>
</tr>
</thead>
<tbody
id="log-list"
class="bg-white divide-y divide-gray-200"
phx-update="stream"
>
<tr
:for={
{id,
%Flows.Log{
attempt: attempt,
inserted_at: inserted_at,
level: level,
content: content
}} <-
@streams.logs
}
id={id}
>
<td class="px-3 py-2 whitespace-nowrap text-xs text-gray-800 font-mono">
{inserted_at}
</td>
<td class="px-3 py-2 whitespace-nowrap text-center align-middle">
<div class="text-xs badge">{attempt}</div>
</td>
<td class="px-3 py-2 whitespace-nowrap">
<.log_badge level={level} />
</td>
<td class="px-3 py-2 text-xs text-gray-800 bg-slate-100">
{content}
</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
<div class="dag-card mb-4">
<section class="dag-card__header">
<div class="dag-card__title-row">
<h3 class={["dag-card__title dag-card__title-with-icon", "text-base"]}>
<.icon name="hero-chart-bar" class="h-5 w-5 text-sky-600" />
<span>Graph</span>
</h3>
</div>
</section>
<div class="dag-card__body">
<div class="bg-slate-50 p-3 rounded-lg border border-slate-200/70 overflow-auto">
<pre class="mermaid" phx-hook="Mermaid" id="mermaid-chart">
{"""
---
config:
theme: 'base'
themeVariables:
primaryColor: '#e8f2ff'
primaryTextColor: '#0f172a'
primaryBorderColor: '#0ea5e9'
lineColor: '#475569'
secondaryColor: '#c7e8ff'
tertiaryColor: '#f8fafc'
---
#{mermaid_chart(@dag_def.tasks)}
"""}
</pre>
</div>
</div>
</div>
<div class="dag-card mb-4">
<section class="dag-card__header">
<div class="dag-card__title-row">
<h3 class={["dag-card__title dag-card__title-with-icon", "text-base"]}>
<.icon name="hero-code-bracket" class="h-5 w-5 text-sky-600" />
<span>Code</span>
</h3>
<span id="reload-time" class="badge">{reload_time(@reload_dag_file)}</span>
</div>
</section>
<div class="dag-card__body">
<pre class="language-elixir rounded-md overflow-x-auto">
<code id="code-highlight" class={"language-#{@dag_def.adapter} code-block p-3 text-xs leading-relaxed"} phx-hook="CodeHighlight">
{read_code(@reload_dag_file)}
</code>
</pre>
</div>
</div>
</Layouts.app>