graphs renamed to web
This commit is contained in:
59
web/src/pages/add_logic/loader.rs
Normal file
59
web/src/pages/add_logic/loader.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use axum::http::HeaderMap;
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
auth::GetAuthorizationRequest,
|
||||
definitions::common::Empty,
|
||||
services::authenticated_request,
|
||||
};
|
||||
|
||||
use super::state::{AddLogicPageState, CreateLogicForm, TableOption};
|
||||
|
||||
pub(crate) async fn load_page(
|
||||
state: AppState,
|
||||
headers: &HeaderMap,
|
||||
form: CreateLogicForm,
|
||||
error: Option<String>,
|
||||
) -> Result<AddLogicPageState, LoadError> {
|
||||
let request = authenticated_request(headers, GetAuthorizationRequest {})
|
||||
.map_err(|_| LoadError::Unauthenticated)?;
|
||||
let mut auth = state.auth;
|
||||
let authorization = auth
|
||||
.get_authorization(request)
|
||||
.await
|
||||
.map_err(|error| match error.code() {
|
||||
tonic::Code::Unauthenticated => LoadError::Unauthenticated,
|
||||
_ => LoadError::Backend(error.message().to_string()),
|
||||
})?
|
||||
.into_inner();
|
||||
if authorization.role != "admin" {
|
||||
return Err(LoadError::Forbidden);
|
||||
}
|
||||
|
||||
let mut definitions = state.definitions;
|
||||
let tree = definitions
|
||||
.get_profile_tree(
|
||||
authenticated_request(headers, Empty {}).map_err(|_| LoadError::Unauthenticated)?,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| LoadError::Backend(error.message().to_string()))?
|
||||
.into_inner();
|
||||
let tables = tree
|
||||
.profiles
|
||||
.into_iter()
|
||||
.flat_map(|profile| {
|
||||
profile.tables.into_iter().map(move |table| TableOption {
|
||||
id: table.id,
|
||||
profile_name: profile.name.clone(),
|
||||
table_name: table.name,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(AddLogicPageState { tables, form, error })
|
||||
}
|
||||
|
||||
pub(crate) enum LoadError {
|
||||
Unauthenticated,
|
||||
Forbidden,
|
||||
Backend(String),
|
||||
}
|
||||
78
web/src/pages/add_logic/logic.rs
Normal file
78
web/src/pages/add_logic/logic.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use axum::{
|
||||
Form,
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
|
||||
use crate::{AppState, services::authenticated_request};
|
||||
|
||||
use super::{
|
||||
loader::{LoadError, load_page},
|
||||
state::CreateLogicForm,
|
||||
ui,
|
||||
};
|
||||
|
||||
pub(crate) async fn new_logic_page(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> Response {
|
||||
render_loaded(load_page(state, &headers, CreateLogicForm::default(), None).await)
|
||||
}
|
||||
|
||||
pub(crate) async fn create_logic(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Form(form): Form<CreateLogicForm>,
|
||||
) -> Response {
|
||||
let submitted = form.clone();
|
||||
if let Err(error) = load_page(state.clone(), &headers, submitted.clone(), None).await {
|
||||
return render_loaded(Err(error));
|
||||
}
|
||||
if headers
|
||||
.get("sec-fetch-site")
|
||||
.is_some_and(|value| value == "cross-site")
|
||||
{
|
||||
return (StatusCode::FORBIDDEN, "Cross-site form submission rejected").into_response();
|
||||
}
|
||||
let request = match form.into_request() {
|
||||
Ok(request) => request,
|
||||
Err(message) => {
|
||||
return render_loaded(load_page(state, &headers, submitted, Some(message)).await);
|
||||
}
|
||||
};
|
||||
let mut scripts = state.scripts;
|
||||
let request = match authenticated_request(&headers, request) {
|
||||
Ok(request) => request,
|
||||
Err(_) => return Redirect::to("/login").into_response(),
|
||||
};
|
||||
match scripts.post_table_script(request).await {
|
||||
Ok(response) => Html(ui::render_success(
|
||||
response.get_ref().id,
|
||||
&response.get_ref().warnings,
|
||||
))
|
||||
.into_response(),
|
||||
Err(error) => (
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Html(ui::render_submission_error(error.message())),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_loaded(result: Result<super::state::AddLogicPageState, LoadError>) -> Response {
|
||||
match result {
|
||||
Ok(page) => Html(ui::render_page(&page)).into_response(),
|
||||
Err(LoadError::Unauthenticated) => Redirect::to("/login").into_response(),
|
||||
Err(LoadError::Forbidden) => (
|
||||
StatusCode::FORBIDDEN,
|
||||
Html(ui::render_submission_error("Administrator access is required.")),
|
||||
)
|
||||
.into_response(),
|
||||
Err(LoadError::Backend(message)) => (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Html(ui::render_submission_error(&message)),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
17
web/src/pages/add_logic/mod.rs
Normal file
17
web/src/pages/add_logic/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
mod loader;
|
||||
mod logic;
|
||||
mod state;
|
||||
mod ui;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, post},
|
||||
};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
pub(crate) fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/admin/logic/new", get(logic::new_logic_page))
|
||||
.route("/admin/logic", post(logic::create_logic))
|
||||
}
|
||||
52
web/src/pages/add_logic/state.rs
Normal file
52
web/src/pages/add_logic/state.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use crate::definitions::table_script::PostTableScriptRequest;
|
||||
|
||||
#[derive(Clone, Debug, Default, serde::Deserialize)]
|
||||
pub(crate) struct CreateLogicForm {
|
||||
#[serde(default)]
|
||||
pub table_definition_id: i64,
|
||||
#[serde(default)]
|
||||
pub logic_name: String,
|
||||
#[serde(default)]
|
||||
pub target_column: String,
|
||||
#[serde(default)]
|
||||
pub script: String,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
pub(crate) struct AddLogicPageState {
|
||||
pub tables: Vec<TableOption>,
|
||||
pub form: CreateLogicForm,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) struct TableOption {
|
||||
pub id: i64,
|
||||
pub profile_name: String,
|
||||
pub table_name: String,
|
||||
}
|
||||
|
||||
impl CreateLogicForm {
|
||||
pub(crate) fn into_request(self) -> Result<PostTableScriptRequest, String> {
|
||||
if self.table_definition_id <= 0 {
|
||||
return Err("Select a target table.".to_string());
|
||||
}
|
||||
let target_column = self.target_column.trim().to_string();
|
||||
if target_column.is_empty() {
|
||||
return Err("Enter the target column.".to_string());
|
||||
}
|
||||
let script = self.script.trim().to_string();
|
||||
if script.is_empty() {
|
||||
return Err("Enter a Steel expression.".to_string());
|
||||
}
|
||||
if !script.starts_with('(') {
|
||||
return Err("The Steel expression must start with `(`.".to_string());
|
||||
}
|
||||
Ok(PostTableScriptRequest {
|
||||
table_definition_id: self.table_definition_id,
|
||||
target_column,
|
||||
script,
|
||||
description: self.description.trim().to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
45
web/src/pages/add_logic/ui.rs
Normal file
45
web/src/pages/add_logic/ui.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use super::state::AddLogicPageState;
|
||||
|
||||
const ADMIN_CSS: &str = include_str!("../../../static/admin.css");
|
||||
|
||||
pub(crate) fn render_page(page: &AddLogicPageState) -> String {
|
||||
let tables = page
|
||||
.tables
|
||||
.iter()
|
||||
.map(|table| {
|
||||
format!(
|
||||
"<option value=\"{}\" {}>{}.{}</option>",
|
||||
table.id,
|
||||
if page.form.table_definition_id == table.id { "selected" } else { "" },
|
||||
crate::escape_html(&table.profile_name),
|
||||
crate::escape_html(&table.table_name),
|
||||
)
|
||||
})
|
||||
.collect::<String>();
|
||||
let error = page.error.as_deref().map(render_submission_error).unwrap_or_default();
|
||||
format!(
|
||||
"<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Add logic</title><script src=\"https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js\"></script><style>{ADMIN_CSS}</style></head><body><header class=\"topbar\"><div><strong>Komp Accounting</strong></div><nav><a href=\"/admin\">Admin</a><a href=\"/\">Analytics</a></nav></header><main class=\"form-main\"><a class=\"back-link\" href=\"/admin\">← Admin panel</a><section class=\"form-card\"><p class=\"eyebrow\">Computed column</p><h1>Add logic</h1><p>Create or update a Steel script through the existing table-script service.</p><form hx-post=\"/admin/logic\" hx-target=\"#submission-status\" hx-swap=\"innerHTML\" hx-disabled-elt=\"button[type=submit]\"><div class=\"form-grid\"><label class=\"wide\">Table<select name=\"table_definition_id\" required><option value=\"0\">Choose a table</option>{tables}</select></label><label>Logic name<input name=\"logic_name\" value=\"{}\" placeholder=\"invoice total\"></label><label>Target column<input name=\"target_column\" value=\"{}\" required placeholder=\"total\"></label><label class=\"wide\">Steel expression<textarea name=\"script\" rows=\"12\" required placeholder=\"(+ (get-var "subtotal") (get-var "tax"))\">{}</textarea></label><label class=\"wide\">Description<input name=\"description\" value=\"{}\"></label></div><div id=\"submission-status\" aria-live=\"polite\">{error}</div><div class=\"form-actions\"><a href=\"/admin\">Cancel</a><button type=\"submit\">Save logic</button></div></form></section></main></body></html>",
|
||||
crate::escape_html(&page.form.logic_name),
|
||||
crate::escape_html(&page.form.target_column),
|
||||
crate::escape_html(&page.form.script),
|
||||
crate::escape_html(&page.form.description),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn render_submission_error(message: &str) -> String {
|
||||
format!(
|
||||
"<div class=\"form-error\"><strong>Could not save the logic</strong><p>{}</p></div>",
|
||||
crate::escape_html(message),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn render_success(id: i64, warnings: &str) -> String {
|
||||
let warnings = if warnings.trim().is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("<p><strong>Warnings:</strong> {}</p>", crate::escape_html(warnings))
|
||||
};
|
||||
format!(
|
||||
"<div class=\"form-success\"><strong>Logic saved</strong><p>Script #{id} was saved.</p>{warnings}</div>"
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user