v0.7.7: PG roles/grants, table maintenance, Create/Edit Table + atomic column-reorder rebuild, FK management, table options (#13)

* feat(models): Change::RebuildTable + role/privilege/readiness/maintenance structs (Task 1.1)

* feat(introspection): role/privilege/tablespace/rebuild-readiness query builders (Task 1.2)

* feat(lib): rebuild_table payload, table/role object kinds, v0.7.7 capabilities + types (Task 1.3)

* feat(object_crud): table_ddl builder (create/edit-diff/options/drop) + build_ddl table arm (Task 2.1)

* feat(object_crud): role_ddl builder (create/edit/drop/grant/revoke) + build_ddl role arm (Task 2.2)

* feat(object_crud): rebuild_script assembler + fk/grant/sequence introspection queries (Task 2.3)

* feat(commands): transactional RebuildTable arm + build_rebuild_script + get_table_columns commands (Task 2.4)

* feat(commands): get_roles/privileges/readiness/tablespaces + run_maintenance (Task 2.5)

* feat(commands): role/privilege/rebuild/maintenance typed wrappers (Task 3.1)

* feat(store): roles slice + openFormTab table/role dedup (Task 3.2)

* feat(ui): TableForm (create/edit-diff/rebuild) + cross-schema FK with ON DELETE/UPDATE (Task 4.1)

* feat(ui): RoleForm + RoleDetail + RoleGrantsEditor (Task 4.2)

* feat(ui): wire roles into object registry + explorer (Task 4.3)

* feat(ui): Edit Table + maintenance menu + Create Table toolbar + rebuild badge (Task 4.4)

* chore: bump version to 0.7.7 (Task 5.1)

* docs: sync README/ROADMAP/AGENTS to v0.7.7 (Task 5.2)

* test: v0.7.7 manual smoke checklist (Task 5.3)

* chore: sync Cargo.lock to v0.7.7

* style(ui): match TableForm visual layout to shared object-form rows (remove stray padding)

* feat(ui): table creator grid redesign + per-column FK slide-in panel with relationship controls

* style(ui): table-creator columns grid — bordered cells like the data table view

* feat(ui): table creator — type dropdown, PK/Nullable rules, drag-to-reorder columns (vertical), tidy add-column row

* feat(ui): table creator — single-PK exclusivity, name on top, fixed column widths + contained h-scroll, no-wrap constraints

* style(ui): table creator — constraints column sized to content (no clip), overscroll containment

* style(ui): table creator — overscrollBehavior none on both scroll containers, add-column row py-2

* feat(ui): table creator — centered add-column button, schema dropdown auto-selected to current schema

* style(ui): table creator — columns grid fills container width, scrolls only when too narrow

* feat(ui): table creator — shorthand PG types in dropdown, Auto-Increment only for int-family

* feat(ui): FK panel — animate in/out from the right, flush create-table-style rows

* fix(ui): FK panel — smooth slide (willChange + eased), local column options from the form grid

* feat(ui): FK composer (multi-column, type preview, gated sections) + composite PK + constraints cog in actions col + auto-named FKs

* fix(ui): FK panel blocks unnamed table with hint; Foreign keys section lists staged + DB FKs

* style(ui): FK panel — transparent full-width selects, title↔select borders, 2-col column picker

* style(ui): FK panel — vertical divider between local/ref columns

* style(ui): FK panel — section title padding py-2

* style(ui): FK panel — remove row margins + select/text padding in column picker

* style(ui): FK panel — flush column picker (no section padding), border-r + py-1 cells

* style(ui): FK panel — cell padding + row borders on column picker (user polish)

* feat: FK inline into CREATE TABLE (single staged change in create mode); touch up class

* style(ui): hide FK icon on PK columns; fix grid header/row column alignment

* feat(ui): SQL preview in table creator uses read-only Monaco editor

* feat(ui): Foreign keys section — relation rows with icon + Edit/Remove actions

* style(ui): FK relation row — label, icon, table on one line

* style(ui): FormRow selects transparent like FK panel; test matches FK relation label

* style(ui): form-row focus outline amber-400/20 (subtle)

* style(ui): SQL view — remove px-4 py-3 wrapper padding

* feat(ui): Create Table button label; auto tree refresh after schema-modifying commits; table name on staged ddl card

* fix(roles): cast rolconnlimit::int8 to fix panic deserializing int4 into i64

* fix(privileges): cast privilege_type::text to avoid domain-array panic; aclexplode for sequences/schemas (role_sequence_grants removed in PG15, schema_privileges never existed)

* feat(ui): role privileges sections collapsible (collapsed by default) with 5-entry preview + fade + Show more

* feat(ui): role privileges — collapsed state shows first 5 with fade; expand reveals all

* chore(test): drop unused fireEvent import in RoleDetail test

* style(ui): hide chevron when section has 5 or fewer entries

* docs: README/ROADMAP/AGENTS — v0.7.7 shipped details (roles/grants, table editor, FK composer, privilege explorer); ROADMAP adds v0.7.8 Next up (quick wins)

* docs(readme): collapse Recent changes and Key features into details blocks

* docs(readme): move hero caption under image; align download + comparison table columns

* docs(readme): collapse only body content — keep Recent Changes / Key Features headings visible

* docs(readme): per-subsection collapsibles with meaningful summary labels
This commit is contained in:
2026-08-07 05:51:23 +08:00
committed by GitHub
parent 0c74d77e75
commit 7401e91a12
64 changed files with 6255 additions and 200 deletions
+112 -1
View File
@@ -146,6 +146,55 @@ pub struct DependencyInfo {
pub name: String, // resolved dependent object name
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoleMembership {
pub role: String,
pub member: String,
pub grantor: String,
pub admin_option: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoleInfo {
pub name: String,
pub superuser: bool,
pub inherit: bool,
pub create_db: bool,
pub create_role: bool,
pub can_login: bool,
pub replication: bool,
pub bypass_rls: bool,
pub connection_limit: i64,
pub valid_until: Option<String>,
pub memberships: Vec<RoleMembership>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrivilegeEntry {
pub object_class: String, // "table" | "sequence" | "routine" | "schema" | "database"
pub schema: Option<String>,
pub name: String,
pub privileges: Vec<String>,
pub grantable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RebuildReadiness {
pub ok: bool,
pub reasons: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MaintenanceResult {
pub duration_ms: i64,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TablespaceInfo {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Change {
@@ -197,6 +246,10 @@ pub enum Change {
id: String,
sql: String,
},
RebuildTable {
id: String,
sql: String,
},
}
impl Change {
@@ -209,7 +262,8 @@ impl Change {
| Change::BulkInsert { id, .. }
| Change::DropTable { id, .. }
| Change::EmptyTable { id, .. }
| Change::Ddl { id, .. } => id,
| Change::Ddl { id, .. }
| Change::RebuildTable { id, .. } => id,
}
}
}
@@ -619,4 +673,61 @@ mod tests {
assert!(json.contains("\"deptype\":\"n\""));
assert!(json.contains("v_users"));
}
#[test]
fn change_rebuild_table_serializes_with_snake_case_tag() {
let change = Change::RebuildTable {
id: "chg-rb1".to_string(),
sql: "CREATE TABLE _t ...".to_string(),
};
let json = serde_json::to_string(&change).unwrap();
assert!(
json.contains(r#""type":"rebuild_table""#),
"RebuildTable should serialize with tag 'rebuild_table'; got: {json}"
);
assert_eq!(change.id(), "chg-rb1");
}
#[test]
fn change_rebuild_table_roundtrip() {
let json = serde_json::json!({
"type": "rebuild_table", "id": "rb", "sql": "SELECT 1"
});
let c: Change = serde_json::from_value(json).unwrap();
match c {
Change::RebuildTable { sql, .. } => assert_eq!(sql, "SELECT 1"),
_ => panic!("expected RebuildTable"),
}
}
#[test]
fn role_info_roundtrip() {
let r = RoleInfo {
name: "app".into(), superuser: false, inherit: true, create_db: false,
create_role: false, can_login: true, replication: false, bypass_rls: false,
connection_limit: -1, valid_until: None, memberships: vec![RoleMembership {
role: "parent".into(), member: "app".into(), grantor: "admin".into(), admin_option: false,
}],
};
let json = serde_json::to_string(&r).unwrap();
assert!(json.contains(r#""name":"app""#));
assert!(json.contains(r#""can_login":true"#));
let back: RoleInfo = serde_json::from_str(&json).unwrap();
assert_eq!(back.memberships.len(), 1);
}
#[test]
fn privilege_entry_and_readiness_roundtrip() {
let pe = PrivilegeEntry {
object_class: "table".into(), schema: Some("public".into()), name: "users".into(),
privileges: vec!["SELECT".into(), "INSERT".into()], grantable: false,
};
assert!(serde_json::to_string(&pe).unwrap().contains(r#""object_class":"table""#));
let rr = RebuildReadiness { ok: false, reasons: vec!["has triggers".into()] };
assert!(serde_json::to_string(&rr).unwrap().contains(r#""ok":false"#));
let mr = MaintenanceResult { duration_ms: 42, message: "ok".into() };
assert!(serde_json::to_string(&mr).unwrap().contains(r#""duration_ms":42"#));
let ts = TablespaceInfo { name: "pg_default".into() };
assert!(serde_json::to_string(&ts).unwrap().contains("pg_default"));
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ pub mod tag;
pub use connection::{Connection, ConnectionInput};
#[allow(unused_imports)]
pub use db_viewer::{Change, ColumnInfo, FilterRule, Pagination, QueryResult, SortRule, TableInfo};
pub use db_viewer::{Change, ColumnInfo, FilterRule, MaintenanceResult, Pagination, PrivilegeEntry, QueryResult, RebuildReadiness, RoleInfo, RoleMembership, SortRule, TableInfo, TablespaceInfo};
pub use folder::{Folder, FolderInput};
pub use recent::RecentConnection;
pub use settings::Settings;