feat: pico css styling and not-found page.

Changes:

 1. integrates pico minimal responsive css framework for light/dark
    themes and overall better looks.

 2. adds a not-found page for bad url queries.

 3. catches axum/serde deserialization errors of user input and routes
    them to the not-found page with a helpful hint (parse error msg)
This commit is contained in:
danda 2024-05-10 14:20:43 -07:00
parent e53d80d9fa
commit 0f9cbdf0b4
11 changed files with 189 additions and 97 deletions

View File

@ -1,7 +1,8 @@
use crate::html::component::header::HeaderHtml; use crate::html::component::header::HeaderHtml;
use crate::html::page::not_found::not_found_html_response;
use crate::model::app_state::AppState; use crate::model::app_state::AppState;
use crate::model::path_block_selector::PathBlockSelector; use crate::model::path_block_selector::PathBlockSelector;
use crate::rpc::block_info::block_info_with_value_worker; use axum::extract::rejection::PathRejection;
use axum::extract::Path; use axum::extract::Path;
use axum::extract::State; use axum::extract::State;
use axum::response::Html; use axum::response::Html;
@ -10,18 +11,22 @@ use html_escaper::Escape;
use html_escaper::Trusted; use html_escaper::Trusted;
use neptune_core::rpc_server::BlockInfo; use neptune_core::rpc_server::BlockInfo;
use std::sync::Arc; use std::sync::Arc;
use tarpc::context;
pub async fn block_page( pub async fn block_page(
Path(path_block_selector): Path<PathBlockSelector>, user_input_maybe: Result<Path<PathBlockSelector>, PathRejection>,
state: State<Arc<AppState>>, state: State<Arc<AppState>>,
) -> Result<Html<String>, Response> { ) -> Result<Html<String>, Response> {
let Path(path_block_selector) = user_input_maybe
.map_err(|e| not_found_html_response(state.clone(), Some(e.to_string())))?;
let value_path: Path<(PathBlockSelector, String)> = Path((path_block_selector, "".to_string())); let value_path: Path<(PathBlockSelector, String)> = Path((path_block_selector, "".to_string()));
block_page_with_value(value_path, state).await block_page_with_value(Ok(value_path), state).await
} }
#[axum::debug_handler] #[axum::debug_handler]
pub async fn block_page_with_value( pub async fn block_page_with_value(
Path((path_block_selector, value)): Path<(PathBlockSelector, String)>, user_input_maybe: Result<Path<(PathBlockSelector, String)>, PathRejection>,
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
) -> Result<Html<String>, Response> { ) -> Result<Html<String>, Response> {
#[derive(boilerplate::Boilerplate)] #[derive(boilerplate::Boilerplate)]
@ -31,12 +36,32 @@ pub async fn block_page_with_value(
block_info: BlockInfo, block_info: BlockInfo,
} }
let Path((path_block_selector, value)) = user_input_maybe
.map_err(|e| not_found_html_response(State(state.clone()), Some(e.to_string())))?;
let header = HeaderHtml { let header = HeaderHtml {
site_name: "Neptune Explorer".to_string(), site_name: "Neptune Explorer".to_string(),
state: state.clone(), state: state.clone(),
}; };
let block_info = block_info_with_value_worker(state, path_block_selector, &value).await?; let block_selector = path_block_selector
.as_block_selector(&value)
.map_err(|e| not_found_html_response(State(state.clone()), Some(e.to_string())))?;
let block_info = match state
.clone()
.rpc_client
.block_info(context::current(), block_selector)
.await
.map_err(|e| not_found_html_response(State(state.clone()), Some(e.to_string())))?
{
Some(info) => Ok(info),
None => Err(not_found_html_response(
State(state),
Some("Block does not exist".to_string()),
)),
}?;
let block_info_page = BlockInfoHtmlPage { header, block_info }; let block_info_page = BlockInfoHtmlPage { header, block_info };
Ok(Html(block_info_page.to_string())) Ok(Html(block_info_page.to_string()))
} }

View File

@ -1,3 +1,4 @@
pub mod block; pub mod block;
pub mod not_found;
pub mod root; pub mod root;
pub mod utxo; pub mod utxo;

View File

