Skip to content

Commit 16447fa

Browse files
committed
implement base admin panel
1 parent 2c70474 commit 16447fa

7 files changed

Lines changed: 286 additions & 4 deletions

File tree

admin_app/src/lib/api.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export interface Task {
2+
id: string;
3+
execution_date: string;
4+
webhook_name: string;
5+
executed_command: string;
6+
command_exit_code: number;
7+
command_stdout: string;
8+
command_stderr: string;
9+
}
10+
11+
export interface FetchTasksOptions {
12+
cursor?: string;
13+
limit?: number;
14+
}
15+
16+
export async function fetchTasks(options: FetchTasksOptions = {}): Promise<Task[]> {
17+
try {
18+
const params = new URLSearchParams();
19+
if (options.cursor) {
20+
params.append('cursor', options.cursor);
21+
}
22+
if (options.limit) {
23+
params.append('limit', options.limit.toString());
24+
}
25+
26+
const url = params.toString() ? `/api/tasks?${params.toString()}` : '/api/tasks';
27+
const response = await fetch(url);
28+
29+
if (!response.ok) {
30+
return Promise.reject(Error(`HTTP ${response.status}: Failed to fetch tasks`));
31+
}
32+
33+
return await response.json();
34+
} catch (error) {
35+
console.error('Error fetching tasks:', error);
36+
throw error;
37+
}
38+
}

