Skip to content

Commit

Permalink
feat: change UI to react-admin
Browse files Browse the repository at this point in the history
Switch from a custom management UI to react-admin managed by vite. The
build artifact by vite is used in the ui crate.

There is a justfile that will build the entire project.

UI features:

- origin management
- view requests
- view attempts
  • Loading branch information
hjr3 committed Jan 13, 2024
1 parent b18a857 commit 066719f
Show file tree
Hide file tree
Showing 44 changed files with 5,907 additions and 377 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
target
soldr.db
crates/ui/static
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,30 @@ This request will be retried repeatedly by the retry queue.

In the terminal window running soldr, you will see a lot of tracing output that demonstrates how soldr is running.

### Management UI

The management UI makes it easier to manage the proxy settings and troubleshoot issues. It runs independently of the proxy server. It requires a URL to the management API.

Start the proxy server:

```
RUST_LOG=soldr=info cargo run -- --config-path=soldr.example.toml
Running `target/debug/soldr --config-path=soldr.example.toml`
2024-01-01T20:30:23.213548Z INFO soldr: starting retry queue
2024-01-01T20:30:23.213548Z INFO soldr: ingest listening on 0.0.0.0:3000
2024-01-01T20:30:23.213549Z INFO soldr: management API listening on 0.0.0.0:3443
```

The management API is listening on port 3443. We can run the UI local to the server like this:

```
RUST_LOG=ui=info cargo run --bin ui -- --api-url=http://localhost:3443
Compiling ui v0.0.0 (/Users/herman/Code/soldr/crates/ui)
Finished dev [unoptimized + debuginfo] target(s) in 3.26s
Running `/Users/herman/Code/soldr/target/debug/ui '--api-url=http://localhost:3443'`
2024-01-13T17:09:52.115907Z INFO ui: listening on 127.0.0.1:8888
```

## Use Cases

- You have a service that sends transactional emails to an email service provider (ESP) and your ESP has planned (or unplanned!) downtime. Soldr will alert you, queue your messages and retry them until the service comes back up.
Expand Down
2 changes: 1 addition & 1 deletion crates/proxy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tokio = { version = "1.0", features = ["full"] }
toml = "0.7.5"
tower = { version = "0.4", features = ["util"] }
tower-http = { version = "0.4.0", features = ["trace"] }
tower-http = { version = "0.4.0", features = ["trace", "cors"] }

[dev-dependencies]
criterion = {version = "0.4", features = ["async_tokio"]}
Expand Down
171 changes: 156 additions & 15 deletions crates/proxy/src/db.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
use anyhow::Result;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use sqlx::sqlite::{SqlitePool, SqliteQueryResult};
use sqlx::{query_builder::QueryBuilder, FromRow, Row};

use shared_types::{NewOrigin, Origin};

use crate::request::HttpRequest;
use crate::retry::backoff;

#[derive(Debug, Deserialize, Serialize)]
pub struct GetListResponse<T> {
pub total: i64,
pub items: Vec<T>,
}

