yggdrasil/components/
sql_result_table.rs1use dioxus::prelude::*;
10
11use crate::api::database::sql_console::SqlResult;
12use crate::components::ui::ADMIN_TABLE_CLASS;
13
14const EXPAND_MAX_HEIGHT_CLASS: &str = "max-h-80";
16const TEXT_CELL_MAX_WIDTH_CLASS: &str = "max-w-[24rem]";
18
19fn render_cell(value: &serde_json::Value) -> Element {
25 use serde_json::Value;
26 match value {
27 Value::Null => rsx! {
28 span { class: "italic text-[var(--color-paper-tertiary)]", "NULL" }
29 },
30 Value::Bool(true) => rsx! {
31 span {
32 class: "inline-flex items-center px-1.5 py-0.5 rounded text-xs font-mono",
33 style: "background-color: var(--color-paper-accent-soft); color: var(--color-paper-accent);",
34 "true"
35 }
36 },
37 Value::Bool(false) => rsx! {
38 span {
39 class: "inline-flex items-center px-1.5 py-0.5 rounded text-xs font-mono",
40 style: "background-color: rgb(254 243 199); color: rgb(180 83 9);",
41 "false"
42 }
43 },
44 Value::Number(n) => rsx! {
45 span { class: "block text-right tabular-nums text-[var(--color-paper-primary)]",
46 "{n}"
47 }
48 },
49 Value::String(s) => rsx! {
50 span { class: "block font-mono text-xs text-[var(--color-paper-secondary)] truncate {TEXT_CELL_MAX_WIDTH_CLASS}",
51 "{s}"
52 }
53 },
54 other => rsx! {
56 span { class: "block font-mono text-xs text-[var(--color-paper-secondary)] truncate {TEXT_CELL_MAX_WIDTH_CLASS}",
57 "{other}"
58 }
59 },
60 }
61}
62
63fn render_expanded_value(col: &str, value: &serde_json::Value) -> Element {
68 use serde_json::Value;
69 let display = match value {
70 Value::Null => "NULL".to_string(),
71 Value::Bool(b) => b.to_string(),
72 Value::Number(n) => n.to_string(),
73 Value::String(s) => s.clone(),
74 other => other.to_string(),
75 };
76 rsx! {
77 div { class: "flex gap-2 py-0.5",
78 span { class: "shrink-0 font-mono text-xs text-[var(--color-paper-tertiary)] min-w-[6rem]",
79 "{col}"
80 }
81 span { class: "font-mono text-xs text-[var(--color-paper-secondary)]", "{display}" }
82 }
83 }
84}
85
86#[derive(Props, Clone, PartialEq)]
88pub struct SqlResultTableProps {
89 pub result: SqlResult,
90}
91
92#[component]
98#[allow(non_snake_case)]
99#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut))]
100pub fn SqlResultTable(props: SqlResultTableProps) -> Element {
101 let mut expanded_row: Signal<Option<usize>> = use_signal(|| None);
102 let cols_len = props.result.columns.len();
103 let expand_colspan = cols_len + 1;
105
106 rsx! {
107 div { class: "{ADMIN_TABLE_CLASS}",
108 div { class: "overflow-auto max-h-[70vh]",
109 table { class: "w-full text-sm border-collapse",
110 thead {
111 tr { class: "border-b border-[var(--color-paper-border)] sticky top-0 bg-[var(--color-paper-entry)] z-10",
112 th { class: "w-8 px-2 py-2", "" }
113 for col in props.result.columns.iter() {
114 th { class: "px-4 py-2 text-left font-medium whitespace-nowrap text-[var(--color-paper-secondary)]",
115 "{col}"
116 }
117 }
118 }
119 }
120 tbody {
121 for (row_idx, row) in props.result.rows.iter().enumerate() {
122 tr {
124 class: "border-b border-[var(--color-paper-border)] last:border-0 hover:bg-[var(--color-paper-accent-soft)] transition-colors cursor-pointer",
125 onclick: move |_| {
126 let cur = expanded_row();
127 if cur == Some(row_idx) {
128 expanded_row.set(None);
129 } else {
130 expanded_row.set(Some(row_idx));
131 }
132 },
133 td { class: "px-2 py-2 text-center text-[var(--color-paper-tertiary)] text-xs select-none",
134 if expanded_row() == Some(row_idx) {
135 "▾"
136 } else {
137 "▸"
138 }
139 }
140 for cell in row.iter() {
141 td { class: "px-4 py-2 align-top", {render_cell(cell)} }
142 }
143 }
144 if expanded_row() == Some(row_idx) {
146 tr {
147 td {
148 colspan: "{expand_colspan}",
149 class: "px-4 py-3 bg-[var(--color-paper-theme)]",
150 div { class: "{EXPAND_MAX_HEIGHT_CLASS} overflow-y-auto space-y-0.5 whitespace-pre-wrap break-all",
151 for (ci, col) in props.result.columns.iter().enumerate() {
152 {render_expanded_value(col, row.get(ci).unwrap_or(&serde_json::Value::Null))}
153 }
154 }
155 }
156 }
157 }
158 }
159 }
160 }
161 }
162 }
163 }
164}