admin_app/src/routes/+page.svelte

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,164 @@
1-
<h1>Pagoo admin</h1>
1+
<script lang="ts">
2+
import { fetchTasks } from '$lib/api';
3+
import type { Task } from '$lib/api';
4+
5+
const ITEMS_PER_PAGE = 30;
6+
7+
let allTasks = $state<Task[]>([]);
8+
let loading = $state(true);
9+
let alertMessage = $state<string | null>(null);
10+
let messageType = $state<'danger'|'warning'>('danger');
11+
let currentPage = $state(0);
12+
13+
let latestCursor = $state<string | null>(null);
14+
15+
$effect.pre(async () => {
16+
await syncTasks();
17+
});
18+
19+
async function syncTasks() {
20+
loading = true;
21+
alertMessage = null;
22+
try {
23+
const newTasks = await fetchTasks({ cursor: latestCursor, limit: ITEMS_PER_PAGE });
24+
if (newTasks.length > 0) {
25+
allTasks = [...newTasks, ...allTasks];
26+
latestCursor = newTasks[0]?.execution_date || latestCursor;
27+
currentPage = 0;
28+
}
29+
} catch (err) {
30+
alertMessage = err instanceof Error ? err.message : 'Failed to sync tasks';
31+
messageType = 'danger';
32+
} finally {
33+
loading = false;
34+
}
35+
}
36+
37+
let paginatedTasks = $derived.by(() => {
38+
const start = currentPage * ITEMS_PER_PAGE;
39+
const end = start + ITEMS_PER_PAGE;
40+
return allTasks.slice(start, end);
41+
});
42+
43+
let totalPages = $derived.by(() => {
44+
return Math.ceil(allTasks.length / ITEMS_PER_PAGE);
45+
});
46+
47+
function nextPage() {
48+
if (currentPage < totalPages - 1) {
49+
currentPage++;
50+
}
51+
}
52+
53+
function prevPage() {
54+
if (currentPage > 0) {
55+
currentPage--;
56+
}
57+
}
58+
</script>
59+
60+
<div class="container-fluid mt-4">
61+
<div class="row">
62+
<div class="col align-self-start">
63+
<h1>Pagoo Admin</h1>
64+
</div>
65+
<div class="col-sm-3 align-self-end btn-group-vertical btn-group-lg">
66+
<button class="btn btn-primary btn-block" disabled={loading} onclick={syncTasks}>
67+
{#if loading}
68+
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
69+
Syncing...
70+
{:else}
71+
🔄 Sync
72+
{/if}
73+
</button>
74+
</div>
75+
</div>
76+
77+
{#if alertMessage}
78+
<div class="alert alert-{messageType} alert-dismissible fade show" role="alert">
79+
{alertMessage}
80+
<button type="button" class="btn-close" onclick={() => (alertMessage = null)}></button>
81+
</div>
82+
{/if}
83+
84+
{#if !loading && allTasks.length === 0 && !alertMessage}
85+
<div class="alert alert-secondary" role="alert">
86+
No tasks found. Run some webhooks to see them here.
87+
</div>
88+
{/if}
89+
90+
{#if allTasks.length > 0}
91+
<div class="mb-3">
92+
<p class="text-muted">
93+
Showing {currentPage * ITEMS_PER_PAGE + 1} to {Math.min((currentPage + 1) * ITEMS_PER_PAGE, allTasks.length)} of {allTasks.length} tasks
94+
</p>
95+
</div>
96+
97+
<div class="table-responsive">
98+
<table class="table table-striped table-hover">
99+
<thead class="table-dark">
100+
<tr>
101+
<th>#</th>
102+
<th>Execution Date</th>
103+
<th>Webhook Name</th>
104+
<th>Command</th>
105+
<th>Exit Code</th>
106+
<th>Stdout</th>
107+
<th>Stderr</th>
108+
</tr>
109+
</thead>
110+
<tbody>
111+
{#each paginatedTasks as task}
112+
<tr>
113+
<td>{task.id}</td>
114+
<td>{task.execution_date}</td>
115+
<td>{task.webhook_name}</td>
116+
<td><code>{task.executed_command}</code></td>
117+
<td>
118+
<span class="badge {task.command_exit_code === 0 ? 'bg-success' : 'bg-danger'}">
119+
{task.command_exit_code}
120+
</span>
121+
</td>
122+
<td>
123+
<pre class="output-code p-3 border border-1 rounded-3 text-muted bg-primary-subtle border-primary" title={task.command_stdout}>
124+
{task.command_stdout}
125+
</pre>
126+
</td>
127+
<td>
128+
<pre class="output-code p-3 border border-1 rounded-3 text-muted bg-danger-subtle border-danger" title={task.command_stderr}>
129+
{task.command_stderr}
130+
</pre>
131+
</td>
132+
</tr>
133+
{/each}
134+
</tbody>
135+
</table>
136+
</div>
137+
138+
{#if totalPages > 1}
139+
<nav aria-label="Page navigation" class="mt-4">
140+
<ul class="pagination justify-content-center">
141+
<li class="page-item {currentPage === 0 ? 'disabled' : ''}">
142+
<button class="page-link" onclick={prevPage} disabled={currentPage === 0}>Previous</button>
143+
</li>
144+
{#each Array.from({ length: totalPages }) as _, i (i)}
145+
<li class="page-item {currentPage === i ? 'active' : ''}">
146+
<button class="page-link" onclick={() => (currentPage = i)}>
147+
{i + 1}
148+
</button>
149+
</li>
150+
{/each}
151+
<li class="page-item {currentPage === totalPages - 1 ? 'disabled' : ''}">
152+
<button class="page-link" onclick={nextPage} disabled={currentPage === totalPages - 1}>Next</button>
153+
</li>
154+
</ul>
155+
</nav>
156+
{/if}
157+
{/if}
158+
</div>
159+
160+
<style>
161+
.output-code {
162+
resize: both;
163+
}
164+
</style>

src/actions/executor.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ pub(crate) fn execute_webhook_actions(
2424
cmd.args(actions.clone());
2525
let output: std::io::Result<Output> = cmd.output();
2626

27-
let mut status: i32;
27+
let status: i32;
2828
let mut stdout_str = String::from("");
29-
let mut stderr_str: String;
29+
let stderr_str: String;
3030

3131
if output.is_ok() {
3232
let output_result = output.unwrap();

src/db/migrations/01-logs_ids.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ALTER TABLE logs_webhooks ADD COLUMN id VARCHAR(255) NOT NULL DEFAULT (lower(hex(randomblob(16))));
2+
3+
CREATE UNIQUE INDEX id ON logs_webhooks(id);

src/db/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,8 @@ fn get_database_flags() -> OpenFlags {
4747
}
4848

4949
fn get_migrations() -> Migrations<'static> {
50-
Migrations::new(vec![M::up(include_str!("./migrations/00-schema.sql"))])
50+
Migrations::new(vec![
51+
M::up(include_str!("./migrations/00-schema.sql")),
52+
M::up(include_str!("./migrations/01-logs_ids.sql"))
53+
])
5154
}

src/http/admin.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
use actix_web::get;
2+
use actix_web::web;
23
use actix_web::HttpResponse;
34
use actix_web::Responder;
5+
use rusqlite::Connection;
6+
use serde::Serialize;
7+
use std::sync::Arc;
8+
use std::sync::Mutex;
49

510
pub(crate) fn frontend_assets(path: String) -> Option<HttpResponse> {
611
let path = &mut path.clone();
@@ -25,7 +30,76 @@ pub(crate) fn frontend_assets(path: String) -> Option<HttpResponse> {
2530
)
2631
}
2732

33+
#[derive(Serialize)]
34+
pub(crate) struct Task {
35+
pub id: String,
36+
pub execution_date: String,
37+
pub webhook_name: String,
38+
pub executed_command: String,
39+
pub command_exit_code: i32,
40+
pub command_stdout: String,
41+
pub command_stderr: String,
42+
}
43+
2844
#[get("/api")]
2945
pub(crate) async fn api_root() -> impl Responder {
3046
"Api endpoint.".to_string()
3147
}
48+
49+
#[get("/api/tasks")]
50+
pub(crate) async fn get_tasks(
51+
db: web::Data<Arc<Mutex<Connection>>>,
52+
query: web::Query<std::collections::HashMap<String, String>>,
53+
) -> actix_web::Result<HttpResponse> {
54+
let db = db.lock().map_err(|_| actix_web::error::ErrorInternalServerError("Database lock failed"))?;
55+
56+
let cursor = query.get("cursor").cloned();
57+
let limit: i32 = query
58+
.get("limit")
59+
.and_then(|s| s.parse().ok())
60+
.unwrap_or(30);
61+
62+
let tasks: Vec<Task> = if let Some(cursor_val) = cursor {
63+
let mut stmt = db
64+
.prepare("SELECT id, execution_date, webhook_name, executed_command, command_exit_code, command_stdout, command_stderr FROM logs_webhooks WHERE execution_date > ? ORDER BY execution_date DESC LIMIT ?")
65+
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to prepare query"))?;
66+
67+
let rows = stmt.query_map(rusqlite::params![cursor_val, limit], |row| {
68+
Ok(Task {
69+
id: row.get(0)?,
70+
execution_date: row.get(1)?,
71+
webhook_name: row.get(2)?,
72+
executed_command: row.get(3)?,
73+
command_exit_code: row.get(4)?,
74+
command_stdout: row.get(5)?,
75+
command_stderr: row.get(6)?,
76+
})
77+
})
78+
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to query tasks"))?;
79+
80+
rows.collect::<Result<Vec<_>, _>>()
81+
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to collect tasks"))?
82+
} else {
83+
let mut stmt = db
84+
.prepare("SELECT id, execution_date, webhook_name, executed_command, command_exit_code, command_stdout, command_stderr FROM logs_webhooks ORDER BY execution_date DESC LIMIT ?")
85+
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to prepare query"))?;
86+
87+
let rows = stmt.query_map(rusqlite::params![limit], |row| {
88+
Ok(Task {
89+
id: row.get(0)?,
90+
execution_date: row.get(1)?,
91+
webhook_name: row.get(2)?,
92+
executed_command: row.get(3)?,
93+
command_exit_code: row.get(4)?,
94+
command_stdout: row.get(5)?,
95+
command_stderr: row.get(6)?,
96+
})
97+
})
98+
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to query tasks"))?;
99+
100+
rows.collect::<Result<Vec<_>, _>>()
101+
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to collect tasks"))?
102+
};
103+
104+
Ok(HttpResponse::Ok().json(tasks))
105+
}

src/serve/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ pub(crate) fn serve_admin(
118118
}
119119
})
120120
.service(http::admin::api_root)
121+
.service(http::admin::get_tasks)
121122
})
122123
.bind((host, port_as_int))?
123124
.run()

0 commit comments

Comments
 (0)