Skip to content

Commit f44b3c9

Browse files
authored
Share pipeline using pastila + vis.js directly over GraphvizOnline (#216)
Fixes: #215
2 parents ed08ab4 + b47a296 commit f44b3c9

File tree

8 files changed

+76
-33
lines changed

8 files changed

+76
-33
lines changed

Cargo.lock

Lines changed: 1 addition & 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
@@ -45,6 +45,7 @@ log = { version = "0.4", default-features = false }
4545
futures-util = { version = "*", default-features = false }
4646
semver = { version = "*", default-features = false }
4747
serde = { version = "*", features = ["derive"] }
48+
serde_json = { version = "*", default-features = false, features = ["std"] }
4849
serde_yaml = { version = "*", default-features = false }
4950
quick-xml = { version = "*", features = ["serialize"] }
5051
urlencoding = { version = "*", default-features = false }

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ introspection, like `top` for Linux.
7676
- [Flamegraphs](Documentation/FAQ.md#what-is-flamegraph) (CPU/Real/Memory/Live) in TUI (thanks to [flamelens](https://github.com/ys-l/flamelens))
7777
- Share flamegraphs (using [pastila.nl](https://pastila.nl/) and [speedscope](https://www.speedscope.app/))
7878
- Share logs via [pastila.nl](https://pastila.nl/)
79-
- Share query pipelines (using [GraphvizOnline](https://dreampuf.github.io/GraphvizOnline/?engine=dot#digraph%0A%7B%0A%20%20rankdir%3D%22LR%22%3B%0A%20%20%7B%20node%20%5Bshape%20%3D%20rect%5D%0A%20%20%20%20n0%5Blabel%3D%22CountingTransform_7%22%5D%3B%0A%20%20%20%20n1%5Blabel%3D%22AddDeduplicationInfoTransform_6%22%5D%3B%0A%20%20%20%20n2%5Blabel%3D%22PlanSquashingTransform_5%22%5D%3B%0A%20%20%20%20n3%5Blabel%3D%22ApplySquashingTransform_4%22%5D%3B%0A%20%20%20%20n4%5Blabel%3D%22ConvertingTransform_0%22%5D%3B%0A%20%20%20%20n5%5Blabel%3D%22RemovingReplicatedColumnsTransform_1%22%5D%3B%0A%20%20%20%20n6%5Blabel%3D%22NestedElementsValidationTransform_2%22%5D%3B%0A%20%20%20%20n7%5Blabel%3D%22SharedMergeTreeSink_3%22%5D%3B%0A%20%20%20%20n8%5Blabel%3D%22EmptySink_8%22%5D%3B%0A%20%20%7D%0A%20%20n0%20-%3E%20n1%3B%0A%20%20n1%20-%3E%20n2%3B%0A%20%20n2%20-%3E%20n3%3B%0A%20%20n3%20-%3E%20n4%3B%0A%20%20n4%20-%3E%20n5%3B%0A%20%20n5%20-%3E%20n6%3B%0A%20%20n6%20-%3E%20n7%3B%0A%20%20n7%20-%3E%20n8%3B%0A%7D))
79+
- Share query pipelines (using [viz.js](https://github.com/mdaines/viz-js) and [pastila.nl](https://pastila.nl/))
8080
- Cluster support (`--cluster`) - aggregate data from all hosts in the cluster
8181
- Historical support (`--history`) - includes rotated `system.*_log_*` tables
8282
- `clickhouse-client` compatibility (including `--connection`) for options and configuration files

src/interpreter/flamegraph.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,7 @@ pub async fn open_in_speedscope(
9393
return Err(Error::msg("Flamegraph is empty"));
9494
}
9595

96-
let pastila_url =
97-
pastila::upload_to_pastila(&data, pastila_clickhouse_host, pastila_url).await?;
96+
let pastila_url = pastila::upload(&data, pastila_clickhouse_host, pastila_url).await?;
9897

9998
let url = format!(
10099
"https://www.speedscope.app/#profileURL={}",

src/interpreter/worker.rs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
flamegraph,
77
},
88
pastila,
9-
utils::{highlight_sql, open_graph_in_browser},
9+
utils::{highlight_sql, share_graph},
1010
view::{self, Navigation},
1111
};
1212
use anyhow::{Result, anyhow};
@@ -31,7 +31,7 @@ pub enum Event {
3131
LastQueryLog(String, RelativeDateTime, RelativeDateTime, u64),
3232
// (view_name, args)
3333
TextLog(&'static str, TextLogArguments),
34-
// [bool (true - show in TUI, false - open in browser), type, start, end]
34+
// [bool (true - show in TUI, false - share via pastila), type, start, end]
3535
ServerFlameGraph(bool, TraceType, DateTime<Local>, DateTime<Local>),
3636
// (type, bool (true - show in TUI, false - open in browser), start time, end time, [query_ids])
3737
QueryFlameGraph(
@@ -55,7 +55,7 @@ pub enum Event {
5555
// (database, query)
5656
ExplainPipeline(String, String),
5757
// (database, query)
58-
ExplainPipelineOpenGraphInBrowser(String, String),
58+
ExplainPipelineShareGraph(String, String),
5959
// (database, query)
6060
ExplainPlanIndexes(String, String),
6161
// (database, table)
@@ -74,7 +74,7 @@ pub enum Event {
7474
TableParts(String, String),
7575
// (database, table)
7676
AsynchronousInserts(String, String),
77-
// (content to share)
77+
// (content to share via pastila)
7878
ShareLogs(String),
7979
}
8080

@@ -94,9 +94,7 @@ impl Event {
9494
Event::ExplainSyntax(..) => "ExplainSyntax".to_string(),
9595
Event::ExplainPlan(..) => "ExplainPlan".to_string(),
9696
Event::ExplainPipeline(..) => "ExplainPipeline".to_string(),
97-
Event::ExplainPipelineOpenGraphInBrowser(..) => {
98-
"ExplainPipelineOpenGraphInBrowser".to_string()
99-
}
97+
Event::ExplainPipelineShareGraph(..) => "ExplainPipelineShareGraph".to_string(),
10098
Event::ExplainPlanIndexes(..) => "ExplainPlanIndexes".to_string(),
10199
Event::ShowCreateTable(..) => "ShowCreateTable".to_string(),
102100
Event::SQLQuery(view_name, _query) => format!("SQLQuery({})", view_name),
@@ -541,21 +539,24 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool)
541539
}))
542540
.map_err(|_| anyhow!("Cannot send message to UI"))?;
543541
}
544-
Event::ExplainPipelineOpenGraphInBrowser(database, query) => {
542+
Event::ExplainPipelineShareGraph(database, query) => {
545543
let pipeline = clickhouse
546544
.explain_pipeline_graph(database.as_str(), query.as_str())
547545
.await?
548546
.join("\n");
549-
cb_sink
550-
.send(Box::new(move |siv: &mut cursive::Cursive| {
551-
open_graph_in_browser(pipeline)
552-
.or_else(|err| {
553-
siv.add_layer(views::Dialog::info(err.to_string()));
554-
return anyhow::Ok(());
555-
})
556-
.unwrap();
557-
}))
558-
.map_err(|_| anyhow!("Cannot send message to UI"))?;
547+
548+
// Upload graph to pastila and open in browser
549+
match share_graph(pipeline, &pastila_clickhouse_host, &pastila_url).await {
550+
Ok(_) => {}
551+
Err(err) => {
552+
let error_msg = err.to_string();
553+
cb_sink
554+
.send(Box::new(move |siv: &mut cursive::Cursive| {
555+
siv.add_layer(views::Dialog::info(error_msg));
556+
}))
557+
.map_err(|_| anyhow!("Cannot send message to UI"))?;
558+
}
559+
}
559560
}
560561
Event::ShowCreateTable(database, table) => {
561562
let create_statement = clickhouse
@@ -718,8 +719,7 @@ async fn process_event(context: ContextArc, event: Event, need_clear: &mut bool)
718719
}
719720
Event::ShareLogs(content) => {
720721
let url =
721-
pastila::upload_logs_encrypted(&content, &pastila_clickhouse_host, &pastila_url)
722-
.await?;
722+
pastila::upload_encrypted(&content, &pastila_clickhouse_host, &pastila_url).await?;
723723

724724
let url_clone = url.clone();
725725
cb_sink

src/pastila.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ async fn get_pastila_client(pastila_clickhouse_host: &str) -> Result<clickhouse_
172172
Ok(client)
173173
}
174174

175-
pub async fn upload_to_pastila(
175+
pub async fn upload(
176176
content: &str,
177177
pastila_clickhouse_host: &str,
178178
pastila_url: &str,
@@ -210,7 +210,7 @@ pub async fn upload_to_pastila(
210210
Ok(clickhouse_url)
211211
}
212212

213-
pub async fn upload_logs_encrypted(
213+
pub async fn upload_encrypted(
214214
content: &str,
215215
pastila_clickhouse_host: &str,
216216
pastila_url: &str,

src/utils.rs

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::actions::ActionDescription;
2+
use crate::pastila;
23
use crate::view::Navigation;
34
use anyhow::{Context, Error, Result};
45
use cursive::Cursive;
@@ -16,7 +17,6 @@ use std::io::Write;
1617
use std::process::{Command, Stdio};
1718
use syntect::{highlighting::ThemeSet, parsing::SyntaxSet};
1819
use tempfile::Builder;
19-
use urlencoding::encode;
2020

2121
pub fn fuzzy_actions<F>(siv: &mut Cursive, actions: Vec<ActionDescription>, on_select: F)
2222
where
@@ -228,16 +228,58 @@ pub fn open_url_command(url: &str) -> Command {
228228
cmd
229229
}
230230

231-
pub fn open_graph_in_browser(graph: String) -> Result<()> {
231+
pub async fn share_graph(
232+
graph: String,
233+
pastila_clickhouse_host: &str,
234+
pastila_url: &str,
235+
) -> Result<()> {
232236
if graph.is_empty() {
233237
return Err(Error::msg("Graph is empty"));
234238
}
235-
let url = format!(
236-
"https://dreampuf.github.io/GraphvizOnline/#{}",
237-
encode(&graph)
239+
240+
// Create a self-contained HTML file that renders the Graphviz graph
241+
// Using viz.js from CDN for client-side rendering
242+
let html = format!(
243+
r#"<!DOCTYPE html>
244+
<html>
245+
<head>
246+
<meta charset="utf-8">
247+
<title>Graphviz Graph</title>
248+
<style>
249+
body {{ margin: 0; padding: 20px; font-family: sans-serif; }}
250+
#graph {{ text-align: center; }}
251+
</style>
252+
</head>
253+
<body>
254+
<div id="graph">Loading graph...</div>
255+
<script src="https://cdn.jsdelivr.net/npm/@viz-js/viz@3.2.4/lib/viz-standalone.js"></script>
256+
<script>
257+
const dot = {};
258+
Viz.instance().then(viz => {{
259+
const svg = viz.renderSVGElement(dot);
260+
const container = document.getElementById('graph');
261+
container.innerHTML = '';
262+
container.appendChild(svg);
263+
}}).catch(err => {{
264+
document.getElementById('graph').textContent = 'Error rendering graph: ' + err;
265+
}});
266+
</script>
267+
</body>
268+
</html>"#,
269+
serde_json::to_string(&graph)?
238270
);
271+
272+
// Upload HTML to pastila with end-to-end encryption
273+
let mut url = pastila::upload_encrypted(&html, pastila_clickhouse_host, pastila_url).await?;
274+
275+
if let Some(anchor_pos) = url.find('#') {
276+
url.insert_str(anchor_pos, ".html");
277+
}
278+
279+
// Open the URL in the browser
239280
open_url_command(&url).status()?;
240-
return Ok(());
281+
282+
Ok(())
241283
}
242284

243285
pub fn find_common_hostname_prefix_and_suffix<'a, I>(hostnames: I) -> (String, String)

src/view/queries_view.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,7 @@ impl QueriesView {
669669
let mut context_locked = self.context.lock().unwrap();
670670
context_locked.worker.send(
671671
true,
672-
WorkerEvent::ExplainPipelineOpenGraphInBrowser(database, query),
672+
WorkerEvent::ExplainPipelineShareGraph(database, query),
673673
);
674674
Ok(Some(EventResult::consumed()))
675675
}
@@ -1104,7 +1104,7 @@ impl QueriesView {
11041104
add_action!(context, &mut event_view, "Share Query events flamegraph", action_show_flamegraph(false, Some(TraceType::ProfileEvents)));
11051105
add_action!(context, &mut event_view, "Share Query live flamegraph", action_show_flamegraph(false, None));
11061106
add_action!(context, &mut event_view, "EXPLAIN INDEXES", 'I', action_explain_indexes);
1107-
add_action!(context, &mut event_view, "EXPLAIN PIPELINE graph=1 (open in browser)", 'G', action_explain_pipeline_graph);
1107+
add_action!(context, &mut event_view, "EXPLAIN PIPELINE graph=1 (share)", 'G', action_explain_pipeline_graph);
11081108
add_action!(context, &mut event_view, "KILL query", 'K', action_kill_query);
11091109
add_action!(context, &mut event_view, "Increase number of queries to render to 20", '(', action_increase_limit);
11101110
add_action!(context, &mut event_view, "Decrease number of queries to render to 20", ')', action_decrease_limit);

0 commit comments

Comments
 (0)