From 6c1111182ca23860f14ec054100c9f7a772c7e35 Mon Sep 17 00:00:00 2001 From: Gosha Tcherednitchenko Date: Thu, 23 Jan 2025 16:08:03 +0000 Subject: [PATCH] feat(graph): refactor to use UIx and react-flow --- build.clj | 5 +- bun.lockb | Bin 55356 -> 73673 bytes cljs/tools/ifs/parts/core.cljs | 83 ++---- deps.edn | 25 +- package.json | 5 +- resources/public/css/flow.css | 408 +++++++++++++++++++++++++++ shadow-cljs.edn | 8 +- src/tools/ifs/parts.clj | 1 + src/tools/ifs/parts/layouts/main.clj | 5 +- src/tools/ifs/parts/pages.clj | 6 +- 10 files changed, 469 insertions(+), 77 deletions(-) create mode 100644 resources/public/css/flow.css diff --git a/build.clj b/build.clj index e7ae07b..84f447e 100644 --- a/build.clj +++ b/build.clj @@ -22,7 +22,8 @@ (ns build (:require [clojure.pprint :as pprint] - [clojure.tools.build.api :as build-api])) + [clojure.tools.build.api :as build-api] + [shadow.cljs.devtools.api :as shadow])) ;; --------------------------------------------------------- ;; Project configuration @@ -66,6 +67,8 @@ (build-api/copy-dir {:src-dirs ["src" "resources"] :target-dir class-directory}) + (shadow/release :app) + (build-api/compile-clj {:basis project-basis :class-dir class-directory :src-dirs ["src"]}) diff --git a/bun.lockb b/bun.lockb index e4ca7eec7cbcbbb3b39203da22a0152db2c63db2..cffb42a931a96fbe7fefde3f326fadc39398c8c9 100755 GIT binary patch delta 19958 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 delta 8831 zcmeHMd3;n=vc9L2hTM?O0$IA#30oqf3=K&jVKW^>eS{E(j7XSaL=w`HKsJ&fAYcas zF$j*KbfV*cfcQiO1Oo_&vLj&+tAZQKras^BbVfjZI^TEiZ3Ums&)-|-uXp%;w@%fm zQ&p$x+|xIlrY*snR|h@jx#{DTL7x_d?`rwdZ;n4RD7NHC|BZQvyZ!aGb}MHd-R4

