Skip to content

Commit e533424

Browse files
committed
Add static flow in editor visualizer
1 parent 21db721 commit e533424

4 files changed

Lines changed: 146 additions & 7 deletions

File tree

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Add the following dependency to your project:
4040
;; http://localhost:9876/index.html#/?port=9876
4141
```
4242

43-
## Custom Transit Handlers
43+
### Custom Transit Handlers
4444

4545
You can provide custom Transit write handlers to properly serialize types that aren't natively supported by Transit (used for visualizing state). These handlers should follow the format expected by cognitect.transit/writer :handlers. If not provided, the default handler will be used, which converts objects to strings.
4646

@@ -91,6 +91,21 @@ You can run multiple monitoring servers simultaneously to monitor different flow
9191
(monitor/stop-server server2)
9292
```
9393

94+
## Static Flow Graph
95+
96+
Both [Cursive](https://youtu.be/ZXIZdMcHUCA) and [Calva](https://calva.io/flares/) support displaying HTML in the editor. A static graph can be generated from your flow-config and displayed in either of those editor environments with the following:
97+
98+
``` clojure
99+
(:require
100+
[clojure.core.async.flow-static :refer [graph]]
101+
[clojure.core.async.flow :as flow])
102+
103+
;; Create a flow
104+
(def my-flow (flow/create-flow ...))
105+
106+
(graph my-flow)
107+
```
108+
94109
## License
95110

96111
Copyright © 2025

deps.edn

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
lambdaisland/glogi {:mvn/version "1.3.169"}
1515
day8.re-frame/async-flow-fx {:mvn/version "0.4.0"}
1616
day8.re-frame/http-fx {:mvn/version "0.2.4"}
17-
com.cognitect/transit-cljs {:mvn/version "0.8.280"}}
17+
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
18+
hiccup/hiccup {:mvn/version "2.0.0-RC5"}}
1819

