From 687dec948ec0e64852fe987b1cf01e3a79cfa79a Mon Sep 17 00:00:00 2001 From: Jaromil Date: Sun, 14 Oct 2018 12:53:35 +0200 Subject: [PATCH] mustache templating, reorganisation and cleanup of code --- clojure_frontend/project.clj | 3 +- .../resources/templates/body_addjob.html | 24 +++ .../resources/templates/body_footer.html | 28 +++ .../resources/templates/body_loginform.html | 27 +++ .../resources/templates/body_signupform.html | 32 +++ .../resources/templates/html_head.html | 15 ++ clojure_frontend/src/toaster/bulma.clj | 194 ------------------ clojure_frontend/src/toaster/handler.clj | 84 ++++---- clojure_frontend/src/toaster/jobs.clj | 1 - clojure_frontend/src/toaster/session.clj | 41 +++- clojure_frontend/src/toaster/views.clj | 50 ++--- 11 files changed, 215 insertions(+), 284 deletions(-) create mode 100644 clojure_frontend/resources/templates/body_addjob.html create mode 100644 clojure_frontend/resources/templates/body_footer.html create mode 100644 clojure_frontend/resources/templates/body_loginform.html create mode 100644 clojure_frontend/resources/templates/body_signupform.html create mode 100644 clojure_frontend/resources/templates/html_head.html diff --git a/clojure_frontend/project.clj b/clojure_frontend/project.clj index b163614..183b5e5 100644 --- a/clojure_frontend/project.clj +++ b/clojure_frontend/project.clj @@ -26,7 +26,8 @@ [me.raynes/conch "0.8.0"] ;; time from joda-time [clj-time "0.14.4"] - [clojure-humanize "0.2.2"]] + [clojure-humanize "0.2.2"] + [de.ubercode.clostache/clostache "1.4.0"]] :aliases {"test" "midje"} :source-paths ["src"] :resource-paths ["resources"] diff --git a/clojure_frontend/resources/templates/body_addjob.html b/clojure_frontend/resources/templates/body_addjob.html new file mode 100644 index 0000000..8f03d25 --- /dev/null +++ b/clojure_frontend/resources/templates/body_addjob.html @@ -0,0 +1,24 @@ +
+

Upload a Dockerfile to toast

+

Choose the file in your computer and click 'Submit' to proceed to validation.

+
+
+
+
+
+ +
+
+ +
diff --git a/clojure_frontend/resources/templates/body_footer.html b/clojure_frontend/resources/templates/body_footer.html new file mode 100644 index 0000000..d7ca76b --- /dev/null +++ b/clojure_frontend/resources/templates/body_footer.html @@ -0,0 +1,28 @@ + diff --git a/clojure_frontend/resources/templates/body_loginform.html b/clojure_frontend/resources/templates/body_loginform.html new file mode 100644 index 0000000..e126f69 --- /dev/null +++ b/clojure_frontend/resources/templates/body_loginform.html @@ -0,0 +1,27 @@ +
+
+
+
+

toaster.do

+

from Docker to VM in a few clicks, powered by DECODEproject.EU

+

please login to operate

+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+

...or signup for a new account

+
+
+
+
diff --git a/clojure_frontend/resources/templates/body_signupform.html b/clojure_frontend/resources/templates/body_signupform.html new file mode 100644 index 0000000..1eb7e43 --- /dev/null +++ b/clojure_frontend/resources/templates/body_signupform.html @@ -0,0 +1,32 @@ +
+
+
+
+

toaster.do

+

