web interface for table_definition improved

This commit is contained in:
Priec
2026-08-04 17:20:58 +02:00
parent c2002c7da3
commit 803207b1af
20 changed files with 2989 additions and 612 deletions

View File

@@ -0,0 +1,196 @@
//! Reads everything the workspace shows.
//!
//! Three calls, in this order: the profile tree names the profiles and their
//! tables, the profile details describe the selected table's columns and
//! scripts, and the rename history explains how those columns got their names.
//! Nothing here trusts the posted selection — a profile or table that is gone
//! is dropped from the selection rather than reported as an error, because the
//! commonest way to get here with a stale one is having just deleted it.
use axum::http::HeaderMap;
use crate::{
AppState,
auth::GetAuthorizationRequest,
definitions::{
common::Empty,
table_definition::{
GetColumnAliasRenameHistoryRequest, GetProfileDetailsRequest, MoneyRounding,
},
},
services::authenticated_request,
};
use super::state::{
DetailColumn, LoadError, PageInputs, RenameEntry, ScriptView, TableDefinitionPageState,
TableDetailView, TableSummary,
};
pub(crate) async fn load_page(
state: AppState,
headers: &HeaderMap,
mut inputs: PageInputs,
) -> Result<TableDefinitionPageState, LoadError> {
let authorization_request = authenticated_request(headers, GetAuthorizationRequest {})
.map_err(|_| LoadError::Unauthenticated)?;
let mut auth = state.auth;
let authorization = auth
.get_authorization(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 profiles = tree
.profiles
.iter()
.map(|profile| profile.name.clone())
.collect::<Vec<_>>();
// A profile that no longer exists takes the table selection with it.
if !profiles.contains(&inputs.selection.profile) {
inputs.selection.profile.clear();
inputs.selection.table.clear();
}
let tables = tree
.profiles
.iter()
.find(|profile| profile.name == inputs.selection.profile)
.map(|profile| {
profile
.tables
.iter()
.map(|table| TableSummary {
name: table.name.clone(),
table_kind: table.table_kind.clone(),
depends_on: table.depends_on.clone(),
row_display_columns: table.row_display_columns.clone(),
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
if !tables
.iter()
.any(|table| table.name == inputs.selection.table)
{
inputs.selection.table.clear();
}
let detail = match inputs.selection.has_table() {
true => {
let request = GetProfileDetailsRequest {
profile_name: inputs.selection.profile.clone(),
};
let details = definitions
.get_profile_details(
authenticated_request(headers, request)
.map_err(|_| LoadError::Unauthenticated)?,
)
.await
.map_err(|error| LoadError::Backend(error.message().to_string()))?
.into_inner();
details
.tables
.into_iter()
.find(|table| table.name == inputs.selection.table)
.map(|table| TableDetailView {
id: table.id,
columns: table
.columns
.iter()
.map(|column| {
let behavior = table.column_behaviors.get(&column.name);
DetailColumn {
name: column.name.clone(),
field_type: column.field_type.clone(),
currency: column.currency.clone(),
quantity_ledger: column.quantity_ledger,
rounded: column.rounding == i32::from(MoneyRounding::HalfUp),
generated: behavior.is_some_and(|behavior| behavior.generated),
read_only: behavior.is_some_and(|behavior| behavior.read_only),
generated_from: behavior
.map(|behavior| behavior.generated_from.clone())
.unwrap_or_default(),
}
})
.collect(),
scripts: table
.scripts
.into_iter()
.map(|script| ScriptView {
target_column: script.target_column,
target_column_type: script.target_column_type,
description: script.description,
script: script.script,
})
.collect(),
row_display_columns: table.row_display_columns,
table_kind: table.table_kind,
name: table.name,
})
}
false => None,
};
// The history is per profile; a selected table narrows it to that table.
let history = match inputs.selection.has_profile() {
true => {
let request = GetColumnAliasRenameHistoryRequest {
profile_name: inputs.selection.profile.clone(),
table_definition_id: detail.as_ref().map(|detail| detail.id),
};
definitions
.get_column_alias_rename_history(
authenticated_request(headers, request)
.map_err(|_| LoadError::Unauthenticated)?,
)
.await
.map_err(|error| LoadError::Backend(error.message().to_string()))?
.into_inner()
.entries
.into_iter()
.map(|entry| RenameEntry {
table_name: entry.table_name,
old_column_name: entry.old_column_name,
new_column_name: entry.new_column_name,
created_at: entry.created_at,
})
.collect()
}
false => Vec::new(),
};
Ok(TableDefinitionPageState {
nav: crate::ui::Nav::new(headers, "admin").with_role(authorization.role),
profiles,
tables,
detail,
history,
selection: inputs.selection,
columns: inputs.columns,
rename: inputs.rename,
copy: inputs.copy,
invoice: inputs.invoice,
status: inputs.status,
error: inputs.error,
sql: inputs.sql,
generated: inputs.generated,
})
}