@ -1,7 +1,7 @@
use crate::html::component::header::HeaderHtml; use crate::html::component::header::HeaderHtml;
use crate::http_util::not_found_err; use crate::html::page::not_found::not_found_html_response;
use crate::http_util::rpc_err;
use crate::model::app_state::AppState; use crate::model::app_state::AppState;
use axum::extract::rejection::PathRejection;
use axum::extract::Path; use axum::extract::Path;
use axum::extract::State; use axum::extract::State;
use axum::response::Html; use axum::response::Html;
@ -14,7 +14,7 @@ use tarpc::context;
#[axum::debug_handler] #[axum::debug_handler]
pub async fn utxo_page( pub async fn utxo_page(
Path(index): Path<u64>, index_maybe: Result<Path<u64>, PathRejection>,
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
) -> Result<Html<String>, Response> { ) -> Result<Html<String>, Response> {
#[derive(boilerplate::Boilerplate)] #[derive(boilerplate::Boilerplate)]
@ -25,14 +25,22 @@ pub async fn utxo_page(
digest: Digest, digest: Digest,
} }
let Path(index) = index_maybe
.map_err(|e| not_found_html_response(State(state.clone()), Some(e.to_string())))?;
let digest = match state let digest = match state
.rpc_client .rpc_client
.utxo_digest(context::current(), index) .utxo_digest(context::current(), index)
.await .await
.map_err(rpc_err)? .map_err(|e| not_found_html_response(State(state.clone()), Some(e.to_string())))?
{ {
Some(digest) => digest, Some(digest) => digest,
None => return Err(not_found_err()), None => {
return Err(not_found_html_response(
State(state.clone()),
Some("The requested UTXO does not exist".to_string()),
))
}
}; };
let header = HeaderHtml { let header = HeaderHtml {

View File

@ -1,4 +1,5 @@
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::Html;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::response::Response; use axum::response::Response;
use tarpc::client::RpcError; use tarpc::client::RpcError;
@ -10,6 +11,10 @@ pub fn not_found_err() -> Response {
(StatusCode::NOT_FOUND, "Not Found".to_string()).into_response() (StatusCode::NOT_FOUND, "Not Found".to_string()).into_response()
} }
pub fn not_found_html_err(html: Html<String>) -> Response {
(StatusCode::NOT_FOUND, html).into_response()
}
pub fn rpc_err(e: RpcError) -> Response { pub fn rpc_err(e: RpcError) -> Response {
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response() (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
} }

View File

@ -58,6 +58,13 @@ async fn main() -> Result<(), RpcError> {
.route("/block/:selector/:value", get(block_page_with_value)) .route("/block/:selector/:value", get(block_page_with_value))
.route("/utxo/:value", get(utxo_page)) .route("/utxo/:value", get(utxo_page))
// -- Static files -- // -- Static files --
.route_service(
"/css/pico.min.css",
ServeFile::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/templates/web/css/pico.min.css"
)),
)
.route_service( .route_service(
"/css/styles.css", "/css/styles.css",
ServeFile::new(concat!( ServeFile::new(concat!(

View File

@ -1,42 +1,8 @@
div.box h1,
div.box h2,
div.box h3 {
margin-top: 0px;
margin-bottom: 5px;
}
div.indent { div.indent {
position: relative; position: relative;
left: 20px; left: 20px;
} }
div.box { .center-text {
margin-bottom: 10px; text-align: center;
border: solid 1px #ccc;
border-top-left-radius: 15px;
padding: 10px;
}
body {
font-family: arial, helvetica;
}
table.alt {
margin-top: 10px;
margin-bottom: 10px;
padding: 10px;
border-collapse: collapse;
}
table.alt td,
th {
padding: 5px;
}
table.alt tr:nth-child(odd) td {
background: #eee;
}
table.alt tr:nth-child(even) td {
background: #fff;
} }

View File

@ -1 +1,3 @@
<header class="container">
<h1>{{self.site_name}} : {{self.state.network}}</h1> <h1>{{self.site_name}} : {{self.state.network}}</h1>
</header>

View File

@ -1,13 +1,19 @@
<!doctype html>
<html> <html>
<head> <head>
<title>Neptune Block Explorer: Block Height {{self.block_info.height}}</title> <title>Neptune Block Explorer: Block Height {{self.block_info.height}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/css/pico.min.css" media="screen" />
<link rel="stylesheet" type="text/css" href="/css/styles.css" media="screen" /> <link rel="stylesheet" type="text/css" href="/css/styles.css" media="screen" />
</head> </head>
<body> <body>
{{Trusted(self.header.to_string())}} {{Trusted(self.header.to_string())}}
<main class="container">
<article>
<h2>Block height: {{self.block_info.height}}</h2> <h2>Block height: {{self.block_info.height}}</h2>
<!-- special_block_notice --> <!-- special_block_notice -->
@ -18,7 +24,7 @@
<p>This is the Latest Block (tip)</p> <p>This is the Latest Block (tip)</p>
%% } %% }
<table class="alt"> <table class="striped">
<tr> <tr>
<td>Digest</td> <td>Digest</td>
<td>{{self.block_info.digest.to_hex()}}</td> <td>{{self.block_info.digest.to_hex()}}</td>
@ -53,6 +59,10 @@
</tr> </tr>
</table> </table>
</article>
<article>
<p> <p>
<a href="/">Home</a> <a href="/">Home</a>
| <a href='/block/genesis'>Genesis</a> | <a href='/block/genesis'>Genesis</a>
@ -70,5 +80,7 @@
%% } %% }
</p> </p>
</article>
</main>
</body> </body>
</html> </html>

View File

@ -0,0 +1,37 @@
<html>
<head>
<title>Neptune Block Explorer: Not Found</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/css/pico.min.css" media="screen" />
<link rel="stylesheet" type="text/css" href="/css/styles.css" media="screen" />
</head>
<body>
<main class="container">
<article>
<header class="center-text"><h3>Not found</h3></header>
<section>
These are not the droids you're looking for.
</section>
%% if self.error_msg.len() > 0 {
<section>
Hint: {{self.error_msg}}
</section>
%% }
</article>
<article>
<p>
<a href="/">Home</a>
| <a href='/block/genesis'>Genesis</a>
| <a href='/block/tip'>Tip</a>
</p>
</article>
</main>
</body>
</html>

View File

@ -2,28 +2,36 @@
<head> <head>
<title>Neptune Block Explorer: (network: {{self.network}})</title> <title>Neptune Block Explorer: (network: {{self.network}})</title>
<link rel="stylesheet" type="text/css" href="/css/styles.css" media="screen" /> <link rel="stylesheet" type="text/css" href="/css/styles.css" media="screen" />
</head> <link rel="stylesheet" type="text/css" href="/css/pico.min.css" media="screen" />
<body>
<h1>Neptune Block Explorer (network: {{self.network}})</h1>
<script> <script>
function handle_submit(form){ function handle_submit(form) {
let value = form.height_or_digest.value; let value = form.height_or_digest.value;
var is_digest = value.length == 80; var is_digest = value.length == 80;
var type = is_digest ? "digest" : "height"; var type = is_digest ? "digest" : "height";
var uri = form.action + "/" + type + "/" + value; var uri = form.action + "/" + type + "/" + value;
window.location.href = uri; window.location.href = uri;
return false; return false;
} }
function handle_utxo_submit(form) { function handle_utxo_submit(form) {
let value = form.utxo.value; let value = form.utxo.value;
var uri = form.action + "/" + value; var uri = form.action + "/" + value;
window.location.href = uri; window.location.href = uri;
return false; return false;
} }
</script> </script>
<div class="box"> </head>
<h3>Block Lookup</h3> <body>
<header class="container">
<h1>Neptune Block Explorer (network: {{self.network}})</h1>
</header>
<main class="container">
<article>
<details open>
<summary>Block Lookup</summary>
<form action="/block" method="get" onsubmit="return handle_submit(this)"> <form action="/block" method="get" onsubmit="return handle_submit(this)">
Block height or digest: Block height or digest:
<input type="text" size="80" name="height_or_digest"/> <input type="text" size="80" name="height_or_digest"/>
@ -33,58 +41,70 @@ Block height or digest:
Quick Lookup: Quick Lookup:
<a href="/block/genesis">Genesis Block</a> | <a href="/block/genesis">Genesis Block</a> |
<a href="/block/tip">Tip</a><br/> <a href="/block/tip">Tip</a><br/>
</div> </details>
</article>
<article>
<div class="box"> <details>
<h3>Utxo Lookup</h3> <summary>Utxo Lookup</summary>
<form action="/utxo" method="get" onsubmit="return handle_utxo_submit(this)"> <form action="/utxo" method="get" onsubmit="return handle_utxo_submit(this)">
Utxo index: Utxo index:
<input type="text" size="10" name="utxo" /> <input type="text" size="10" name="utxo" />
<input type="submit" name="height" value="Lookup Utxo" /> <input type="submit" name="height" value="Lookup Utxo" />
</form> </form>
</div> </details>
</article>
<h2>REST RPCs</h2> <h2>REST RPCs</h2>
<div class="box"> <article>
<details>
<h3>/block_info</h3> <summary>/block_info</summary>
<div class="indent"> <div class="indent">
<h4>Examples</h4> <h4>Examples</h4>
<a href="/rpc/block_info/genesis">/rpc/block_info/genesis</a><br/> <ul>
<a href="/rpc/block_info/tip">/rpc/block_info/tip</a><br/> <li><a href="/rpc/block_info/genesis">/rpc/block_info/genesis</a></li>
<a href="/rpc/block_info/height/2">/rpc/block_info/height/2</a><br/> <li><a href="/rpc/block_info/tip">/rpc/block_info/tip</a></li>
<a href="/rpc/block_info/digest/{{self.genesis_digest.to_hex()}}">/rpc/block_info/digest/{{self.genesis_digest.to_hex()}}</a><br/> <li><a href="/rpc/block_info/height/2">/rpc/block_info/height/2</a></li>
<a href="/rpc/block_info/height_or_digest/1">/rpc/block_info/height_or_digest/1</a><br/> <li><a href="/rpc/block_info/digest/{{self.genesis_digest.to_hex()}}">/rpc/block_info/digest/{{self.genesis_digest.to_hex()}}</a></li>
</div> <li><a href="/rpc/block_info/height_or_digest/1">/rpc/block_info/height_or_digest/1</a></li>
</ul>
</div> </div>
</details>
</article>
<div class="box"> <article>
<details>
<h3>/block_digest</h3> <summary>/block_digest</summary>
<div class="indent"> <div class="indent">
<h4>Examples</h4> <h4>Examples</h4>
<a href="/rpc/block_digest/genesis">/rpc/block_digest/genesis</a><br/> <ul>
<a href="/rpc/block_digest/tip">/rpc/block_digest/tip</a><br/> <li><a href="/rpc/block_digest/genesis">/rpc/block_digest/genesis</a></li>
<a href="/rpc/block_digest/height/2">/rpc/block_digest/height/2</a><br/> <li><a href="/rpc/block_digest/tip">/rpc/block_digest/tip</a></li>
<a href="/rpc/block_digest/digest/{{self.genesis_digest.to_hex()}}">/rpc/block_digest/digest/{{self.genesis_digest.to_hex()}}</a><br/> <li><a href="/rpc/block_digest/height/2">/rpc/block_digest/height/2</a></li>
<a href="/rpc/block_digest/height_or_digest/{{self.genesis_digest.to_hex()}}">/rpc/block_digest/height_or_digest/{{self.genesis_digest.to_hex()}}</a><br/> <li><a href="/rpc/block_digest/digest/{{self.genesis_digest.to_hex()}}">/rpc/block_digest/digest/{{self.genesis_digest.to_hex()}}</a></li>
</div> <li><a href="/rpc/block_digest/height_or_digest/{{self.genesis_digest.to_hex()}}">/rpc/block_digest/height_or_digest/{{self.genesis_digest.to_hex()}}</a></li>
</ul>
</div> </div>
</details>
</article>
<div class="box"> <article>
<details>
<h3>/utxo_digest</h3> <summary>/utxo_digest</summary>
<div class="indent"> <div class="indent">
<h4>Examples</h4> <h4>Examples</h4>
<a href="/rpc/utxo_digest/2">/rpc/utxo_digest/2</a><br/> <ul>
<li><a href="/rpc/utxo_digest/2">/rpc/utxo_digest/2</a><br/></li>
</ul>
</div> </div>
</details>
</article>
</div> </main>
</body> </body>
</html> </html>

View File

@ -2,14 +2,19 @@
<head> <head>
<title>Neptune Block Explorer: Utxo {{self.index}}</title> <title>Neptune Block Explorer: Utxo {{self.index}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/css/pico.min.css" media="screen" />
<link rel="stylesheet" type="text/css" href="/css/styles.css" media="screen" /> <link rel="stylesheet" type="text/css" href="/css/styles.css" media="screen" />
</head> </head>
<body> <body>
{{Trusted(self.header.to_string())}} {{Trusted(self.header.to_string())}}
<h3>Utxo Information</h3> <main class="container">
<table class="alt">
<article>
<summary>Utxo Information</summary>
<table class="striped">
<tr> <tr>
<td>Index</td> <td>Index</td>
<td>{{self.index}}</td> <td>{{self.index}}</td>
@ -19,12 +24,16 @@
<td>{{self.digest.to_hex()}}</td> <td>{{self.digest.to_hex()}}</td>
</tr> </tr>
</table> </table>
</article>
<article>
<p> <p>
<a href="/">Home</a> <a href="/">Home</a>
| <a href='/block/genesis'>Genesis</a> | <a href='/block/genesis'>Genesis</a>
| <a href='/block/tip'>Tip</a> | <a href='/block/tip'>Tip</a>
</p> </p>
</article>
</main>
</body> </body>
</html> </html>