sign up for a new account

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/clojure_frontend/resources/templates/html_head.html b/clojure_frontend/resources/templates/html_head.html new file mode 100644 index 0000000..5e4b85a --- /dev/null +++ b/clojure_frontend/resources/templates/html_head.html @@ -0,0 +1,15 @@ + + + + + toaster.do + + + + + + + + + + diff --git a/clojure_frontend/src/toaster/bulma.clj b/clojure_frontend/src/toaster/bulma.clj index dbb5076..3f47be6 100644 --- a/clojure_frontend/src/toaster/bulma.clj +++ b/clojure_frontend/src/toaster/bulma.clj @@ -24,20 +24,6 @@ [hiccup.page :as page] [hiccup.form :as hf])) -(declare render) -(declare render-head) -(declare navbar-guest) -(declare navbar-account) -(declare render-footer) -(declare render-yaml) -(declare render-edn) - - -;; TODO: navbars for bulma -(def navbar-brand [:div {:class "navbar"}]) -(def navbar-guest [:div {:class "navbar"}]) -(def navbar-account [:div {:class "navbar"}]) - (defn button ([url text] (button url text [:p])) @@ -65,106 +51,6 @@ (:submit-params argmap) "btn-primary btn-lg btn-success col-md-3")]) -(defn render - ([body] - {:headers {"Content-Type" - "text/html; charset=utf-8"} - :body (page/html5 - (render-head) - ;; navbar-guest - body - (render-footer))}) - ([account body] - {:headers {"Content-Type" - "text/html; charset=utf-8"} - :body (page/html5 - (render-head) - ;; (if (empty? account) - ;; navbar-guest - ;; navbar-account) - body - (render-footer))})) - -(defn notify - "render a notification message without ending the page" - ([msg] (notify msg "")) - ([msg class] - [:div {:class (str "notification " class " has-text-centered")} msg])) - -(defn render-error [err] (->> "is-error" (notify (log/spy :error err)) render)) - -(defn render-head - ([] (render-head - "toaster.do" ;; default title - "From Docker to VM or ARM SDcard in a few clicks, powered by Devuan and the DECODE project" "https://toaster.dyne.org")) ;; default desc - - ([title desc url] - [:head [:meta {:charset "utf-8"}] - [:meta {:http-equiv "X-UA-Compatible" :content "IE=edge,chrome=1"}] - [:meta - {:name "viewport" - :content "width=device-width, initial-scale=1"}] - - [:title title] - [:link {:href "https://fonts.googleapis.com/css?family=Ubuntu:300,400,700" - :rel "stylesheet"}] - - ;; non-invasive js for syntax highlight of text-area - (page/include-js "/static/js/codemirror.js") - (page/include-js "/static/js/codemirror-simple.js") - (page/include-js "/static/js/codemirror-dockerfile.js") - - ;; cascade style sheets - (page/include-css "/static/css/bulma.min.css") - (page/include-css "/static/css/json-html.css") - (page/include-css "/static/css/codemirror.css") - - - ;; (page/include-css "/static/css/fa-regular.min.css") - ;; (page/include-css "/static/css/fontawesome.min.css") - - (page/include-css "https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css") - (page/include-css "/static/css/toaster.css")])) - -(defn render-footer [] - [:footer {:class "footer"} - [:div {:class "content has-text-centered"} - [:p {:class "has-text-grey"} "toaster.do transforms your Docker prototype into " - "an installable " [:a {:href "https://devuan.org"} "Devuan GNU+Linux"] - " image, choose any supported target architecture."] - [:div {:class "columns is-variable is-8"} - - [:div {:class "column is-one-fifth"} - [:figure {:class "image"} - [:img {:src "/static/img/ec_logo.png" - :alt "European project DECODE (H2020 nr. 732546)"}]]] - - [:div {:class "column is-one-fifth"} - [:a {:href "https://decodeproject.eu"} - [:figure {:class "image"} - [:img {:src "/static/img/decode_logo.png" - :alt "DECODE project"}]]]] - - [:div {:class "column is-one-fifth"} - [:a {:href "https://www.dyne.org"} - [:figure {:class "image"} - [:img {:src "/static/img/swbydyne.png" - :alt "Software by Dyne.org" - :title "Software by Dyne.org"}]]]] - - [:div {:class "column is-one-fifth"} - [:a {:href "https://devuan.org"} - [:figure {:class "image"} - [:img {:src "/static/img/devuan_logo.png" - :alt "powered by Devuan GNU+Linux"}]]]] - - [:div {:class "column is-one-fifth"} - [:figure {:class "image"} - [:img {:src "/static/img/AGPLv3.png" ;; :style "margin-top: 2.5em" - :alt "Affero GPLv3 License" - :title "Affero GPLv3 License"}]]]]]]) - - ;; highlight functions do no conversion, take the format they highlight ;; render functions take edn and convert to the highlight format ;; download functions all take an edn and convert it in target format @@ -222,83 +108,3 @@ :type "submit" :value "submit"}]]) (def brand-img "/static/img/cafudda.jpg") -(defn- hero-login-box [body] - [:section {:class "hero is-fullheight"} - [:div {:class "hero-body"} - [:div {:class "container has-text-centered"} - body - ]]]) - -(def login-form - (hero-login-box - [:div {:class "column is-4 is-offset-4"} - [:h3 {:class "title has-text-grey"} "toaster.do"] - [:h4 {:class "subtitle has-text-grey"} - "from Docker to VM in a few clicks, powered by " - [:a {:href "https://decodeproject.eu"} "DECODEproject.EU"]] - [:p {:class "subtitle has-text-grey"} - "please login to operate"] - [:div {:class "box"} - [:figure {:class "avatar"} - [:img {:src brand-img }]] - [:form {:action "/login" - :method "post"} - [:div {:class "field"} - ;; [:label {:class "label is-large"} "Email"] - [:div {:class "control has-icons-left"} - [:input {:type "email" :name "username" - :placeholder "Email" - :class "input is-large"}] - [:span {:class "icon is-small is-left"} - [:i {:class "fa fa-envelope fa-xs"}]]]] - [:div {:class "field"} - ;; [:label {:class "label is-large"} "Password"] - [:div {:class "control has-icons-left"} - [:input {:type "password" :name "password" - :placeholder "Password" - :class "input is-large"}] - [:span {:class "icon is-small is-left"} - [:i {:class "fa fa-lock fa-xs"}]]]] - [:div {:class "field"} - [:p {:class "control"} - [:input {:type "submit" :value "Login" - :class "button is-block is-info is-large has-icons-left is-fullwidth"}]]]]] - [:p {:class "subtitle has-text-grey"} - "...or " [:a {:href "/signup"} "signup for a new account"]]])) - -(def signup-form - (hero-login-box - [:div {:class "column is-4 is-offset-4"} - [:h3 {:class "title has-text-grey"} "toaster.do"] - [:h4 {:class "subtitle has-text-grey"} - "sign up for a new account"] -;; [:p {:class "subtitle has-text-grey"}] - [:div {:class "box"} - [:figure {:class "avatar"} - [:img {:src brand-img}]] - [:form {:action "/signup" - :method "post"} - [:div {:class "field"} - [:div {:class "control"} - [:input {:type "text" :name "name" - :placeholder "Name" - :class "input"}]]] - [:div {:class "field"} - [:div {:class "control"} - [:input {:type "email" :name "email" - :placeholder "Email" - :class "input"}]]] - [:div {:class "field"} - [:div {:class "control"} - [:input {:type "password" :name "password" - :placeholder "Password" - :class "input"}]]] - [:div {:class "field"} - [:div {:class "control"} - [:input {:type "password" :name "repeat-password" - :placeholder "Repeat password" - :class "input"}]]] - [:div {:class "field"} - [:div {:class "control"} - [:input {:type "submit" :value "Sign In" - :class "button is-block is-info is-large is-fullwidth"}]]]]]])) diff --git a/clojure_frontend/src/toaster/handler.clj b/clojure_frontend/src/toaster/handler.clj index a0175aa..769e364 100644 --- a/clojure_frontend/src/toaster/handler.clj +++ b/clojure_frontend/src/toaster/handler.clj @@ -37,16 +37,17 @@ [toaster.session :as s] [toaster.config :as conf] - [toaster.bulma :as web :refer [render notify login-form]] [toaster.ring :as ring] - [toaster.views :as views] - [toaster.jobs :as job]) + [toaster.views :as views]) (:gen-class)) (defonce config (conf/load-config "toaster" conf/default-settings)) - -(defn auth-wrap [request fun] +(defn auth-wrap + "Comfortably wrap routes using a function pointer to be called if an + account is correctly authenticated in the currently running session. + (fun) is passed 3 hash-maps args: request, config and account." + [request fun] (f/attempt-all [db (s/check-database) config (s/check-config request) @@ -58,22 +59,9 @@ (s/check-account request))] (fun request config account) (f/when-failed [e] - (web/render [:body - (web/notify (f/message e) "is-error") - web/login-form])))) - -(defn- login-page [request form] - (f/attempt-all - [acct (s/check-account request)] - (web/render acct - [:body - [:h1 {:class "title"} - (str "Already logged in with account: " - (:email acct))] - [:h2 {:class "subtitle"} - [:a {:href "/logout"} "Logout"]]]) - (f/when-failed [e] - (web/render [:body form])))) + (s/render [:body + (s/notify (f/message e) "is-error") + (s/resource s/login)])))) (defroutes app-routes @@ -83,16 +71,16 @@ [db (s/check-database) conf (s/check-config request)] (f/if-let-ok? [account (s/check-account request)] - (web/render [:body (views/dashboard account)]) + (s/render [:body (views/dashboard account)]) ;; else - (web/render [:body web/login-form])) + (s/render [:body (s/resource s/login)])) (f/when-failed[e] - (web/render [:body (web/notify (f/message e) "is-error")])))) + (s/render [:body (s/notify (f/message e) "is-error")])))) ;; NEW ROUTES HERE (POST "/dockerfile" request (->> (fn [req conf acct] - (web/render acct + (s/render acct [:body (views/dockerfile-upload-post req conf acct) (views/dashboard acct)])) @@ -100,34 +88,34 @@ (POST "/remove" request (->> (fn [req conf acct] - (web/render acct [:body + (s/render acct [:body (views/remove-job req conf acct) (views/dashboard acct)])) (auth-wrap request))) (POST "/start" request (->> (fn [req conf acct] - (web/render acct [:body + (s/render acct [:body (views/start-job req conf acct) (views/dashboard acct)])) (auth-wrap request))) (POST "/view" request (->> (fn [req conf acct] - (web/render acct [:body + (s/render acct [:body (views/view-job req conf acct) (views/dashboard acct)])) (auth-wrap request))) (GET "/error" request (->> (fn [req conf acct] - (web/render acct [:body - (web/notify "Generic Error Page" "is-error") + (s/render acct [:body + (s/notify "Generic Error Page" "is-error") (views/dashboard acct)])) (auth-wrap request))) ;; JUST-AUTH ROUTES - (GET "/login" request (login-page request web/login-form)) + (GET "/login" request (s/render (s/resource s/login))) (POST "/login" request (f/attempt-all @@ -139,22 +127,22 @@ (let [session {:session {:config config :auth logged}}] (conj session - (web/render logged [:body (views/dashboard logged)]))) - ;; (web/render + (s/render logged [:body (views/dashboard logged)]))) + ;; (s/render ;; logged ;; [:div ;; [:h1 "Logged in: " username] ;; views/welcome-menu]))) (f/when-failed [e] - (web/render [:body (web/notify + (s/render [:body (s/notify (str "Login failed: " (f/message e)) "is-error")])))) (GET "/logout" request (conj {:session {:config config}} - (web/render [:body + (s/render [:body [:h1 {:class "title"} "Logged out."]]))) - (GET "/signup" request (login-page request web/signup-form)) + (GET "/signup" request (s/render (s/resource s/signup))) (POST "/signup" request (f/attempt-all [name (s/param request :name) @@ -163,7 +151,7 @@ repeat-password (s/param request :repeat-password) activation {:activation-uri (get-in request [:headers "host"])}] - (web/render + (s/render (if (= password repeat-password) (f/try* (f/if-let-ok? @@ -178,15 +166,15 @@ name " <" email ">")] [:h3 "Account pending activation."]] [:body - (web/notify + (s/notify (str "Failure creating account: " (f/message signup)) "is-error") - (login-page request web/signup-form)])) - [:body (web/notify + (s/resource s/signup)])) + [:body (s/notify "Repeat password didnt match" "is-error")])) (f/when-failed [e] - (web/render - [:body (web/notify + (s/render + [:body (s/notify (str "Sign-up failure: " (f/message e)) "is-error")])))) (GET "/activate/:email/:activation-id" @@ -195,28 +183,28 @@ (str "http://" (get-in request [:headers "host"]) "/activate/" email "/" activation-id)] - (web/render + (s/render [:body (f/if-let-failed? [act (auth/activate-account @ring/auth email {:activation-link activation-uri})] - (web/notify + (s/notify [:span [:h1 {:class "title"} "Failure activating account"] [:h2 {:class "subtitle"} (f/message act)] [:p (str "Email: " email " activation-id: " activation-id)]] "is-error") - (web/notify [:h1 {:class "title"} (str "Account activated - " email)] "is-success"))]))) + (s/notify [:h1 {:class "title"} (str "Account activated - " email)] "is-success"))]))) ;; -- end of JUST-AUTH (POST "/" request ;; generic endpoint for canceled operations - (web/render (s/check-account request) - (web/notify + (s/render (s/check-account request) + (s/notify (s/param request :message) "is-error"))) (route/resources "/") - (route/not-found (web/render [:body (web/notify "Page Not Found" "is-error")])) + (route/not-found (s/render [:body (s/notify "Page Not Found" "is-error")])) ) ;; end of routes diff --git a/clojure_frontend/src/toaster/jobs.clj b/clojure_frontend/src/toaster/jobs.clj index 730ca91..650b2f3 100644 --- a/clojure_frontend/src/toaster/jobs.clj +++ b/clojure_frontend/src/toaster/jobs.clj @@ -9,7 +9,6 @@ [failjure.core :as f] [taoensso.timbre :as log :refer [debug]] [me.raynes.conch :as sh :refer [with-programs]] - [toaster.bulma :as web] [toaster.config :refer :all] [toaster.ring :refer [jobs]] [hiccup.form :as hf])) diff --git a/clojure_frontend/src/toaster/session.clj b/clojure_frontend/src/toaster/session.clj index cf16655..d00a429 100644 --- a/clojure_frontend/src/toaster/session.clj +++ b/clojure_frontend/src/toaster/session.clj @@ -5,8 +5,19 @@ [taoensso.timbre :as log] [failjure.core :as f] [just-auth.core :as auth] - [toaster.ring :as ring] - [toaster.bulma :as web])) + [hiccup.page :as page :refer [html5]] + [clostache.parser :refer [render-resource]] + [toaster.ring :as ring])) + +(defn resource + "renders a template, optionally passing it an hash-map of parameters." + ([template] (resource template {})) + ([template params] (render-resource template params))) + +(defonce login "templates/body_loginform.html") +(defonce signup "templates/body_signupform.html") +(defonce head "templates/html_head.html") +(defonce footer "templates/body_footer.html") (defn param [request param] (let [value @@ -46,3 +57,29 @@ (if-let [db @ring/db] db (f/fail "No connection to database."))) + + +(defn notify + "render a notification message without ending the page" + ([msg] (notify msg "")) + ([msg class] + ;; support also is-error (not included as notify class in bulma + (let [tclass (if (= class "is-error") "is-danger" class)] + (cond ;; log to console using timbre + (= tclass "is-danger") (log/error msg) + (= tclass "is-warning") (log/warn msg) + (= tclass "is-success") (log/info msg) + (= tclass "is-primary") (log/debug msg)) + [:div {:class (str "notification " tclass " has-text-centered")} msg]))) + +(defn render + "render a full webpage using headers, navbar, body and footer" + ([body] (render nil body)) + ([account body] + {:headers {"Content-Type" + "text/html; charset=utf-8"} + :body (page/html5 + (resource head) + (conj body (resource footer)))})) + +(defn render-error [err] (->> "is-danger" (notify (log/spy :error err)) render)) diff --git a/clojure_frontend/src/toaster/views.clj b/clojure_frontend/src/toaster/views.clj index 6c00d9a..6442547 100644 --- a/clojure_frontend/src/toaster/views.clj +++ b/clojure_frontend/src/toaster/views.clj @@ -22,7 +22,7 @@ [clojure.string :as str] [clojure.contrib.humanize :as humanize :refer [datetime]] ;; [clojure.data.json :as json :refer [read-str]] - [toaster.bulma :as web :refer [button notify render-yaml]] + [toaster.bulma :as web :refer [button render-yaml]] [toaster.session :as s] [toaster.ring :as ring] [toaster.jobs :as job] @@ -35,8 +35,10 @@ [clj-time.coerce :as tc] [clj-time.local :as tl] [clj-storage.core :as db] - [hiccup.form :as hf])) + [hiccup.form :as hf] + [clostache.parser :refer [render-resource]])) +;; TODO: templated (defn- box-list [account joblist] [:div {:class "box"} [:h1 {:class "title"} (str "List all toaster jobs for " (:name account))] @@ -61,34 +63,6 @@ (web/button "/remove" "\uD83D\uDDD1" (hf/hidden-field "jobid" jobid))]]] ))]]]) -(defn- box-add [] - [:div {:class "box"} - [:h1 {:class "title"} "Upload a Dockerfile to toast"] - [:p " Choose the file in your computer and click 'Submit' to - proceed to validation."] - [:form {:action "dockerfile" :method "post" - :class "form-shell" - :enctype "multipart/form-data"} - [:div {:class "file has-label is-fullwidth"} - [:label {:class "file-label"} - [:input {:class "file-input inputfile inputfile-2" :id "file" :type "file"}] - [:label {:for "file"} [:span {:id "filename"} "Choose a Dockerfile..."]] - [:span {:class "file-cta"} - [:span {:class "file-icon"} - [:i {:class "fa fa-upload"}]] - [:span {:class "file-label"} "Upload"]]]] - ;; [:fieldset {:class "fieldset-submit"} - [:div {:class "field"} - [:div {:class "control"} - [:input {:class "button is-block is-info is-large is-fullwidth" - :type "submit" - :name "submit" :value "submit"}]]]] - [:script "var file = document.getElementById(\"file\"); - file.onchange = function(){ - if(file.files.length > 0) { - document.getElementById('filename').innerHTML = file.files[0].name; - } };"]]) - (defn dockerfile-upload-post [request config account] (let [tempfile (get-in request [:params :file :tempfile]) @@ -112,7 +86,7 @@ [:h1 {:class "title"} "Job uploaded and added"] [:p "Log messages:"] (web/render-yaml newjob)] - (web/notify (f/message newjob) "is-error"))))))) + (s/notify (f/message newjob) "is-error"))))))) (defn dashboard ([account] (dashboard {} {} account)) @@ -125,9 +99,9 @@ ;(if (> 0 (count joblist)) (box-list account joblist) ;) - (box-add) ]] + (s/resource "templates/body_addjob.html") ]] (f/when-failed [e] - (web/notify + (s/notify (str "Job list failure: " (f/message e)) "is-error"))))) (defn remove-job [request config account] @@ -136,18 +110,18 @@ jobfound (db/query @ring/jobs {:jobid jobid}) r_rmjob (db/delete! @ring/jobs jobid) r_sync (job/sync_jobs config "-d" jobid)] - (web/notify (str "Job removed: " jobid) "is-primary") + (s/notify (str "Job removed: " jobid) "is-primary") (f/when-failed [e] - (web/notify (str "Failure removing job: " (f/message e)) "is-error")))) + (s/notify (str "Failure removing job: " (f/message e)) "is-error")))) (defn start-job [request config account] (f/attempt-all [jobid (s/param request :jobid) jobfound (db/query @ring/jobs {:jobid jobid}) r_sync (job/sync_jobs config "-r" jobid)] - (web/notify (str "Job started: " jobid) "is-success") + (s/notify (str "Job started: " jobid) "is-success") (f/when-failed [e] - (web/notify (str "Failure starting job: " (f/message e)) "is-error")))) + (s/notify (str "Failure starting job: " (f/message e)) "is-error")))) (defn view-job [request config account] (f/attempt-all @@ -160,4 +134,4 @@ [:script "var editor = CodeMirror.fromTextArea(document.getElementById(\"code\"), { lineNumbers: true, mode: \"dockerfile\" });"]] (f/when-failed [e] - (web/notify (str "Failure viewing job: " (f/message e)) "is-error")))) + (s/notify (str "Failure viewing job: " (f/message e)) "is-error"))))