#[derive(Debug)]
pub struct QueuedRequest {
pub id: i64,
Expand Down Expand Up @@ -48,6 +55,7 @@ pub struct Request {
pub headers: String,
pub body: Option<Vec<u8>>,
pub state: RequestState,
pub created_at: i64,
pub retry_ms_at: i64,
}

Expand All @@ -57,6 +65,7 @@ pub struct Attempt {
pub request_id: i64,
pub response_status: i64,
pub response_body: Vec<u8>,
pub created_at: i64,
}

pub async fn ensure_schema(pool: &SqlitePool) -> Result<()> {
Expand Down Expand Up @@ -242,16 +251,58 @@ pub async fn list_failed_requests(pool: &SqlitePool) -> Result<Vec<QueuedRequest
Ok(queued_requests)
}

pub async fn list_requests(pool: &SqlitePool) -> Result<Vec<Request>> {
pub async fn list_requests(
pool: &SqlitePool,
start: u32,
end: u32,
field: &str,
order: &str,
states: Option<Vec<RequestState>>,
) -> Result<GetListResponse<Request>> {
tracing::trace!("list_requests");
let mut conn = pool.acquire().await?;

let requests =
sqlx::query_as::<_, Request>("SELECT * FROM requests ORDER BY id DESC LIMIT 10;")
.fetch_all(&mut *conn)
.await?;
let mut q = QueryBuilder::new("SELECT *, COUNT(*) OVER() AS total FROM requests");

if let Some(states) = states {
q.push(" WHERE state IN (");
for (i, state) in states.iter().enumerate() {
q.push_bind(*state);
if i < states.len() - 1 {
q.push(", ");
}
}
q.push(")");
}

q.push(" ORDER BY ");
q.push_bind(field);

q.push(&format!(" {} LIMIT ", order));
q.push_bind(end - start + 1);
q.push(" OFFSET ");
q.push_bind(start);

Ok(requests)
let q = q.build();
let rows = q.fetch_all(&mut *conn).await?;

let total = if rows.is_empty() {
0
} else {
rows.get(0)
.map(|row| row.try_get("total"))
.context("Failed to get total count of requests.")??
};

let requests: Vec<Request> = rows
.into_iter()
.map(|row| Request::from_row(&row))
.collect::<Result<Vec<_>, _>>()?;

Ok(GetListResponse {
total,
items: requests,
})
}

pub async fn insert_attempt(
Expand Down Expand Up @@ -299,15 +350,52 @@ pub async fn insert_attempt(
Ok(id)
}

pub async fn list_attempts(pool: &SqlitePool) -> Result<Vec<Attempt>> {
pub async fn list_attempts(
pool: &SqlitePool,
start: u32,
end: u32,
field: &str,
order: &str,
request_id: Option<i64>,
) -> Result<GetListResponse<Attempt>> {
tracing::trace!("list_attempts");
let mut conn = pool.acquire().await?;

let attempts = sqlx::query_as::<_, Attempt>("SELECT * FROM attempts ORDER BY id DESC;")
.fetch_all(&mut *conn)
.await?;
let mut q = QueryBuilder::new("SELECT *, COUNT(*) OVER() AS total FROM attempts");

if let Some(request_id) = request_id {
q.push(" WHERE request_id = ");
q.push_bind(request_id);
}

q.push(" ORDER BY ");
q.push_bind(field);

q.push(&format!(" {} LIMIT ", order));
q.push_bind(end - start + 1);
q.push(" OFFSET ");
q.push_bind(start);

let q = q.build();
let rows = q.fetch_all(&mut *conn).await?;

let total = if rows.is_empty() {
0
} else {
rows.get(0)
.map(|row| row.try_get("total"))
.context("Failed to get total count of attempts.")??
};

let attempts: Vec<Attempt> = rows
.into_iter()
.map(|row| Attempt::from_row(&row))
.collect::<Result<Vec<_>, _>>()?;

Ok(attempts)
Ok(GetListResponse {
total,
items: attempts,
})
}

pub async fn insert_origin(pool: &SqlitePool, origin: NewOrigin) -> Result<Origin> {
Expand Down Expand Up @@ -404,15 +492,44 @@ pub async fn update_origin(pool: &SqlitePool, id: i64, origin: NewOrigin) -> Res
Ok(updated_origin)
}

pub async fn list_origins(pool: &SqlitePool) -> Result<Vec<Origin>> {
pub async fn list_origins(
pool: &SqlitePool,
start: u32,
end: u32,
field: &str,
order: &str,
) -> Result<GetListResponse<Origin>> {
tracing::trace!("list_origins");
let mut conn = pool.acquire().await?;

let origins = sqlx::query_as::<_, Origin>("SELECT * FROM origins;")
let q = format!(
"SELECT *, COUNT(*) OVER() AS total FROM origins ORDER BY ? {} LIMIT ? OFFSET ?;",
order
);
let rows = sqlx::query(&q)
.bind(field)
.bind(end - start + 1)
.bind(start)
.fetch_all(&mut *conn)
.await?;

Ok(origins)
let total = if rows.is_empty() {
0
} else {
rows.get(0)
.map(|row| row.try_get("total"))
.context("Failed to get total count of origins.")??
};

let origins: Vec<Origin> = rows
.into_iter()
.map(|row| Origin::from_row(&row))
.collect::<Result<Vec<_>, _>>()?;

Ok(GetListResponse {
total,
items: origins,
})
}

pub async fn get_origin(pool: &SqlitePool, id: i64) -> Result<Origin> {
Expand Down Expand Up @@ -481,3 +598,27 @@ pub async fn add_request_to_queue(pool: &SqlitePool, req_id: i64) -> Result<()>

Ok(())
}

pub async fn get_request(pool: &SqlitePool, id: i64) -> Result<Request> {
tracing::trace!("get_origin");
let mut conn = pool.acquire().await?;

let request = sqlx::query_as::<_, Request>("SELECT * FROM requests WHERE id = ?;")
.bind(id)
.fetch_one(&mut *conn)
.await?;

Ok(request)
}

pub async fn get_attempt(pool: &SqlitePool, id: i64) -> Result<Attempt> {
tracing::trace!("get_origin");
let mut conn = pool.acquire().await?;

let attempt = sqlx::query_as::<_, Attempt>("SELECT * FROM attempts WHERE id = ?;")
.bind(id)
.fetch_one(&mut *conn)
.await?;

Ok(attempt)
}
Loading

0 comments on commit 066719f

Please sign in to comment.