diff --git a/clojure_frontend/src/toaster/bulma.clj b/clojure_frontend/src/toaster/bulma.clj index eab563a..26f949f 100644 --- a/clojure_frontend/src/toaster/bulma.clj +++ b/clojure_frontend/src/toaster/bulma.clj @@ -19,6 +19,7 @@ (ns toaster.bulma (:require [clojure.java.io :as io] [clojure.data.csv :as csv] + [taoensso.timbre :as log] [yaml.core :as yaml] [hiccup.page :as page] [hiccup.form :as hf])) @@ -30,9 +31,6 @@ (declare render-footer) (declare render-yaml) (declare render-edn) -(declare render-error) -(declare render-error-page) -(declare render-static) ;; TODO: navbars for bulma @@ -73,39 +71,28 @@ "text/html; charset=utf-8"} :body (page/html5 (render-head) - [:body ;; {:class "static"} + [:body ;; navbar-guest body (render-footer)])}) ([account body] {:headers {"Content-Type" "text/html; charset=utf-8"} - :body (page/html5 - (render-head) - [:body ;; (if (empty? account) - ;; navbar-guest - ;; navbar-account) - body - (render-footer)])})) + :body (page/html5 + (render-head) + [:body ;; (if (empty? account) + ;; navbar-guest + ;; navbar-account) + body + (render-footer)])})) -(defn render-success - "render a successful message without ending the page" - [succ] - [:div {:class "notification is-primary"} succ]) - -(defn render-error - "render an error message without ending the page" - [err] - [:div {:class "notification is-danger"} err]) - -(defn render-error-page - ([] (render-error-page {} "Unknown")) - ([err] (render-error-page {} err)) - ([session error] - (render - [:div {:class "container"} - (render-error error)]))) +(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 @@ -136,45 +123,39 @@ [: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!"] + " image, choose any supported target architecture."] [:div {:class "columns is-variable is-8"} - [:div {:class "column is-one-quarter"} + [: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-quarter"} + [: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-quarter"} + [: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-quarter"} + [: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"}]]]]]]) -(defn render-static [body] - (page/html5 (render-head) - [:body {:class "fxc static"} - - ;; navbar-guest - - [:div {:class "container"} body] - - (render-footer) - ])) - ;; highlight functions do no conversion, take the format they highlight ;; render functions take edn and convert to the highlight format @@ -232,7 +213,7 @@ [:input {:class "btn btn-success btn-lg pull-top" :type "submit" :value "submit"}]]) -(def brand-img "/static/img/whale_toast.jpg") +(def brand-img "/static/img/cafudda.jpg") (defn- hero-login-box [body] [:section {:class "hero is-fullheight"} [:div {:class "hero-body"} diff --git a/clojure_frontend/src/toaster/handler.clj b/clojure_frontend/src/toaster/handler.clj index 5dfbb2f..59f526c 100644 --- a/clojure_frontend/src/toaster/handler.clj +++ b/clojure_frontend/src/toaster/handler.clj @@ -62,47 +62,50 @@ (defroutes app-routes - (GET "/" request (login-page request web/login-form)) + (GET "/" request + (f/attempt-all + [db (s/check-database) + conf (s/check-config request)] + (f/if-let-ok? [account (s/check-account request)] + (web/render (views/dashboard account)) + ;; else + (web/render web/login-form)) + (f/when-failed[e] + (web/render (web/notify (f/message e) "is-error"))))) ;; NEW ROUTES HERE - (GET "/upload" request - (->> (fn [req conf acct] - (web/render acct [:div - views/dockerfile-upload-form - (views/list-jobs acct)])) - (s/check request))) - (POST "/dockerfile" request (->> (fn [req conf acct] - (web/render acct (views/dockerfile-upload-post req conf acct))) - (s/check request))) + (web/render acct + [:span (views/dockerfile-upload-post req conf acct) + (views/dashboard acct)])) + (s/auth-wrap request))) - (GET "/list" request - (->> (fn [req conf acct] - (web/render acct (views/list-jobs acct))) - (s/check request))) + ;; (GET "/list" request + ;; (->> (fn [req conf acct] + ;; (web/render acct (views/list-jobs acct))) + ;; (s/auth-wrap request))) (POST "/remove" request (->> (fn [req conf acct] (web/render acct (views/remove-job req conf acct))) - (s/check request))) + (s/auth-wrap request))) (POST "/start" request (->> (fn [req conf acct] (web/render acct (views/start-job req conf acct))) - (s/check request))) + (s/auth-wrap request))) (POST "/view" request (->> (fn [req conf acct] (web/render acct (views/view-job req conf acct))) - (s/check request))) + (s/auth-wrap request))) (GET "/error" request (->> (fn [req conf acct] (web/render acct [:div - (web/render-error "Generic Error Page") - (views/list-jobs acct)])) - (s/check request))) + (web/notify "Generic Error Page" "is-error")])) + (s/auth-wrap request))) ;; JUST-AUTH ROUTES (GET "/login" request (login-page request web/login-form)) @@ -117,15 +120,15 @@ (let [session {:session {:config config :auth logged}}] (conj session - (web/render logged (views/list-jobs logged)))) + (web/render logged (views/dashboard logged)))) ;; (web/render ;; logged ;; [:div ;; [:h1 "Logged in: " username] ;; views/welcome-menu]))) (f/when-failed [e] - (web/render (web/render-error - (str "Login failed: " (f/message e))))))) + (web/render (web/notify + (str "Login failed: " (f/message e)) "is-error"))))) (GET "/logout" request (conj {:session {:config config}} @@ -155,15 +158,15 @@ [:h2 (str "Account created: " name " <" email ">")] [:h3 "Account pending activation."]] - (web/render-error + (web/notify (str "Failure creating account: " - (f/message signup))))) - (web/render-error - "Repeat password didnt match"))) + (f/message signup)) "is-error"))) + (web/notify + "Repeat password didnt match" "is-error"))) (f/when-failed [e] (web/render - (web/render-error - (str "Sign-up failure: " (f/message e))))))) + (web/notify + (str "Sign-up failure: " (f/message e)) "is-error"))))) (GET "/activate/:email/:activation-id" [email activation-id :as request] @@ -177,22 +180,22 @@ [act (auth/activate-account @ring/auth email {:activation-link activation-uri})] - (web/render-error - [:div - [:h1 "Failure activating account"] - [:h2 (f/message act)] - [:p (str "Email: " email " activation-id: " activation-id)]]) - [:h1 (str "Account activated - " email)])]))) + (web/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"))]))) ;; -- end of JUST-AUTH (POST "/" request ;; generic endpoint for canceled operations (web/render (s/check-account request) - [:div {:class (str "alert alert-danger") :role "alert"} - (s/param request :message)])) + (web/notify + (s/param request :message) "is-error"))) (route/resources "/") - (route/not-found (web/render (web/render-error "Page Not Found"))) + (route/not-found (web/render (web/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 2d2ffd3..23f2576 100644 --- a/clojure_frontend/src/toaster/jobs.clj +++ b/clojure_frontend/src/toaster/jobs.clj @@ -9,11 +9,27 @@ [failjure.core :as f] [taoensso.timbre :as log :refer [debug]] [me.raynes.conch :as sh :refer [with-programs]] - [toaster.webpage :as web] + [toaster.bulma :as web] [toaster.config :refer :all] [toaster.ring :refer [jobs]] [hiccup.form :as hf])) +;; list of arm targets to chose from +;"beagleboneblack" +;"chromeacer" +;"chromeveyron" +;"droid4" +;"n900" +;"odroidxu4" +;"odroidxu" +;"ouya" +;"raspi1" +;"raspi2" +;"raspi3" +;"rock64" +;"sunxi" +;"turbox-twister" + (defn- ssh-host [config] (str (q config [:jenkins :user]) "@" (q config [:jenkins :host]))) @@ -59,8 +75,7 @@ {:lint r_lint :job r_job} (f/when-failed [e] - (web/render-error - (str "Job add failure: " (f/message e)))))))) + (f/fail (str "Job add '" jobname "' failure: " (f/message e)))))))) (defn trash [jobid config] (f/attempt-all [r_sync (sync_jobs config "-d" jobid)] diff --git a/clojure_frontend/src/toaster/session.clj b/clojure_frontend/src/toaster/session.clj index d2d712d..f2090ce 100644 --- a/clojure_frontend/src/toaster/session.clj +++ b/clojure_frontend/src/toaster/session.clj @@ -6,7 +6,7 @@ [failjure.core :as f] [just-auth.core :as auth] [toaster.ring :as ring] - [toaster.webpage :as web])) + [toaster.bulma :as web])) (defn param [request param] (let [value @@ -29,7 +29,7 @@ (if (contains? session :config) (:config session) (conf/load-config "toaster" conf/default-settings)) - (f/fail "Session not found. "))) + (f/fail "Session not found."))) (defn check-account [request] ;; check if login is present in session @@ -39,22 +39,27 @@ user (f/when-failed [e] (->> e f/message - (str "Unauthorized access. ") + (str "Unauthorized access: ") f/fail)))) (defn check-database [] (if-let [db @ring/db] db - (f/fail "No connection to database. "))) + (f/fail "No connection to database."))) -(defn check [request fun] +(defn auth-wrap [request fun] (f/attempt-all - [db (check-database) - config (check-config request) - account (check-account request)] - (fun request config account) - (f/when-failed [e] - (web/render - [:div - (web/render-error (f/message e)) - web/login-form])))) + [db (check-database) + config (check-config request) + account (if (conf/q config [:webserver :mock-auth]) + {:email "mock@dyne.org" + :name "MockUser" + :activated true} + ;; else + (check-account request))] + (fun request config account) + (f/when-failed [e] + (web/render [:span + (web/notify (f/message e) "is-error") + web/login-form + ])))) diff --git a/clojure_frontend/src/toaster/views.clj b/clojure_frontend/src/toaster/views.clj index dc3238d..b845512 100644 --- a/clojure_frontend/src/toaster/views.clj +++ b/clojure_frontend/src/toaster/views.clj @@ -1,3 +1,21 @@ +;; Copyright (C) 2018 Dyne.org foundation + +;; Sourcecode designed, written and maintained by +;; Denis Roio + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU Affero General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU Affero General Public License for more details. + +;; You should have received a copy of the GNU Affero General Public License +;; along with this program. If not, see . + (ns toaster.views (:require [clojure.java.io :as io] @@ -19,8 +37,32 @@ [clj-storage.core :as db] [hiccup.form :as hf])) -(def dockerfile-upload-form - [:div {:class "container"} +(defn- box-list [account joblist] + [:div {:class "box column"} + [:h1 {:class "title"} (str "List all toaster jobs for " (:name account))] + [:table {:class "table is-fullwidth is-hoverable"} + [:thead nil + [:tr nil [:th nil "Date"] [:th nil "Type"] [:th nil "Status"] [:th nil "Actions"]]] + [:tbody nil + (for [j joblist] + (let [type (:type j) ;; (-> j (str/split #"-") second) + tstamp (:timestamp j) ;; (-> j (str/split #"-") last) + jobid (:jobid j) + joburl (str/replace jobid #"@" "AT")] + [:tr nil + [:td {:class "date"} (-> tstamp Long/valueOf tc/from-long humanize/datetime)] + [:td {:class "job"} [:a {:href (str "https://sdk.dyne.org:4443/view/web-sdk-builds/job/" joburl)} type]] + [:td {:class "status"} [:a {:href (str "https://sdk.dyne.org:4443/job/" joburl "/lastBuild/console")} + [:img {:src (str "https://sdk.dyne.org:4443/job/" joburl "/badge/icon")}]]] + [:td {:class "actions"} + [:div {:class "field is-grouped"} + (web/button "/view" "\uD83D\uDC41" (hf/hidden-field "jobid" jobid)) + (web/button "/start" "▶" (hf/hidden-field "jobid" jobid)) + (web/button "/remove" "\uD83D\uDDD1" (hf/hidden-field "jobid" jobid))]]] + ))]]]) + +(defn- box-add [] + [:div {:class "box column"} [:h1 {:class "title"} "Upload a Dockerfile to toast"] [:p " Choose the file in your computer and click 'Submit' to proceed to validation."] @@ -37,31 +79,12 @@ "Docker file..."]] [:span {:class "file-name"}]]] ;; [: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"}]]]]]) -(def dockerfile-edit-form - [:div {:class "container"} - [:h1 {:class "title"} "Edit your Dockerfile to toast"] - [:div {:class "form-group"} - [:form {:action "dockerfile" :method "post" - :class "form-shell" - :enctype "multipart/form-data"} - [:fieldset {:class "fieldset btn btn-default btn-file btn-lg"} - [:textarea {:name "editor" :id "editor" - :rows 30 :cols 72} "FROM: dyne/devuan:ascii"] - [:input {:class "btn btn-primary btn-lg" - :id "field-submit" :type "submit" - :name "submit" :value "submit"}]]]] - [:script "var editor = ace.edit(\"editor\"); - editor.setTheme(\"ace/theme/monokai\"); - editor.session.setMode(\"ace/mode/dockerfile\");"]]) - - (defn dockerfile-upload-post [request config account] (let [tempfile (get-in request [:params :file :tempfile]) @@ -71,52 +94,35 @@ (> (get-in params [:file :size]) 64000) ;; max upload size in bytes ;; TODO: put in config - (web/render-error params "File too big in upload (64KB limit).") + (f/fail "File too big in upload (64KB limit).") :else (let [file (io/copy tempfile (io/file "/tmp" filename)) path (str "/tmp/" filename)] (io/delete-file tempfile) (if (not (.exists (io/file path))) - (web/render-error - (log/spy :error - [:h1 (str "Uploaded file not found: " filename)])) + (f/fail + (str "Uploaded file not found: " filename)) ;; file is now in 'tmp' var - [:div {:class "container"} - [:h1 {:class "title"} "Job uploaded and added"] - [:p "Log messages:"] - (web/render-yaml (job/add path config account))]))))) + (f/if-let-ok? [newjob (job/add path config account)] + [:div {:class "container"} + [:h1 {:class "title"} "Job uploaded and added"] + [:p "Log messages:"] + (web/render-yaml newjob)] + (web/notify (f/message newjob) "is-error"))))))) -(defn list-jobs [account] +(defn dashboard + ([account] (dashboard {} {} account)) + ([request config account] (f/attempt-all [joblist (db/query @ring/jobs {:email (:email account)})] [:div {:class "container has-text-centered"} - [:h1 {:class "title"} (str "List all toaster jobs for " (:name account))] - [:div {:class "box"} - [:table {:class "table is-fullwidth is-hoverable"} - [:thead nil - [:tr nil [:th nil "Date"] [:th nil "Type"] [:th nil "Status"] [:th nil "Actions"]]] - [:tbody nil - (for [j joblist] - (let [type (:type j) ;; (-> j (str/split #"-") second) - tstamp (:timestamp j) ;; (-> j (str/split #"-") last) - jobid (:jobid j) - joburl (str/replace jobid #"@" "AT")] - [:tr nil - [:td {:class "date"} (-> tstamp Long/valueOf tc/from-long humanize/datetime)] - [:td {:class "job"} [:a {:href (str "https://sdk.dyne.org:4443/view/web-sdk-builds/job/" joburl)} type]] - [:td {:class "status"} [:a {:href (str "https://sdk.dyne.org:4443/job/" joburl "/lastBuild/console")} - [:img {:src (str "https://sdk.dyne.org:4443/job/" joburl "/badge/icon")}]]] - [:td {:class "actions"} - [:div {:class "field is-grouped"} - (web/button "/view" "\uD83D\uDC41" (hf/hidden-field "jobid" jobid)) - (web/button "/start" "▶" (hf/hidden-field "jobid" jobid)) - (web/button "/remove" "\uD83D\uDDD1" (hf/hidden-field "jobid" jobid))]]] - ))]]]] + [:div {:class "columns"} + (if (> 0 (count joblist)) (box-list account joblist)) + (box-add) ]] (f/when-failed [e] - (web/render-error - (str "Job list failure: " (f/message e)))))) - + (web/notify + (str "Job list failure: " (f/message e)) "is-error"))))) (defn remove-job [request config account] (f/attempt-all @@ -124,32 +130,26 @@ jobfound (db/query @ring/jobs {:jobid jobid}) r_rmjob (db/delete! @ring/jobs jobid) r_sync (job/trash jobid config)] - [:span - [:div {:class "notification is-primary has-text-centered"} - (str "Job removed: " jobid)] - (list-jobs account)] + (web/notify (str "Job removed: " jobid) "is-primary") (f/when-failed [e] - (web/render-error (str "Failure removing job: " (f/message e)))))) + (web/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/start jobid config)] - [:div {:class "container"} - [:h1 (str "Job started: " jobid)] - [:p [:a {:href (str "https://sdk.dyne.org:4443/view/web-sdk-builds/job/" - (str/replace jobid #"@" "AT"))}]]] + (web/notify (str "Job started: " jobid) "is-success") (f/when-failed [e] - (web/render-error (str "Failure starting job: " (f/message e)))))) + (web/notify (str "Failure starting job: " (f/message e)) "is-error")))) (defn view-job [request config account] (f/attempt-all [jobid (s/param request :jobid) jobfound (db/fetch @ring/jobs jobid) - dockerfile (log/spy (-> jobfound :dockerfile))] + dockerfile (-> jobfound :dockerfile)] [:div {:class "container"} [:h1 {:class "title"} (str "Viewing job: " jobid)] [:pre dockerfile]] (f/when-failed [e] - (web/render-error (str "Failure viewing job: " (f/message e)))))) + (web/notify (str "Failure viewing job: " (f/message e)) "is-error")))) diff --git a/clojure_frontend/src/toaster/webpage.clj b/clojure_frontend/src/toaster/webpage.clj index 7f13f6f..9496e27 100644 --- a/clojure_frontend/src/toaster/webpage.clj +++ b/clojure_frontend/src/toaster/webpage.clj @@ -38,6 +38,7 @@ (declare render-error) (declare render-error-page) (declare render-static) +(declare render-error) (defn q [req] "wrapper to retrieve parameters" diff --git a/clojure_frontend/toaster.yaml b/clojure_frontend/toaster.yaml index 9de71aa..ade3df0 100644 --- a/clojure_frontend/toaster.yaml +++ b/clojure_frontend/toaster.yaml @@ -1,11 +1,13 @@ webserver: anti-forgery: false ssl-redirect: false + mock-auth: false jenkins: host: "bridge.toaster" user: "jenkins" key: "../id_ed25519" + url: "https://sdk.dyne.org:4443" just-auth: email-server: "mail.dyne.org"