Skip to content

Commit 53be933

Browse files
authored
Enable image preview for byte columns (#109)
1 parent 31834a6 commit 53be933

File tree

3 files changed

+69
-1
lines changed

3 files changed

+69
-1
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ getrandom = { version = "0.3", features = ["wasm_js"] }
5757
byte-unit = "5.2.0"
5858
tracing = "0.1.44"
5959
dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false }
60+
mimetype-detector = "0.3.7"
6061

6162
[profile.release]
6263
strip = true

src/views/query_results.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
use std::sync::Arc;
22

3+
use arrow::array::AsArray;
34
use arrow::compute::concat_batches;
5+
use arrow::datatypes::DataType;
46
use arrow::record_batch::RecordBatch;
7+
use arrow_cast::base64::{BASE64_STANDARD, Engine};
58
use arrow_cast::display::array_value_to_string;
69
use datafusion::physical_plan::{ExecutionPlan, SendableRecordBatchStream};
710
use dioxus::prelude::*;
811
use futures::StreamExt;
12+
use mimetype_detector::detect;
913

1014
use crate::components::ui::Panel;
1115
use crate::utils::{export_to_csv_inner, export_to_parquet_inner, format_arrow_type};
@@ -61,6 +65,9 @@ pub fn QueryResultView(
6165
let record_batches = use_signal(Vec::<RecordBatch>::new);
6266
let remaining_stream = use_signal(|| None::<SendableRecordBatchStream>);
6367

68+
let mut decode_images = use_signal(|| false);
69+
let mut expanded_image_url = use_signal(|| None::<Arc<str>>);
70+
6471
if !initialized() {
6572
initialized.set(true);
6673
let query = query.clone();
@@ -204,6 +211,12 @@ pub fn QueryResultView(
204211
onclick: move |_| on_hide.call(id),
205212
"Hide"
206213
}
214+
button {
215+
class: if decode_images() { "btn btn-xs btn-primary" } else { "btn btn-xs btn-ghost" },
216+
title: "Decode bytes as images",
217+
onclick: move |_| decode_images.set(!decode_images()),
218+
"Decode bytes as images"
219+
}
207220
}
208221
}
209222
}
@@ -221,6 +234,18 @@ pub fn QueryResultView(
221234
div { class: "mb-4", {physical_plan_view(plan)} }
222235
}
223236

237+
if let Some(url) = expanded_image_url() {
238+
div {
239+
class: "modal modal-open",
240+
onclick: move |_| expanded_image_url.set(None),
241+
div {
242+
class: "modal-box w-fit max-w-[80vw] max-h-[80vh] overflow-auto",
243+
onclick: move |ev| ev.stop_propagation(),
244+
img { src: "{url}" }
245+
}
246+
}
247+
}
248+
224249
if batches.is_empty() {
225250
div { class: "text-xs text-base-content opacity-75",
226251
"Query executed successfully, no rows returned."
@@ -235,6 +260,7 @@ pub fn QueryResultView(
235260
let schema = merged_record_batch.schema();
236261
let total_rows = merged_record_batch.num_rows();
237262
let show_rows = visible_rows().min(total_rows);
263+
let decode_images = decode_images();
238264
rsx! {
239265
div { class: "max-h-[32rem] overflow-auto overflow-x-auto relative",
240266
table { class: "table table-zebra table-pin-rows table-xs",
@@ -261,9 +287,43 @@ pub fn QueryResultView(
261287
let cell_value = array_value_to_string(column.as_ref(), row_idx)
262288
.unwrap_or_else(|_| "NULL".to_string());
263289
let preview = cell_value.chars().take(200).collect::<String>();
290+
291+
let image_data_url: Option<String> = if decode_images {
292+
let column_value: Option<&[u8]> = if column.is_null(row_idx){
293+
None
294+
} else {
295+
match column.data_type() {
296+
DataType::BinaryView => Some(column.as_binary_view().value(row_idx)),
297+
DataType::Binary => Some(column.as_binary::<i32>().value(row_idx)),
298+
DataType::LargeBinary => Some(column.as_binary::<i64>().value(row_idx)),
299+
_ => None,
300+
}
301+
};
302+
303+
column_value.and_then(|bytes| {
304+
let mime = detect(bytes);
305+
if !mime.kind().is_image() {
306+
return None;
307+
}
308+
309+
let b64_string = BASE64_STANDARD.encode(bytes);
310+
Some(format!("data:{};base64,{}", mime.mime(), b64_string))
311+
})
312+
} else {
313+
None
314+
};
264315
rsx! {
265316
td { class: "px-1 py-1 leading-tight break-words",
266-
if cell_value.len() > 200 {
317+
if let Some(url) = &image_data_url {
318+
img {
319+
class: "max-h-24 max-w-xs object-contain cursor-pointer hover:opacity-80 transition-opacity",
320+
src: "{url}",
321+
onclick: {
322+
let url = Arc::from(url.as_str());
323+
move |_| expanded_image_url.set(Some(Arc::clone(&url)))
324+
},
325+
}
326+
} else if cell_value.len() > 200 {
267327
details {
268328
summary { class: "cursor-pointer select-none", "{preview}..." }
269329
pre { class: "whitespace-pre-wrap", "{cell_value}" }

0 commit comments

Comments
 (0)