web displays system or user defined columns
This commit is contained in:
2
client
2
client
Submodule client updated: 819a4b6300...e17321c951
2
server
2
server
Submodule server updated: 32b38e5a5b...8b7a88ec3e
@@ -12,6 +12,12 @@ mod schema;
|
|||||||
mod services;
|
mod services;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod authz;
|
mod authz;
|
||||||
|
// The server's system column vocabulary, read out of `common` the same way the
|
||||||
|
// generated protos are: this crate compiles that source tree directly instead
|
||||||
|
// of depending on the crate. Only `is_system_column` is used here.
|
||||||
|
#[path = "../../common/src/system_column.rs"]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
mod system_column;
|
||||||
mod analytics {
|
mod analytics {
|
||||||
include!(concat!(
|
include!(concat!(
|
||||||
env!("CARGO_MANIFEST_DIR"),
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ pub(crate) async fn load_admin_page(
|
|||||||
nullable: column.is_nullable,
|
nullable: column.is_nullable,
|
||||||
primary_key: column.is_primary_key,
|
primary_key: column.is_primary_key,
|
||||||
quantity_ledger: column.quantity_ledger,
|
quantity_ledger: column.quantity_ledger,
|
||||||
|
system: crate::system_column::is_system_column(&column.name),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,20 @@ pub(crate) struct AdminPageState {
|
|||||||
pub can_export: bool,
|
pub can_export: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AdminPageState {
|
||||||
|
/// The columns the user defined, in the order the backend reported them.
|
||||||
|
/// The columns pane renders these first.
|
||||||
|
pub(crate) fn user_columns(&self) -> Vec<&ColumnView> {
|
||||||
|
self.columns.iter().filter(|column| !column.system).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The columns the server puts on every managed table. They are hidden
|
||||||
|
/// everywhere else, so they trail the user's own here.
|
||||||
|
pub(crate) fn system_columns(&self) -> Vec<&ColumnView> {
|
||||||
|
self.columns.iter().filter(|column| column.system).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct ProfileView {
|
pub(crate) struct ProfileView {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -40,12 +54,18 @@ pub(crate) struct ColumnView {
|
|||||||
pub nullable: bool,
|
pub nullable: bool,
|
||||||
pub primary_key: bool,
|
pub primary_key: bool,
|
||||||
pub quantity_ledger: bool,
|
pub quantity_ledger: bool,
|
||||||
|
/// The server named this column, not the user. Ordinary screens never show
|
||||||
|
/// it, so the admin panel is the only place it is visible.
|
||||||
|
pub system: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ColumnView {
|
impl ColumnView {
|
||||||
/// The badge list rendered under each column name.
|
/// The badge list rendered under each column name.
|
||||||
pub(crate) fn flags(&self) -> Vec<&'static str> {
|
pub(crate) fn flags(&self) -> Vec<&'static str> {
|
||||||
let mut flags = Vec::new();
|
let mut flags = Vec::new();
|
||||||
|
if self.system {
|
||||||
|
flags.push("system");
|
||||||
|
}
|
||||||
if self.primary_key {
|
if self.primary_key {
|
||||||
flags.push("primary key");
|
flags.push("primary key");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,4 +80,41 @@ mod tests {
|
|||||||
assert!(html.contains(route), "missing admin action route {route}");
|
assert!(html.contains(route), "missing admin action route {route}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn system_columns_render_after_the_user_defined_ones() {
|
||||||
|
let column = |name: &str, system: bool| crate::pages::admin::admin::state::ColumnView {
|
||||||
|
name: name.to_string(),
|
||||||
|
data_type: "TEXT".to_string(),
|
||||||
|
nullable: true,
|
||||||
|
primary_key: false,
|
||||||
|
quantity_ledger: false,
|
||||||
|
system,
|
||||||
|
};
|
||||||
|
let page = AdminPageState {
|
||||||
|
nav: Nav::default(),
|
||||||
|
profiles: Vec::new(),
|
||||||
|
selected_profile: Some("books".to_string()),
|
||||||
|
tables: Vec::new(),
|
||||||
|
selected_table: Some("invoice".to_string()),
|
||||||
|
// The backend reports the physical order, system columns first.
|
||||||
|
columns: vec![
|
||||||
|
column("id", true),
|
||||||
|
column("title", false),
|
||||||
|
column("created_at", true),
|
||||||
|
],
|
||||||
|
can_manage_tables: true,
|
||||||
|
can_manage_scripts: true,
|
||||||
|
can_manage_validations: true,
|
||||||
|
can_export: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let html = render_workspace(&page);
|
||||||
|
let title = html.find(">title<").expect("the user column is rendered");
|
||||||
|
let label = html
|
||||||
|
.find("System columns")
|
||||||
|
.expect("the system group is labelled");
|
||||||
|
let id = html.find(">id<").expect("the system column is rendered");
|
||||||
|
assert!(title < label && label < id, "system columns must trail");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,9 @@
|
|||||||
.column { display: grid; grid-template-columns: minmax(100px, 1fr) auto; gap: 3px 12px; padding: 10px 11px; border-bottom: 1px solid #edf0f3; }
|
.column { display: grid; grid-template-columns: minmax(100px, 1fr) auto; gap: 3px 12px; padding: 10px 11px; border-bottom: 1px solid #edf0f3; }
|
||||||
.column code { color: #315078; font-size: 12px; }
|
.column code { color: #315078; font-size: 12px; }
|
||||||
.column small { grid-column: 1 / -1; }
|
.column small { grid-column: 1 / -1; }
|
||||||
|
.column.system { color: #6b7686; background: #fafbfc; }
|
||||||
|
.column.system code { color: #67788e; }
|
||||||
|
.group-label { margin: 14px 0 4px; padding: 0 11px; color: #7c8796; font-size: 11px; letter-spacing: .04em; text-transform: uppercase; }
|
||||||
.empty { margin: 8px; color: #7c8796; }
|
.empty { margin: 8px; color: #7c8796; }
|
||||||
|
|
||||||
/* ---------- Forms (components/form_card.html) ---------- */
|
/* ---------- Forms (components/form_card.html) ---------- */
|
||||||
|
|||||||
@@ -49,20 +49,34 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="pane">
|
<section class="pane">
|
||||||
<div class="pane-title"><h2>Columns</h2><span>{{ page.columns.len() }}</span></div>
|
<div class="pane-title">
|
||||||
|
<h2>Columns</h2>
|
||||||
|
<span>{{ page.user_columns().len() }} user · {{ page.system_columns().len() }} system</span>
|
||||||
|
</div>
|
||||||
<div class="pane-list">
|
<div class="pane-list">
|
||||||
{% if page.selected_table.is_none() %}
|
{% if page.selected_table.is_none() %}
|
||||||
<p class="empty">Select a table to inspect its columns.</p>
|
<p class="empty">Select a table to inspect its columns.</p>
|
||||||
{% else if page.columns.is_empty() %}
|
{% else if page.columns.is_empty() %}
|
||||||
<p class="empty">This table has no visible columns.</p>
|
<p class="empty">This table has no visible columns.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for column in page.columns %}
|
{% for column in page.user_columns() %}
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<span>{{ column.name }}</span>
|
<span>{{ column.name }}</span>
|
||||||
<code>{{ column.data_type }}</code>
|
<code>{{ column.data_type }}</code>
|
||||||
<small>{{ column.flags()|join(" · ") }}</small>
|
<small>{{ column.flags()|join(" · ") }}</small>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% let system_columns = page.system_columns() %}
|
||||||
|
{% if !system_columns.is_empty() %}
|
||||||
|
<p class="group-label">System columns · hidden from users</p>
|
||||||
|
{% for column in system_columns %}
|
||||||
|
<div class="column system">
|
||||||
|
<span>{{ column.name }}</span>
|
||||||
|
<code>{{ column.data_type }}</code>
|
||||||
|
<small>{{ column.flags()|join(" · ") }}</small>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user