ks1B+udIj^z_iw3bZ9vz9IzhXEGKjGi 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 diff --git a/cljs/tools/ifs/parts/core.cljs b/cljs/tools/ifs/parts/core.cljs index dfaa64f..a02c27c 100644 --- a/cljs/tools/ifs/parts/core.cljs +++ b/cljs/tools/ifs/parts/core.cljs @@ -1,67 +1,32 @@ (ns tools.ifs.parts.core (:require - ["d3" :as d3] - ["htmx.org" :default htmx])) + ["reactflow" :refer [Background Controls ReactFlow ReactFlowProvider]] + [uix.core :refer [$ defui]] + [uix.dom])) -(def node-data - [{:type "exile"} - {:type "exile"} - {:type "exile"} - {:type "manager"} - {:type "firefighter"} - {:type "firefighter"}]) +(def initial-nodes + [{:id "1" :data {:label "1"} :position {:x 250 :y 25}} + {:id "2" :data {:label "2"} :position {:x 250 :y 125}}]) -(defn drag-start [event _d] - (js/console.log "Drag started") - (let [subject (d3/select (.-subject event))] - (.raise subject))) +(def initial-edges + [{:id "e1-2" :source "1" :target "2"}]) -(defn drag [event _d] - (js/console.log "Dragging" event _d) - (let [x (.-x event) - y (.-y event) - subject (d3/select (.-subject event))] - (-> subject - (.attr "x" 10) - (.attr "y" 10)))) +(defui flow-diagram [] + (let [[nodes set-nodes] (uix.core/use-state initial-nodes) + [edges set-edges] (uix.core/use-state initial-edges)] + ($ ReactFlowProvider + ($ :div {:style {:width "100%" :height "600px"}} + ($ ReactFlow + {:nodes (clj->js nodes) + :edges (clj->js edges) + :fitView true}) + ($ Background) + ($ Controls))))) -(defn drag-end [_event _d] - (js/console.log "Drag ended")) - -(defn create-visualization [el] - (js/console.log "Creating visualization" el) - (-> d3 - (.select el) - (.append "svg") - (.attr "width" 600) - (.attr "height" 400))) - -(defn load-nodes [svg data] - (let [width 600 - height 400 - drag-behavior (-> d3 - (.drag) - (.on "start" drag-start) - (.on "drag" drag) - (.on "end" drag-end))] - (doseq [node data] - (let [x (rand-int width) - y (rand-int height) - type (:type node) - img-path (str "/images/nodes/" type ".svg")] - (-> svg - (.append "image") - (.attr "xlink:href" img-path) - (.attr "x" x) - (.attr "y" y) - (.attr "width" 50) - (.attr "height" 50) - (.attr "class" type) - (.call drag-behavior)))))) +(defonce root + (uix.dom/create-root (js/document.getElementById "root"))) (defn ^:export init [] - (.on htmx "htmx:load" - (fn [_event] - (js/console.log "htmx loaded!") - (let [svg (create-visualization (.getElementById js/document "chart"))] - (load-nodes svg node-data))))) + (uix.dom/render-root + ($ flow-diagram) + root)) diff --git a/deps.edn b/deps.edn index 95576f8..a06a432 100644 --- a/deps.edn +++ b/deps.edn @@ -1,6 +1,6 @@ {;; --------------------------------------------------------- :paths - ["src" "resources" "cljs"] + ["src" "resources"] ;; --------------------------------------------------------- ;; --------------------------------------------------------- @@ -35,11 +35,7 @@ ;; Hiccup is a library for representing HTML in Clojure. It uses vectors to ;; represent elements, and maps to represent an element's attributes. ;; https://github.com/weavejester/hiccup - hiccup/hiccup {:mvn/version "2.0.0-RC3"} - ;; - ;; Clojurescript - ;; https://github.com/clojure/clojurescript - org.clojure/clojurescript {:mvn/version "1.11.132"}} + hiccup/hiccup {:mvn/version "2.0.0-RC3"}} ;; --------------------------------------------------------- :aliases @@ -54,6 +50,23 @@ :exec-args {:name "Clojure"}} ;; ------------ + ;; ------------ + ;; Frontend + :frontend + {:extra-paths ["cljs"] + :extra-deps + {org.clojure/clojurescript {:mvn/version "1.11.132"} + com.pitch/uix.core {:mvn/version "1.3.1"} + com.pitch/uix.dom {:mvn/version "1.3.1"}}} + + :shadow-cljs + {:extra-paths ["dev/cljs"] + :extra-deps + {thheller/shadow-cljs {:mvn/version "2.28.15"} + cider/cider-nrepl {:mvn/version "0.51.1"} + refactor-nrepl/refactor-nrepl {:mvn/version "3.10.0"} + binaryage/devtools {:mvn/version "1.0.7"}}} + ;; ------------ ;; Add libraries and paths to support additional test tools :test/env diff --git a/package.json b/package.json index f84c806..702a886 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,12 @@ "dependencies": { "d3": "^7.9.0", "htmx.org": "^2.0.2", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "reactflow": "^11.11.4", "shadow-cljs": "^2.28.15" }, "scripts": { - "watch": "shadow-cljs watch app" + "watch": "bunx shadow-cljs watch app" } } diff --git a/resources/public/css/flow.css b/resources/public/css/flow.css new file mode 100644 index 0000000..a087c28 --- /dev/null +++ b/resources/public/css/flow.css @@ -0,0 +1,408 @@ +/* From: https://unpkg.com/reactflow@11.11.4/dist/style.css */ + +/* this gets exported as style.css and can be used for the default theming */ +/* these are the necessary styles for React Flow, they get used by base.css and style.css */ +.react-flow { + direction: ltr; +} +.react-flow__container { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; +} +.react-flow__pane { + z-index: 1; + cursor: -webkit-grab; + cursor: grab; +} +.react-flow__pane.selection { + cursor: pointer; + } +.react-flow__pane.dragging { + cursor: -webkit-grabbing; + cursor: grabbing; + } +.react-flow__viewport { + transform-origin: 0 0; + z-index: 2; + pointer-events: none; +} +.react-flow__renderer { + z-index: 4; +} +.react-flow__selection { + z-index: 6; +} +.react-flow__nodesselection-rect:focus, +.react-flow__nodesselection-rect:focus-visible { + outline: none; +} +.react-flow .react-flow__edges { + pointer-events: none; + overflow: visible; +} +.react-flow__edge-path, +.react-flow__connection-path { + stroke: #b1b1b7; + stroke-width: 1; + fill: none; +} +.react-flow__edge { + pointer-events: visibleStroke; + cursor: pointer; +} +.react-flow__edge.animated path { + stroke-dasharray: 5; + -webkit-animation: dashdraw 0.5s linear infinite; + animation: dashdraw 0.5s linear infinite; + } +.react-flow__edge.animated path.react-flow__edge-interaction { + stroke-dasharray: none; + -webkit-animation: none; + animation: none; + } +.react-flow__edge.inactive { + pointer-events: none; + } +.react-flow__edge.selected, + .react-flow__edge:focus, + .react-flow__edge:focus-visible { + outline: none; + } +.react-flow__edge.selected .react-flow__edge-path, + .react-flow__edge:focus .react-flow__edge-path, + .react-flow__edge:focus-visible .react-flow__edge-path { + stroke: #555; + } +.react-flow__edge-textwrapper { + pointer-events: all; + } +.react-flow__edge-textbg { + fill: white; + } +.react-flow__edge .react-flow__edge-text { + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + } +.react-flow__connection { + pointer-events: none; +} +.react-flow__connection .animated { + stroke-dasharray: 5; + -webkit-animation: dashdraw 0.5s linear infinite; + animation: dashdraw 0.5s linear infinite; + } +.react-flow__connectionline { + z-index: 1001; +} +.react-flow__nodes { + pointer-events: none; + transform-origin: 0 0; +} +.react-flow__node { + position: absolute; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + pointer-events: all; + transform-origin: 0 0; + box-sizing: border-box; + cursor: -webkit-grab; + cursor: grab; +} +.react-flow__node.dragging { + cursor: -webkit-grabbing; + cursor: grabbing; + } +.react-flow__nodesselection { + z-index: 3; + transform-origin: left top; + pointer-events: none; +} +.react-flow__nodesselection-rect { + position: absolute; + pointer-events: all; + cursor: -webkit-grab; + cursor: grab; + } +.react-flow__handle { + position: absolute; + pointer-events: none; + min-width: 5px; + min-height: 5px; + width: 6px; + height: 6px; + background: #1a192b; + border: 1px solid white; + border-radius: 100%; +} +.react-flow__handle.connectionindicator { + pointer-events: all; + cursor: crosshair; + } +.react-flow__handle-bottom { + top: auto; + left: 50%; + bottom: -4px; + transform: translate(-50%, 0); + } +.react-flow__handle-top { + left: 50%; + top: -4px; + transform: translate(-50%, 0); + } +.react-flow__handle-left { + top: 50%; + left: -4px; + transform: translate(0, -50%); + } +.react-flow__handle-right { + right: -4px; + top: 50%; + transform: translate(0, -50%); + } +.react-flow__edgeupdater { + cursor: move; + pointer-events: all; +} +.react-flow__panel { + position: absolute; + z-index: 5; + margin: 15px; +} +.react-flow__panel.top { + top: 0; + } +.react-flow__panel.bottom { + bottom: 0; + } +.react-flow__panel.left { + left: 0; + } +.react-flow__panel.right { + right: 0; + } +.react-flow__panel.center { + left: 50%; + transform: translateX(-50%); + } +.react-flow__attribution { + font-size: 10px; + background: rgba(255, 255, 255, 0.5); + padding: 2px 3px; + margin: 0; +} +.react-flow__attribution a { + text-decoration: none; + color: #999; + } +@-webkit-keyframes dashdraw { + from { + stroke-dashoffset: 10; + } +} +@keyframes dashdraw { + from { + stroke-dashoffset: 10; + } +} +.react-flow__edgelabel-renderer { + position: absolute; + width: 100%; + height: 100%; + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} +.react-flow__edge.updating .react-flow__edge-path { + stroke: #777; + } +.react-flow__edge-text { + font-size: 10px; + } +.react-flow__node.selectable:focus, + .react-flow__node.selectable:focus-visible { + outline: none; + } +.react-flow__node-default, +.react-flow__node-input, +.react-flow__node-output, +.react-flow__node-group { + padding: 10px; + border-radius: 3px; + width: 150px; + font-size: 12px; + color: #222; + text-align: center; + border-width: 1px; + border-style: solid; + border-color: #1a192b; + background-color: white; +} +.react-flow__node-default.selectable:hover, .react-flow__node-input.selectable:hover, .react-flow__node-output.selectable:hover, .react-flow__node-group.selectable:hover { + box-shadow: 0 1px 4px 1px rgba(0, 0, 0, 0.08); + } +.react-flow__node-default.selectable.selected, + .react-flow__node-default.selectable:focus, + .react-flow__node-default.selectable:focus-visible, + .react-flow__node-input.selectable.selected, + .react-flow__node-input.selectable:focus, + .react-flow__node-input.selectable:focus-visible, + .react-flow__node-output.selectable.selected, + .react-flow__node-output.selectable:focus, + .react-flow__node-output.selectable:focus-visible, + .react-flow__node-group.selectable.selected, + .react-flow__node-group.selectable:focus, + .react-flow__node-group.selectable:focus-visible { + box-shadow: 0 0 0 0.5px #1a192b; + } +.react-flow__node-group { + background-color: rgba(240, 240, 240, 0.25); +} +.react-flow__nodesselection-rect, +.react-flow__selection { + background: rgba(0, 89, 220, 0.08); + border: 1px dotted rgba(0, 89, 220, 0.8); +} +.react-flow__nodesselection-rect:focus, + .react-flow__nodesselection-rect:focus-visible, + .react-flow__selection:focus, + .react-flow__selection:focus-visible { + outline: none; + } +.react-flow__controls { + box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.08); +} +.react-flow__controls-button { + border: none; + background: #fefefe; + border-bottom: 1px solid #eee; + box-sizing: content-box; + display: flex; + justify-content: center; + align-items: center; + width: 16px; + height: 16px; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + padding: 5px; + } +.react-flow__controls-button:hover { + background: #f4f4f4; + } +.react-flow__controls-button svg { + width: 100%; + max-width: 12px; + max-height: 12px; + } +.react-flow__controls-button:disabled { + pointer-events: none; + } +.react-flow__controls-button:disabled svg { + fill-opacity: 0.4; + } +.react-flow__minimap { + background-color: #fff; +} +.react-flow__minimap svg { + display: block; +} +.react-flow__resize-control { + position: absolute; +} +.react-flow__resize-control.left, +.react-flow__resize-control.right { + cursor: ew-resize; +} +.react-flow__resize-control.top, +.react-flow__resize-control.bottom { + cursor: ns-resize; +} +.react-flow__resize-control.top.left, +.react-flow__resize-control.bottom.right { + cursor: nwse-resize; +} +.react-flow__resize-control.bottom.left, +.react-flow__resize-control.top.right { + cursor: nesw-resize; +} +/* handle styles */ +.react-flow__resize-control.handle { + width: 4px; + height: 4px; + border: 1px solid #fff; + border-radius: 1px; + background-color: #3367d9; + transform: translate(-50%, -50%); +} +.react-flow__resize-control.handle.left { + left: 0; + top: 50%; +} +.react-flow__resize-control.handle.right { + left: 100%; + top: 50%; +} +.react-flow__resize-control.handle.top { + left: 50%; + top: 0; +} +.react-flow__resize-control.handle.bottom { + left: 50%; + top: 100%; +} +.react-flow__resize-control.handle.top.left { + left: 0; +} +.react-flow__resize-control.handle.bottom.left { + left: 0; +} +.react-flow__resize-control.handle.top.right { + left: 100%; +} +.react-flow__resize-control.handle.bottom.right { + left: 100%; +} +/* line styles */ +.react-flow__resize-control.line { + border-color: #3367d9; + border-width: 0; + border-style: solid; +} +.react-flow__resize-control.line.left, +.react-flow__resize-control.line.right { + width: 1px; + transform: translate(-50%, 0); + top: 0; + height: 100%; +} +.react-flow__resize-control.line.left { + left: 0; + border-left-width: 1px; +} +.react-flow__resize-control.line.right { + left: 100%; + border-right-width: 1px; +} +.react-flow__resize-control.line.top, +.react-flow__resize-control.line.bottom { + height: 1px; + transform: translate(0, -50%); + left: 0; + width: 100%; +} +.react-flow__resize-control.line.top { + top: 0; + border-top-width: 1px; +} +.react-flow__resize-control.line.bottom { + border-bottom-width: 1px; + top: 100%; +} diff --git a/shadow-cljs.edn b/shadow-cljs.edn index cab4678..68f3880 100644 --- a/shadow-cljs.edn +++ b/shadow-cljs.edn @@ -1,9 +1,5 @@ -{:source-paths - ["cljs"] - - :dependencies - [[cider/cider-nrepl "0.51.1"] - [refactor-nrepl/refactor-nrepl "3.10.0"]] +{:deps + {:aliases [:shadow-cljs :frontend]} :nrepl {:middleware [cider.nrepl/cider-middleware diff --git a/src/tools/ifs/parts.clj b/src/tools/ifs/parts.clj index cda6285..fa0ad8b 100644 --- a/src/tools/ifs/parts.clj +++ b/src/tools/ifs/parts.clj @@ -34,6 +34,7 @@ (ring/ring-handler (ring/router [["/" {:get {:handler #(pages/home-page %)}}] + ["/system" {:get {:handler #(pages/system-graph %)}}] ["/up" {:get {:handler (fn [_] {:status 200 :body "OK"})}}] ["/waitlist-signup" {:post {:handler #(waitlist/signup %)}}]] diff --git a/src/tools/ifs/parts/layouts/main.clj b/src/tools/ifs/parts/layouts/main.clj index 6606090..4d33576 100644 --- a/src/tools/ifs/parts/layouts/main.clj +++ b/src/tools/ifs/parts/layouts/main.clj @@ -16,14 +16,15 @@ [:link {:rel "apple-touch-icon" :href "/images/icons/favicon.png"}] [:title (str title " — Parts")] [:link {:rel "stylesheet" :href "/css/style.css"}] + [:link {:rel "stylesheet" :href "/css/flow.css"}] [:link {:rel "preconnect" :href "https://fonts.googleapis.com"}] [:link {:rel "preconnect" :href "https://fonts.gstatic.com" :crossorigin true}] [:link {:rel "stylesheet" :href "https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"}] - [:script {:src "/js/main.js"}] [:script {:defer true :data-domain "parts.ifs.tools" :src "https://plausible.io/js/script.outbound-links.tagged-events.js"}]] [:body (header) content - (footer)])) + (footer) + [:script {:src "/js/main.js"}]])) diff --git a/src/tools/ifs/parts/pages.clj b/src/tools/ifs/parts/pages.clj index 31ea984..fe50611 100644 --- a/src/tools/ifs/parts/pages.clj +++ b/src/tools/ifs/parts/pages.clj @@ -11,8 +11,10 @@ (response/response (html (layout "System" - [:div [:h2 "System"]] - [:div#chart])))) + [:section.container + [:div.content + [:div [:h2 "System"]] + [:div#root]]])))) (defn home-page "Page rendered for GET /"