1920
:aliases {:min {:main-opts ["-m" "shadow.cljs.devtools.cli" "release" "app"]}
2021
:dev {:extra-paths ["test"]
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
(ns clojure.core.async.flow-static
2+
(:require
3+
[clojure.java.io :as io]
4+
[hiccup2.core :as h]
5+
[clojure.data.json :as json]))
6+
7+
(defn titleize-keyword [kw]
8+
(as-> (name kw) $
9+
(clojure.string/replace $ #"-" " ")
10+
(clojure.string/split $ #"\s+")
11+
(map clojure.string/capitalize $)
12+
(clojure.string/join " " $)))
13+
14+
(defn flow-relationships [data]
15+
(reduce (fn [res [[from-proc out-port] [to-proc in-port]]]
16+
(-> res
17+
(update-in [from-proc :from] (fnil conj []))
18+
(update-in [to-proc :to] (fnil conj []))
19+
(update-in [from-proc :ins] (fnil conj []))
20+
(update-in [to-proc :outs] (fnil conj []))
21+
(update-in [to-proc :from] (fnil conj []) from-proc)
22+
(update-in [from-proc :to] (fnil conj []) to-proc)
23+
(update-in [to-proc :ins] (fnil conj []) in-port)
24+
(update-in [from-proc :outs] (fnil conj []) out-port)))
25+
{} data))
26+
27+
(defn flow-levels [relationships]
28+
(loop [result []
29+
current-level (filter (fn [[_ v]] (empty? (:from v))) relationships)
30+
remaining (apply dissoc relationships (map first current-level))]
31+
(if (empty? current-level)
32+
result
33+
(let [next-level (select-keys remaining (mapcat (fn [[_ v]] (:to v)) current-level))]
34+
(recur (conj result (map (fn [[k v]] {k v}) current-level))
35+
next-level
36+
(apply dissoc remaining (keys next-level)))))))
37+
38+
(defn proc-card [proc]
39+
[:div.middle-section-one-container
40+
[:div.title-container [:h2.title (titleize-keyword proc)]]])
41+
42+
(defn proc-el [proc-map]
43+
(let [proc (-> proc-map keys first)
44+
ins (-> proc-map vals first :ins)
45+
outs (-> proc-map vals first :outs)]
46+
[:div.card-container {:id (name proc)}
47+
[:div.proc-card {:class "expanded"}
48+
[:div.expanded-view
49+
[:div.header-labels
50+
(for [io-id ins]
51+
[:div.header-label {:id (str proc "-" io-id)} io-id])]
52+
(proc-card proc)
53+
[:div.output-section
54+
[:div.output-container
55+
(for [io-id outs]
56+
[:div.output {:id (str proc "-" io-id)} io-id])]]]]]))
57+
58+
(defn proc-row [idx row]
59+
[:div.row-3
60+
(for [proc row]
61+
(proc-el proc))])
62+
63+
(defn chart [conns]
64+
(let [relationships (flow-relationships conns)]
65+
[:div#chart
66+
(for [[idx row] (map-indexed vector (flow-levels relationships))]
67+
(proc-row idx row))]))
68+
69+
(defn json-friendly [conn]
70+
(let [from-data (first conn)
71+
to-data (second conn)
72+
from-proc (name (first from-data))
73+
from-port (name (second from-data))
74+
to-proc (name (first to-data))
75+
to-port (name (second to-data))]
76+
{"from" {"proc" from-proc "port" from-port}
77+
"to" {"proc" to-proc "port" to-port}}))
78+
79+
(defn connections-to-json [connections]
80+
(json/write-str (mapv json-friendly connections)))
81+
82+
(defn template [{:keys [conns]}]
83+
(str
84+
(h/html
85+
[:html
86+
[:head
87+
[:title "Flow Chart"]
88+
[:script (h/raw (slurp (io/resource "public/assets/js/vendor/leader-line.min.js")))]
89+
[:div#flow-data {:style "display: none;" :data-connections (h/raw (connections-to-json conns))}]
90+
[:script (h/raw "
91+
window.addEventListener('load', function() {
92+
const dataEl = document.getElementById('flow-data');
93+
const connectionsJSON = dataEl.getAttribute('data-connections');
94+
const connections = JSON.parse(connectionsJSON);
95+
function drawConnections() {
96+
setTimeout(() => {
97+
connections.forEach(conn => {
98+
const outSocketId = `:${conn.from.proc}-:${conn.from.port}`;
99+
const inSocketId = `:${conn.to.proc}-:${conn.to.port}`;
100+
const outSocketEl = document.getElementById(outSocketId);
101+
const inSocketEl = document.getElementById(inSocketId);
102+
const line = new LeaderLine(
103+
outSocketEl,
104+
inSocketEl,
105+
{color: '#52606D',
106+
size: 3,
107+
startSocket: 'bottom',
108+
endSocket: 'top',
109+
path: 'grid',
110+
// hide: true,
111+
animOptions: {duration: 1000, timing: 'ease'}});
112+
// line.show('draw');
113+
});
114+
}, 500);
115+
}
116+
drawConnections();});")]
117+
[:style "html,\nbody {\n margin: 0;\n padding: 0;\n}\n\nbody {\n height: 100vh;\n width: 100vw;\n margin-top: 50px;\n background-color: #CBD2D9;\n}\n\n.row-3 {\n display: flex;\n flex-direction: row;\n justify-content: center;\n gap: 10px;\n align-items: center;\n}\n\n.card-container {\n display: flex;\n flex-direction: column;\n align-items: center;\n margin-bottom: 40px;\n min-width: 220px;\n}\n.card-container .proc-card {\n background: #F5F7FA;\n border-radius: 3px;\n width: 100%;\n display: inline-block;\n position: relative;\n transition: all 0.4s ease-in-out;\n will-change: height;\n margin-bottom: 55px;\n}\n.card-container .proc-card.expanded .expanded-view {\n max-height: 500px;\n opacity: 1;\n visibility: visible;\n}\n.card-container .proc-card .expanded-view {\n transition: all 0.4s ease-in-out;\n max-height: 0;\n opacity: 0;\n overflow: hidden;\n visibility: hidden;\n}\n.card-container .proc-card .expanded-view .header-labels {\n display: flex;\n justify-content: center;\n padding: 0 15px;\n}\n.card-container .proc-card .expanded-view .header-labels .header-label {\n font-size: 1.75em;\n font-weight: 500;\n color: #4a4a4a;\n text-align: center;\n width: 150px;\n}\n.card-container .proc-card .expanded-view .middle-section-one-container {\n box-sizing: border-box;\n background: #52606D;\n color: #E4E7EB;\n border-radius: 2px;\n position: relative;\n padding: 10px 0;\n width: calc(100% - 20px);\n margin: auto;\n}\n.card-container .proc-card .expanded-view .title-container {\n text-align: center;\n}\n.card-container .proc-card .expanded-view .title-container .title {\n font-size: 2.3em;\n font-weight: 600;\n margin: 0;\n color: white;\n}\n.card-container .proc-card .output-section {\n width: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n box-sizing: border-box;\n}\n.card-container .proc-card .output-section .output-container {\n display: flex;\n flex-direction: row;\n gap: 15px;\n justify-content: center;\n align-items: center;\n width: calc(100% - 14px);\n margin: 0 7px;\n padding: 0 0;\n box-sizing: border-box;\n}\n.card-container .proc-card .output-section .output-container .output {\n flex: 1;\n min-width: 110px;\n padding: 0 8px;\n font-size: 1.75em;\n color: #4a4a4a;\n text-align: center;\n height: 40px;\n display: flex;\n justify-content: center;\n align-items: center;\n white-space: nowrap;\n}\n"]]
118+
[:body
119+
(chart conns)]])))
120+
121+
(defn graph [flow-config]
122+
(tagged-literal 'flare/html {:html (template flow-config)
123+
:title "Flow Chart"}))

src/clojurescript/flow_monitor_ui/routes/index/view.cljs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616

1717
; = Data Shaping Functions =====================================================
1818
(defn titleize-keyword [kw]
19-
(when kw (-> (name kw)
20-
(clojure.string/replace #"-" " ")
21-
(clojure.string/split #"\s+")
22-
(->> (map clojure.string/capitalize)
23-
(clojure.string/join " ")))))
19+
(as-> (name kw) $
20+
(clojure.string/replace $ #"-" " ")
21+
(clojure.string/split $ #"\s+")
22+
(map clojure.string/capitalize $)
23+
(clojure.string/join " " $)))
2424

2525
(defn format-number [n]
2626
(if n

0 commit comments

Comments
 (0)