Added trivial web ui.
This commit is contained in:
1 parent
f99513a8c8
commit
894f969d0f
11 files changed
+1382
-19
No files matched your search
Generated
+1116
-6
File diff suppressed because it is too large.
Load diff
@@ -21,6 +21,12 @@ regex = "1.7.0"
|
|||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
tempfile = "3.3.0"
|
tempfile = "3.3.0"
|
||||||
fraction = "0.13.0"
|
fraction = "0.13.0"
|
||||||
|
actix-web = "4.3.0"
|
||||||
|
tokio = { version = "1.24.2", features = ["full"] }
|
||||||
|
minijinja = { version = "0.30.2", features = ["source"] }
|
||||||
|
actix-utils = "3.0.1"
|
||||||
|
actix-web-lab = "0.18.9"
|
||||||
|
minijinja-autoreload = "0.30.2"
|
||||||
|
|
||||||
[dependencies.apache-avro]
|
[dependencies.apache-avro]
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
|
|||||||
@@ -22,3 +22,15 @@ nix build .#development-environment
|
|||||||
nix develop
|
nix develop
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Workwork
|
||||||
|
|
||||||
|
* [ ] Parametrize `CURRENT_TIMESTAMP`
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
#### API
|
||||||
|
|
||||||
|
|
||||||
|
#### Front end
|
||||||
|
|
||||||
|
* [ ] return hello world.
|
||||||
+6
-6
@@ -190,16 +190,16 @@ pub fn is_owner(tx: &Connection, account_id: &AccountId, user_id: &UserId) -> Cu
|
|||||||
Ok(option.is_some())
|
Ok(option.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Receipt {
|
pub struct AccountChangeReceipt {
|
||||||
pub account_id: AccountId,
|
pub account_id: AccountId,
|
||||||
pub version: i64,
|
pub version: i64,
|
||||||
pub timestamp: i64,
|
pub timestamp: i64,
|
||||||
pub amount: i64,
|
pub amount: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Receipt {
|
impl AccountChangeReceipt {
|
||||||
pub fn from_row(row: &Row) -> Result<Receipt> {
|
pub fn from_row(row: &Row) -> Result<AccountChangeReceipt> {
|
||||||
Ok(Receipt {
|
Ok(AccountChangeReceipt {
|
||||||
account_id: AccountId::from_id(row.get("id")?),
|
account_id: AccountId::from_id(row.get("id")?),
|
||||||
version: row.get("version")?,
|
version: row.get("version")?,
|
||||||
amount: row.get("amount")?,
|
amount: row.get("amount")?,
|
||||||
@@ -224,7 +224,7 @@ fn update_account_history(tx: &Connection, account_id: &AccountId) -> CustomResu
|
|||||||
|
|
||||||
pub fn add_to_account(tx: &Connection,
|
pub fn add_to_account(tx: &Connection,
|
||||||
account_id: &AccountId,
|
account_id: &AccountId,
|
||||||
amount: i64) -> CustomResult<Receipt> {
|
amount: i64) -> CustomResult<AccountChangeReceipt> {
|
||||||
let q = "
|
let q = "
|
||||||
UPDATE accounts SET amount = CASE account_type
|
UPDATE accounts SET amount = CASE account_type
|
||||||
WHEN 'security_emitter'
|
WHEN 'security_emitter'
|
||||||
@@ -236,7 +236,7 @@ pub fn add_to_account(tx: &Connection,
|
|||||||
RETURNING id, version, amount, UNIXEPOCH(CURRENT_TIMESTAMP) as timestamp
|
RETURNING id, version, amount, UNIXEPOCH(CURRENT_TIMESTAMP) as timestamp
|
||||||
";
|
";
|
||||||
let params = params![amount, account_id];
|
let params = params![amount, account_id];
|
||||||
let ret = tx.query_row(q, params, Receipt::from_row).map_err(Error::SqlError);
|
let ret = tx.query_row(q, params, AccountChangeReceipt::from_row).map_err(Error::SqlError);
|
||||||
update_account_history(tx, account_id)?;
|
update_account_history(tx, account_id)?;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ pub mod users {
|
|||||||
|
|
||||||
pub mod accounts {
|
pub mod accounts {
|
||||||
pub use super::_accounts::{Account,
|
pub use super::_accounts::{Account,
|
||||||
Receipt,
|
AccountChangeReceipt,
|
||||||
create_checking_account,
|
create_checking_account,
|
||||||
create_security_emitter_account,
|
create_security_emitter_account,
|
||||||
is_owner,
|
is_owner,
|
||||||
|
|||||||
+80
-3
@@ -1,7 +1,7 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::{Arg, ArgAction, ArgMatches, Command, value_parser};
|
||||||
use clap::parser::ValuesRef;
|
use clap::parser::ValuesRef;
|
||||||
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -10,14 +10,16 @@ use simple_logger::SimpleLogger;
|
|||||||
use db::securities::list_securities;
|
use db::securities::list_securities;
|
||||||
|
|
||||||
use crate::db::users::{create_user, list_users};
|
use crate::db::users::{create_user, list_users};
|
||||||
use crate::event_processor::{process_event_files};
|
use crate::event_processor::process_event_files;
|
||||||
use crate::migration::apply_migrations;
|
use crate::migration::apply_migrations;
|
||||||
|
use crate::webui::web_ui_main;
|
||||||
|
|
||||||
mod db;
|
mod db;
|
||||||
mod event_processor;
|
mod event_processor;
|
||||||
mod migration;
|
mod migration;
|
||||||
mod services;
|
mod services;
|
||||||
mod util;
|
mod util;
|
||||||
|
mod webui;
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct Output<T> {
|
struct Output<T> {
|
||||||
@@ -42,6 +44,8 @@ trait CommandLineUserLogic {
|
|||||||
fn list_securities(&mut self, conn: &Connection);
|
fn list_securities(&mut self, conn: &Connection);
|
||||||
|
|
||||||
fn process_events(&mut self, conn: &mut Connection, files: ValuesRef<PathBuf>);
|
fn process_events(&mut self, conn: &mut Connection, files: ValuesRef<PathBuf>);
|
||||||
|
|
||||||
|
fn run_webui(&mut self, conn: &mut Connection, port: u16, enable_template_autoreload: bool);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verbosity_arg() -> Arg {
|
fn verbosity_arg() -> Arg {
|
||||||
@@ -130,12 +134,36 @@ fn ls_securities_command_line_parser() -> Command {
|
|||||||
.about("List all securities.");
|
.about("List all securities.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_web_ui_command_line_parser() -> Command {
|
||||||
|
return Command::new("run")
|
||||||
|
.arg(Arg::new("port")
|
||||||
|
.long("port")
|
||||||
|
.value_parser(clap::value_parser!(u16).range(3000..))
|
||||||
|
.action(ArgAction::Set)
|
||||||
|
.default_value("3200"))
|
||||||
|
.arg(Arg::new("enable-template-autoreload")
|
||||||
|
.long("enable-template-autoreload")
|
||||||
|
.required(false)
|
||||||
|
.num_args(0)
|
||||||
|
.help("Reload underlying template (for development)")
|
||||||
|
)
|
||||||
|
.about("Run web user interface.");
|
||||||
|
}
|
||||||
|
|
||||||
fn securities_command_line_parser() -> Command {
|
fn securities_command_line_parser() -> Command {
|
||||||
return Command::new("securities")
|
return Command::new("securities")
|
||||||
|
.about("Handy command about securities.")
|
||||||
.subcommand(ls_securities_command_line_parser())
|
.subcommand(ls_securities_command_line_parser())
|
||||||
.arg_required_else_help(true);
|
.arg_required_else_help(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn web_ui_command_line_parser() -> Command {
|
||||||
|
return Command::new("wui")
|
||||||
|
.about("Web user interface.")
|
||||||
|
.subcommand(run_web_ui_command_line_parser())
|
||||||
|
.arg_required_else_help(true);
|
||||||
|
}
|
||||||
|
|
||||||
fn command_line_parser() -> Command {
|
fn command_line_parser() -> Command {
|
||||||
return Command::new("prog")
|
return Command::new("prog")
|
||||||
.arg(verbosity_arg())
|
.arg(verbosity_arg())
|
||||||
@@ -143,6 +171,7 @@ fn command_line_parser() -> Command {
|
|||||||
.subcommand(event_command_line_parser())
|
.subcommand(event_command_line_parser())
|
||||||
.subcommand(user_command_line_parser())
|
.subcommand(user_command_line_parser())
|
||||||
.subcommand(securities_command_line_parser())
|
.subcommand(securities_command_line_parser())
|
||||||
|
.subcommand(web_ui_command_line_parser())
|
||||||
.arg_required_else_help(true);
|
.arg_required_else_help(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +236,17 @@ fn process_process_events_command_line(
|
|||||||
logic.process_events(conn, files);
|
logic.process_events(conn, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn process_run_wui_command_line(
|
||||||
|
conn: &mut Connection,
|
||||||
|
m: &ArgMatches,
|
||||||
|
logic: &mut dyn CommandLineUserLogic,
|
||||||
|
) {
|
||||||
|
debug!("Processing run wui command line {:?}.", m);
|
||||||
|
let mut port = m.get_one::<u16>("port").unwrap_or(&3200);
|
||||||
|
let enable_template_autoreload = m.get_flag("enable-template-autoreload");
|
||||||
|
logic.run_webui(conn, *port, enable_template_autoreload);
|
||||||
|
}
|
||||||
|
|
||||||
fn process_securities_command_line(
|
fn process_securities_command_line(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
m: &ArgMatches,
|
m: &ArgMatches,
|
||||||
@@ -235,6 +275,20 @@ fn process_events_command_line(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn process_wui_command_line(
|
||||||
|
conn: &mut Connection,
|
||||||
|
m: &ArgMatches,
|
||||||
|
logic: &mut dyn CommandLineUserLogic,
|
||||||
|
) {
|
||||||
|
debug!("Processing wui command line {:?}.", m);
|
||||||
|
match m.subcommand() {
|
||||||
|
Some(("run", run_wui_match)) => {
|
||||||
|
process_run_wui_command_line(conn, run_wui_match, logic);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn process_command_line(
|
fn process_command_line(
|
||||||
conn: &mut Connection,
|
conn: &mut Connection,
|
||||||
m: &ArgMatches,
|
m: &ArgMatches,
|
||||||
@@ -245,13 +299,15 @@ fn process_command_line(
|
|||||||
Some(("users", user_match)) => {
|
Some(("users", user_match)) => {
|
||||||
process_user_command_line(conn, user_match, logic);
|
process_user_command_line(conn, user_match, logic);
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(("securities", securities_match)) => {
|
Some(("securities", securities_match)) => {
|
||||||
process_securities_command_line(conn, securities_match, logic);
|
process_securities_command_line(conn, securities_match, logic);
|
||||||
}
|
}
|
||||||
Some(("events", events_match)) => {
|
Some(("events", events_match)) => {
|
||||||
process_events_command_line(conn, events_match, logic);
|
process_events_command_line(conn, events_match, logic);
|
||||||
}
|
}
|
||||||
|
Some(("wui", wui_match)) => {
|
||||||
|
process_wui_command_line(conn, wui_match, logic);
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
println!("Received un parsable command. {:?}", m.subcommand());
|
println!("Received un parsable command. {:?}", m.subcommand());
|
||||||
unreachable!();
|
unreachable!();
|
||||||
@@ -268,6 +324,7 @@ mod cli_test {
|
|||||||
create_user_count: usize,
|
create_user_count: usize,
|
||||||
list_securities_count: usize,
|
list_securities_count: usize,
|
||||||
process_events_count: usize,
|
process_events_count: usize,
|
||||||
|
run_webui_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestCommandLineLogic {
|
impl TestCommandLineLogic {
|
||||||
@@ -277,6 +334,7 @@ mod cli_test {
|
|||||||
create_user_count: 0,
|
create_user_count: 0,
|
||||||
list_securities_count: 0,
|
list_securities_count: 0,
|
||||||
process_events_count: 0,
|
process_events_count: 0,
|
||||||
|
run_webui_count: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,6 +355,10 @@ mod cli_test {
|
|||||||
fn process_events(&mut self, conn: &mut Connection, _files: ValuesRef<PathBuf>) {
|
fn process_events(&mut self, conn: &mut Connection, _files: ValuesRef<PathBuf>) {
|
||||||
self.process_events_count += 1;
|
self.process_events_count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_webui(&mut self, conn: &mut Connection, port: u16, enable_template_autoreload: bool) {
|
||||||
|
self.run_webui_count += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -378,6 +440,16 @@ mod cli_test {
|
|||||||
process_command_line(&mut conn, &m, &mut logic);
|
process_command_line(&mut conn, &m, &mut logic);
|
||||||
assert_eq!(logic.process_events_count, 1);
|
assert_eq!(logic.process_events_count, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn run_web_ui() {
|
||||||
|
// let cmd = command_line_parser();
|
||||||
|
// let m = cmd.get_matches_from(vec!["test", "webui", "run"]);
|
||||||
|
// let mut logic = TestCommandLineLogic::new();
|
||||||
|
// let mut conn = Connection::open_in_memory().unwrap();
|
||||||
|
// process_command_line(&mut conn, &m, &mut logic);
|
||||||
|
// assert_eq!(logic.run_webui_count, 1);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ProCommandLineLogic {}
|
struct ProCommandLineLogic {}
|
||||||
@@ -426,6 +498,11 @@ impl CommandLineUserLogic for ProCommandLineLogic {
|
|||||||
|
|
||||||
println!("fuck me");
|
println!("fuck me");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_webui(&mut self, conn: &mut Connection, port: u16, enable_template_autoreload: bool) {
|
||||||
|
info!("Serving web-ui on port {:?}", port);
|
||||||
|
web_ui_main(port, enable_template_autoreload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_connection(m: &ArgMatches) -> Connection {
|
fn get_connection(m: &ArgMatches) -> Connection {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::util::*;
|
|||||||
|
|
||||||
fn _move_securities(tx: &Savepoint,
|
fn _move_securities(tx: &Savepoint,
|
||||||
cheque: &Cheque,
|
cheque: &Cheque,
|
||||||
account_id: &AccountId) -> CustomResult<(accounts::Receipt, accounts::Receipt)> {
|
account_id: &AccountId) -> CustomResult<(accounts::AccountChangeReceipt, accounts::AccountChangeReceipt)> {
|
||||||
let drawer_receipt = accounts::add_to_account(tx, &cheque.drawer_account_id, -cheque.amount)?;
|
let drawer_receipt = accounts::add_to_account(tx, &cheque.drawer_account_id, -cheque.amount)?;
|
||||||
let payee_receipt = accounts::add_to_account(tx, account_id, cheque.amount)?;
|
let payee_receipt = accounts::add_to_account(tx, account_id, cheque.amount)?;
|
||||||
if drawer_receipt.amount < 0 {
|
if drawer_receipt.amount < 0 {
|
||||||
@@ -20,8 +20,8 @@ fn _move_securities(tx: &Savepoint,
|
|||||||
|
|
||||||
fn _add_cheque_to_cashed_cheques_log(tx: &Connection,
|
fn _add_cheque_to_cashed_cheques_log(tx: &Connection,
|
||||||
cheque: &Cheque,
|
cheque: &Cheque,
|
||||||
drawer_receipt: &accounts::Receipt,
|
drawer_receipt: &accounts::AccountChangeReceipt,
|
||||||
payee_receipt: &accounts::Receipt) -> Result<(), Error> {
|
payee_receipt: &accounts::AccountChangeReceipt) -> Result<(), Error> {
|
||||||
let q = "
|
let q = "
|
||||||
INSERT INTO cashed_cheques(
|
INSERT INTO cashed_cheques(
|
||||||
drawer_user_id,
|
drawer_user_id,
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
mod views;
|
||||||
|
|
||||||
|
use actix_utils::future::{ready, Ready};
|
||||||
|
use minijinja;
|
||||||
|
use minijinja_autoreload::AutoReloader;
|
||||||
|
use std::{collections::HashMap, env, fs, io, path::PathBuf};
|
||||||
|
use std::io::{Error, ErrorKind};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use actix_web::{
|
||||||
|
dev::{ServiceResponse},
|
||||||
|
error,
|
||||||
|
http::{header::ContentType, StatusCode},
|
||||||
|
middleware::{ErrorHandlerResponse, ErrorHandlers, Logger},
|
||||||
|
web, App, FromRequest, HttpRequest, HttpResponse, HttpServer, Responder, Result,
|
||||||
|
};
|
||||||
|
use actix_web_lab::respond::Html;
|
||||||
|
|
||||||
|
struct MiniJinjaRenderer {
|
||||||
|
tmpl_env: web::Data<minijinja_autoreload::AutoReloader>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MiniJinjaRenderer {
|
||||||
|
fn render(
|
||||||
|
&self,
|
||||||
|
tmpl: &str,
|
||||||
|
ctx: impl Into<minijinja::value::Value>,
|
||||||
|
) -> actix_web::Result<Html> {
|
||||||
|
log::info!("Rendering template {:?}", tmpl);
|
||||||
|
self.tmpl_env
|
||||||
|
.acquire_env()
|
||||||
|
.map_err(|_| error::ErrorInternalServerError("could not acquire template env"))?
|
||||||
|
.get_template(tmpl)
|
||||||
|
.map_err(|e| error::ErrorInternalServerError(format!("{:?}", e)))?
|
||||||
|
.render(ctx.into())
|
||||||
|
.map(Html)
|
||||||
|
.map_err(|err| {
|
||||||
|
log::error!("{err}");
|
||||||
|
error::ErrorInternalServerError("template error")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRequest for MiniJinjaRenderer {
|
||||||
|
type Error = actix_web::Error;
|
||||||
|
type Future = actix_utils::future::Ready<Result<Self, Self::Error>>;
|
||||||
|
|
||||||
|
fn from_request(req: &HttpRequest, _pl: &mut actix_web::dev::Payload) -> Self::Future {
|
||||||
|
let tmpl_env = <web::Data<minijinja_autoreload::AutoReloader>>::extract(req)
|
||||||
|
.into_inner()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
actix_utils::future::ready(Ok(Self { tmpl_env }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn web_ui_main(port: u16, enable_template_autoreload: bool) -> std::io::Result<()> {
|
||||||
|
// The closure is invoked every time the environment is outdated to recreate it.
|
||||||
|
let tmpl_reloader = AutoReloader::new(move |notifier| {
|
||||||
|
let mut env: minijinja::Environment<'static> = minijinja::Environment::new();
|
||||||
|
|
||||||
|
let tmpl_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("templates");
|
||||||
|
log::info!("Using template dir {:?}", tmpl_path);
|
||||||
|
|
||||||
|
// if watch_path is never called, no fs watcher is created
|
||||||
|
if enable_template_autoreload {
|
||||||
|
notifier.watch_path(&tmpl_path, true);
|
||||||
|
}
|
||||||
|
env.set_source(minijinja::Source::from_path(tmpl_path));
|
||||||
|
|
||||||
|
Ok(env)
|
||||||
|
});
|
||||||
|
|
||||||
|
let tmpl_reloader = web::Data::new(tmpl_reloader);
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.app_data(tmpl_reloader.clone())
|
||||||
|
.service(views::index)
|
||||||
|
.wrap(ErrorHandlers::new().handler(StatusCode::NOT_FOUND, not_found))
|
||||||
|
.wrap(Logger::default())
|
||||||
|
})
|
||||||
|
.workers(2)
|
||||||
|
.bind(("0.0.0.0", port))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error handler for a 404 Page not found error.
|
||||||
|
fn not_found<B>(svc_res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||||
|
let res = get_error_response(&svc_res, "Page not found");
|
||||||
|
|
||||||
|
Ok(ErrorHandlerResponse::Response(ServiceResponse::new(
|
||||||
|
svc_res.into_parts().0,
|
||||||
|
res.map_into_right_body(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic error handler.
|
||||||
|
fn get_error_response<B>(res: &ServiceResponse<B>, error: &str) -> HttpResponse {
|
||||||
|
let req = res.request();
|
||||||
|
|
||||||
|
let tmpl_env = MiniJinjaRenderer::extract(req).into_inner().unwrap();
|
||||||
|
|
||||||
|
// Provide a fallback to a simple plain text response in case an error occurs during the
|
||||||
|
// rendering of the error page.
|
||||||
|
let fallback = |err: &str| {
|
||||||
|
HttpResponse::build(res.status())
|
||||||
|
.content_type(ContentType::plaintext())
|
||||||
|
.body(err.to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
let ctx = minijinja::context! {
|
||||||
|
error => error,
|
||||||
|
status_code => res.status().as_str(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match tmpl_env.render("error.html", ctx) {
|
||||||
|
Ok(body) => body
|
||||||
|
.customize()
|
||||||
|
.with_status(res.status())
|
||||||
|
.respond_to(req)
|
||||||
|
.map_into_boxed_body(),
|
||||||
|
|
||||||
|
Err(_) => fallback(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use actix_web::{Responder, web};
|
||||||
|
use crate::webui::MiniJinjaRenderer;
|
||||||
|
|
||||||
|
#[actix_web::get("/")]
|
||||||
|
pub async fn index(
|
||||||
|
tmpl_env: MiniJinjaRenderer,
|
||||||
|
query: web::Query<HashMap<String, String>>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
tmpl_env.render("index.html", ())
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ledger</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.8.5"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>{% block body %}{% endblock %}</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>Hello World!</h1>
|
||||||
|
{% endblock %}
|
||||||
Reference in new issue
Block a user