yggdrasil/components/
sql_result_table.rs1use leptos::prelude::*;
10
11use crate::api::database::sql_console::SqlResult;
12use crate::components::ui::{Icon, IconName};
13
14fn render_cell(value: &serde_json::Value) -> AnyView {
20 use serde_json::Value;
21 match value {
22 Value::Null => view! { <span class="sql-null">"NULL"</span> }.into_any(),
23 Value::Bool(b) => {
24 let text = b.to_string();
25 let value = text.clone();
26 view! { <span class="sql-bool" data-value=value>{text}</span> }.into_any()
27 }
28 Value::Number(n) => view! { <span class="sql-num">{n.to_string()}</span> }.into_any(),
29 Value::String(s) => view! { <span class="sql-text">{s.clone()}</span> }.into_any(),
30 other => view! { <span class="sql-text">{other.to_string()}</span> }.into_any(),
32 }
33}
34
35fn render_expanded_value(col: &str, value: &serde_json::Value) -> impl IntoView {
39 use serde_json::Value;
40 let (display, is_null) = match value {
41 Value::Null => ("NULL".to_string(), true),
42 Value::Bool(b) => (b.to_string(), false),
43 Value::Number(n) => (n.to_string(), false),
44 Value::String(s) => (s.clone(), false),
45 other => (other.to_string(), false),
46 };
47 view! {
48 <div class="sql-detail-row">
49 <span class="sql-detail-key">{col.to_string()}</span>
50 {if is_null {
51 view! { <span class="sql-null">"NULL"</span> }.into_any()
52 } else {
53 view! { <span class="sql-detail-value">{display}</span> }.into_any()
54 }}
55 </div>
56 }
57}
58
59#[component]
67pub fn SqlResultTable(
68 result: SqlResult,
69 #[prop(optional, into)] initial_expanded: MaybeProp<usize>,
70) -> impl IntoView {
71 let expanded_row = RwSignal::new(initial_expanded.get_untracked());
72 let expand_colspan = (result.columns.len() + 1).to_string();
74 let SqlResult { columns, rows, .. } = result;
75
76 let head_cells = columns
77 .iter()
78 .map(|col| view! { <th>{col.clone()}</th> })
79 .collect_view();
80
81 let body_rows = rows
82 .into_iter()
83 .enumerate()
84 .map(|(row_idx, row)| {
85 let cells = row
86 .iter()
87 .map(|cell| view! { <td>{render_cell(cell)}</td> })
88 .collect_view();
89 let columns = columns.clone();
90 let expand_colspan = expand_colspan.clone();
91 view! {
92 <tr
94 class="sql-row"
95 data-expanded=move || {
96 if expanded_row.get() == Some(row_idx) { "true" } else { "false" }
97 }
98 on:click=move |_| {
99 let cur = expanded_row.get_untracked();
100 expanded_row.set(if cur == Some(row_idx) { None } else { Some(row_idx) });
101 }
102 >
103 <td class="sql-toggle-cell">
104 <Icon name=IconName::ChevronRight size=14/>
105 </td>
106 {cells}
107 </tr>
108 {move || {
110 (expanded_row.get() == Some(row_idx))
111 .then(|| {
112 let details = columns
113 .iter()
114 .enumerate()
115 .map(|(ci, col)| {
116 render_expanded_value(
117 col,
118 row.get(ci).unwrap_or(&serde_json::Value::Null),
119 )
120 })
121 .collect_view();
122 view! {
123 <tr class="sql-detail">
124 <td colspan=expand_colspan.clone()>
125 <div class="sql-detail-body">{details}</div>
126 </td>
127 </tr>
128 }
129 })
130 }}
131 }
132 })
133 .collect_view();
134
135 view! {
136 <div class="sql-result">
137 <table class="admin-table">
138 <thead>
139 <tr>
140 <th class="sql-toggle-cell">
141 <span class="sr-only">"展开"</span>
142 </th>
143 {head_cells}
144 </tr>
145 </thead>
146 <tbody>{body_rows}</tbody>
147 </table>
148 </div>
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 fn sample() -> SqlResult {
157 SqlResult {
158 columns: vec!["id".to_string(), "name".to_string(), "ok".to_string()],
159 rows: vec![
160 vec![
161 serde_json::json!(1),
162 serde_json::json!("alpha"),
163 serde_json::json!(true),
164 ],
165 vec![
166 serde_json::json!(2),
167 serde_json::Value::Null,
168 serde_json::json!(false),
169 ],
170 ],
171 statement_type: "SELECT".to_string(),
172 ..Default::default()
173 }
174 }
175
176 #[test]
177 fn renders_cells_by_value_variant_and_collapsed_rows() {
178 let html = Owner::new().with(|| view! { <SqlResultTable result=sample()/> }.to_html());
179 assert!(html.contains("class=\"sql-num\""), "{html}");
180 assert!(html.contains("class=\"sql-null\""), "{html}");
181 assert!(html.contains("data-value=\"true\""), "{html}");
182 assert!(html.contains("data-expanded=\"false\""), "{html}");
183 assert!(!html.contains("sql-detail"), "{html}");
184 }
185
186 #[test]
187 fn initial_expanded_row_renders_spanning_detail() {
188 let html = Owner::new()
189 .with(|| view! { <SqlResultTable result=sample() initial_expanded=1/> }.to_html());
190 assert!(html.contains("data-expanded=\"true\""), "{html}");
191 assert!(html.contains("colspan=\"4\""), "{html}");
193 assert!(html.contains("sql-detail-key"), "{html}");
194 }
195}