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::page::not_found::not_found_html_response;
use crate::model::app_state::AppState;
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::State;
use axum::response::Html;
@ -10,18 +11,22 @@ use html_escaper::Escape;
use html_escaper::Trusted;
use neptune_core::rpc_server::BlockInfo;
use std::sync::Arc;
use tarpc::context;
pub async fn block_page(
Path(path_block_selector): Path<PathBlockSelector>,
user_input_maybe: Result<Path<PathBlockSelector>, PathRejection>,
state: State<Arc<AppState>>,
) -> 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()));
block_page_with_value(value_path, state).await
block_page_with_value(Ok(value_path), state).await
}
#[axum::debug_handler]
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>>,
) -> Result<Html<String>, Response> {
#[derive(boilerplate::Boilerplate)]
@ -31,12 +36,32 @@ pub async fn block_page_with_value(
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 {
site_name: "Neptune Explorer".to_string(),
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 };
Ok(Html(block_info_page.to_string()))
}

View File

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

View File

@ -1,7 +1,7 @@
use crate::html::component::header::HeaderHtml;
use crate::http_util::not_found_err;
use crate::http_util::rpc_err;
use crate::html::page::not_found::not_found_html_response;
use crate::model::app_state::AppState;
use axum::extract::rejection::PathRejection;
use axum::extract::Path;
use axum::extract::State;
use axum::response::Html;
@ -14,7 +14,7 @@ use tarpc::context;
#[axum::debug_handler]
pub async fn utxo_page(
Path(index): Path<u64>,
index_maybe: Result<Path<u64>, PathRejection>,
State(state): State<Arc<AppState>>,
) -> Result<Html<String>, Response> {
#[derive(boilerplate::Boilerplate)]
@ -25,14 +25,22 @@ pub async fn utxo_page(
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
.rpc_client
.utxo_digest(context::current(), index)
.await
.map_err(rpc_err)?
.map_err(|e| not_found_html_response(State(state.clone()), Some(e.to_string())))?
{
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 {

View File

@ -1,4 +1,5 @@
use axum::http::StatusCode;
use axum::response::Html;
use axum::response::IntoResponse;
use axum::response::Response;
use tarpc::client::RpcError;
@ -10,6 +11,10 @@ pub fn not_found_err() -> 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 {
(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("/utxo/:value", get(utxo_page))
// -- Static files --
.route_service(
"/css/pico.min.css",
ServeFile::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/templates/web/css/pico.min.css"
)),
)
.route_service(
"/css/styles.css",
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 {
position: relative;
left: 20px;
}
div.box {
margin-bottom: 10px;
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;
.center-text {
text-align: center;
}

View File

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

View File

@ -1,13 +1,19 @@
<!doctype html>
<html>
<head>
<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" />
</head>
<body>
{{Trusted(self.header.to_string())}}
<main class="container">
<article>
<h2>Block height: {{self.block_info.height}}</h2>
<!-- special_block_notice -->
@ -18,7 +24,7 @@
<p>This is the Latest Block (tip)</p>
%% }
<table class="alt">
<table class="striped">
<tr>
<td>Digest</td>
<td>{{self.block_info.digest.to_hex()}}</td>
@ -53,6 +59,10 @@
</tr>
</table>
</article>
<article>
<p>
<a href="/">Home</a>
| <a href='/block/genesis'>Genesis</a>
@ -70,5 +80,7 @@
%% }
</p>
</article>
</main>
</body>
</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>
<title>Neptune Block Explorer: (network: {{self.network}})</title>
<link rel="stylesheet" type="text/css" href="/css/styles.css" media="screen" />
</head>
<body>
<h1>Neptune Block Explorer (network: {{self.network}})</h1>
<link rel="stylesheet" type="text/css" href="/css/pico.min.css" media="screen" />
<script>
function handle_submit(form){
let value = form.height_or_digest.value;
var is_digest = value.length == 80;
var type = is_digest ? "digest" : "height";
var uri = form.action + "/" + type + "/" + value;
window.location.href = uri;
return false;
}
function handle_utxo_submit(form) {
let value = form.utxo.value;
var uri = form.action + "/" + value;
window.location.href = uri;
return false;
}
function handle_submit(form) {
let value = form.height_or_digest.value;
var is_digest = value.length == 80;
var type = is_digest ? "digest" : "height";
var uri = form.action + "/" + type + "/" + value;
window.location.href = uri;
return false;
}
function handle_utxo_submit(form) {
let value = form.utxo.value;
var uri = form.action + "/" + value;
window.location.href = uri;
return false;
}
</script>
<div class="box">
<h3>Block Lookup</h3>
</head>
<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)">
Block height or digest:
<input type="text" size="80" name="height_or_digest"/>
@ -33,58 +41,70 @@ Block height or digest:
Quick Lookup:
<a href="/block/genesis">Genesis Block</a> |
<a href="/block/tip">Tip</a><br/>
</div>
</details>
</article>
<div class="box">
<h3>Utxo Lookup</h3>
<article>
<details>
<summary>Utxo Lookup</summary>
<form action="/utxo" method="get" onsubmit="return handle_utxo_submit(this)">
Utxo index:
<input type="text" size="10" name="utxo" />
<input type="submit" name="height" value="Lookup Utxo" />
</form>
</div>
</details>
</article>
<h2>REST RPCs</h2>
<div class="box">
<h3>/block_info</h3>
<article>
<details>
<summary>/block_info</summary>
<div class="indent">
<h4>Examples</h4>
<a href="/rpc/block_info/genesis">/rpc/block_info/genesis</a><br/>
<a href="/rpc/block_info/tip">/rpc/block_info/tip</a><br/>
<a href="/rpc/block_info/height/2">/rpc/block_info/height/2</a><br/>
<a href="/rpc/block_info/digest/{{self.genesis_digest.to_hex()}}">/rpc/block_info/digest/{{self.genesis_digest.to_hex()}}</a><br/>
<a href="/rpc/block_info/height_or_digest/1">/rpc/block_info/height_or_digest/1</a><br/>
</div>
<ul>
<li><a href="/rpc/block_info/genesis">/rpc/block_info/genesis</a></li>
<li><a href="/rpc/block_info/tip">/rpc/block_info/tip</a></li>
<li><a href="/rpc/block_info/height/2">/rpc/block_info/height/2</a></li>
<li><a href="/rpc/block_info/digest/{{self.genesis_digest.to_hex()}}">/rpc/block_info/digest/{{self.genesis_digest.to_hex()}}</a></li>
<li><a href="/rpc/block_info/height_or_digest/1">/rpc/block_info/height_or_digest/1</a></li>
</ul>
</div>
</details>
</article>
<div class="box">
<h3>/block_digest</h3>
<article>
<details>
<summary>/block_digest</summary>
<div class="indent">
<h4>Examples</h4>
<a href="/rpc/block_digest/genesis">/rpc/block_digest/genesis</a><br/>
<a href="/rpc/block_digest/tip">/rpc/block_digest/tip</a><br/>
<a href="/rpc/block_digest/height/2">/rpc/block_digest/height/2</a><br/>
<a href="/rpc/block_digest/digest/{{self.genesis_digest.to_hex()}}">/rpc/block_digest/digest/{{self.genesis_digest.to_hex()}}</a><br/>
<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/>
</div>
<ul>
<li><a href="/rpc/block_digest/genesis">/rpc/block_digest/genesis</a></li>
<li><a href="/rpc/block_digest/tip">/rpc/block_digest/tip</a></li>
<li><a href="/rpc/block_digest/height/2">/rpc/block_digest/height/2</a></li>
<li><a href="/rpc/block_digest/digest/{{self.genesis_digest.to_hex()}}">/rpc/block_digest/digest/{{self.genesis_digest.to_hex()}}</a></li>
<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>
</details>
</article>
<div class="box">
<h3>/utxo_digest</h3>
<article>
<details>
<summary>/utxo_digest</summary>
<div class="indent">
<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>
</details>
</article>
</div>
</main>
</body>
</html>

View File

@ -2,14 +2,19 @@
<head>
<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" />
</head>
<body>
{{Trusted(self.header.to_string())}}
<h3>Utxo Information</h3>
<table class="alt">
<main class="container">
<article>
<summary>Utxo Information</summary>
<table class="striped">
<tr>
<td>Index</td>
<td>{{self.index}}</td>
@ -19,12 +24,16 @@
<td>{{self.digest.to_hex()}}</td>
</tr>
</table>
</article>
<article>
<p>
<a href="/">Home</a>
| <a href='/block/genesis'>Genesis</a>
| <a href='/block/tip'>Tip</a>
</p>
</article>
</main>
</body>
</html>