Revamps all of the IOL's routing, so that the new history page can share with the rest.
This commit is contained in:
@@ -1,94 +1,16 @@
|
||||
(ns auto-ap.ssr.admin
|
||||
(:require
|
||||
[auto-ap.datomic :refer [conn]]
|
||||
[auto-ap.graphql.utils :refer [assert-admin]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]]
|
||||
[auto-ap.ssr.ui :refer [base-page html-response]]
|
||||
[auto-ap.time :as atime]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clojure.string :as str]
|
||||
[clojure.tools.logging :as log]
|
||||
[compojure.core :refer [context defroutes GET POST routes]]
|
||||
[datomic.api :as d]
|
||||
[hiccup2.core :as hiccup]))
|
||||
|
||||
(defn html-page [hiccup]
|
||||
{:status 200
|
||||
:headers {"Content-Type" "text/html"}
|
||||
:body (str
|
||||
"<!DOCTYPE html>"
|
||||
(hiccup/html
|
||||
{}
|
||||
hiccup))})
|
||||
|
||||
(defn base-page [contents]
|
||||
(html-page
|
||||
[:html.has-navbar-fixed-top
|
||||
[:head
|
||||
[:meta {:charset "utf-8"}]
|
||||
[:meta {:http-equiv "X-UA-Compatible", :content "IE=edge"}]
|
||||
[:meta {:name "viewport", :content "width=device-width, initial-scale=1"}]
|
||||
[:title "Integreat"]
|
||||
[:link {:rel "stylesheet", :href "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css", :integrity "sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=", :crossorigin "anonymous"}]
|
||||
[:link {:href "/css/font.min.css", :rel "stylesheet"}]
|
||||
[:link {:rel "stylesheet", :href "/css/bulma.min.css"}]
|
||||
[:link {:rel "stylesheet", :href "/css/bulma-calendar.min.css"}]
|
||||
[:link {:rel "stylesheet", :href "/css/bulma-badge.min.css"}]
|
||||
[:link {:rel "stylesheet", :href "/css/react-datepicker.min.inc.css"}]
|
||||
[:link {:rel "stylesheet", :href "/css/animate.css"}]
|
||||
[:link {:rel "stylesheet", :href "/finance-font/style.css"}]
|
||||
[:link {:rel "stylesheet", :href "/css/main.css"}]
|
||||
[:link {:rel "stylesheet", :href "https://unpkg.com/placeholder-loading/dist/css/placeholder-loading.min.css"}]
|
||||
[:script {:src "https://unpkg.com/hyperscript.org@0.9.7"}]
|
||||
[:script {:src "https://unpkg.com/htmx.org@1.8.4"
|
||||
:integrity "sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV"
|
||||
:crossorigin= "anonymous"}]
|
||||
[:script {:type "text/javascript", :src "https://cdn.yodlee.com/fastlink/v4/initialize.js", :async "async" }]]
|
||||
|
||||
|
||||
[:body
|
||||
[:div {:id "app"}
|
||||
[:div
|
||||
[:nav {:class "navbar has-shadow is-fixed-top is-grey"}
|
||||
|
||||
[:div {:class "container"}
|
||||
[:div {:class "navbar-brand"}
|
||||
[:a {:class "navbar-item", :href "../"}
|
||||
[:img {:src "/img/logo.png"}]]]
|
||||
[:div.navbar-menu {:id "navMenu"}
|
||||
[:div.navbar-start
|
||||
[:a.navbar-item {:href "/"}
|
||||
"Home" ]
|
||||
[:a.navbar-item {:href "/invoices/"}
|
||||
"Invoices" ]
|
||||
[:a.navbar-item {:href "/payments/"}
|
||||
"Payments" ]
|
||||
[:a.navbar-item {:href "/pos/sales-orders/"}
|
||||
"POS" ]
|
||||
[:a.navbar-item {:href "/transactions/"}
|
||||
"Transactions" ]
|
||||
|
||||
[:a.navbar-item {:href "/ledger/"}
|
||||
"Ledger" ]]]]]
|
||||
[:div {:class "columns has-shadow", :id "mail-app", :style "margin-bottom: 0px; height: calc(100vh - 46px);"}
|
||||
[:aside {:class "column aside menu is-2 "}
|
||||
[:div {:class "main left-nav"}
|
||||
[:div]]]
|
||||
[:div {:class "column messages hero ", :id "message-feed", :style "overflow: auto;"}
|
||||
[:div {:class "inbox-messages"}
|
||||
contents]]]
|
||||
[:div]
|
||||
[:div {:id "dz-hidden"}]]]]]))
|
||||
|
||||
(defn html-response [hiccup]
|
||||
{:status 200
|
||||
:headers {"Content-Type" "text/html"}
|
||||
:body (str
|
||||
(hiccup/html
|
||||
{}
|
||||
hiccup))})
|
||||
|
||||
|
||||
(defn inline-add-deletes [history]
|
||||
(defn tx-rows->changes [history]
|
||||
(->> history
|
||||
(group-by (fn [[a _ t]]
|
||||
[a t]))
|
||||
@@ -102,6 +24,9 @@
|
||||
changes))]
|
||||
[t a changes])))))
|
||||
|
||||
(def error-script
|
||||
(hiccup/raw "on htmx:responseError from me set event.detail.target's innerHTML to event.detail.xhr.responseText end"))
|
||||
|
||||
(defn format-value [v]
|
||||
(cond (inst? v)
|
||||
(-> v
|
||||
@@ -121,29 +46,27 @@
|
||||
:hx-target "#history-table"}
|
||||
v]
|
||||
" [" [:a
|
||||
{:hx-get (str "/admin/history/inspect/" v)
|
||||
:hx-swap "innerHTML"
|
||||
:hx-target "#inspector"
|
||||
:hx-trigger "click"}
|
||||
"snapshot"] "]"
|
||||
]
|
||||
|
||||
{:hx-get (str "/admin/history/inspect/" v)
|
||||
:hx-swap "innerHTML"
|
||||
:hx-target "#inspector"
|
||||
:hx-trigger "click"
|
||||
"_" error-script}
|
||||
"snapshot"] "]"]
|
||||
|
||||
|
||||
:else
|
||||
(pr-str v)))
|
||||
|
||||
(comment "_" )
|
||||
|
||||
(defn page-template [& {:keys [table entity-id]}]
|
||||
[:div
|
||||
[:div.columns
|
||||
[:div.column.is-4
|
||||
[:form.hello {"hx-target" "#history-table"
|
||||
"hx-post" "/admin/history/search"
|
||||
"hx-swap" "innerHTML"
|
||||
"_" (hiccup/raw "on htmx:beforeRequest toggle @disabled on me then toggle .is-loading on <#dig/> end
|
||||
[:form {"hx-target" "#history-table"
|
||||
"hx-post" "/admin/history/search"
|
||||
"hx-swap" "innerHTML"
|
||||
"_" (hiccup/raw "on htmx:beforeRequest toggle @disabled on me then toggle .is-loading on <#dig/> end
|
||||
on htmx:afterRequest toggle @disabled on me then toggle .is-loading on <#dig /> end")
|
||||
}
|
||||
}
|
||||
[:div.field.is-grouped
|
||||
[:p.control {}
|
||||
[:input.input {:type "text" :name "entity-id" :placeholder "Entity id" :value entity-id}]]
|
||||
@@ -153,27 +76,63 @@
|
||||
[:div#history-table
|
||||
table]])
|
||||
|
||||
(defn history-search [{:keys [form-params params] identity :identity :as request}]
|
||||
(assert-admin identity)
|
||||
(log/info ::request
|
||||
request)
|
||||
(defn table [entity-id best-guess-entity history]
|
||||
[:div [:h1.title "History for "
|
||||
(str/capitalize best-guess-entity)
|
||||
" "
|
||||
entity-id]
|
||||
[:div.columns
|
||||
[:div.column.is-9
|
||||
[:table.table.compact.grid {:style "width: 100%"}
|
||||
[:thead
|
||||
[:tr
|
||||
[:td {:style "width: 14em"} "Date"]
|
||||
[:td {:style "width: 14em"} "User"]
|
||||
[:td {:style "width: 18em"} "Field"]
|
||||
[:td "From"]
|
||||
[:td "To"]]]
|
||||
[:tbody
|
||||
(for [[tx a c] history]
|
||||
[:tr
|
||||
[:td [:div [:div (some-> (:db/txInstant tx)
|
||||
coerce/to-date-time
|
||||
atime/localize
|
||||
(atime/unparse atime/standard-time))
|
||||
]
|
||||
[:div.tag (:db/id tx)]]]
|
||||
[:td (str (:audit/user tx))]
|
||||
[:td (namespace a) ": " (name a)]
|
||||
|
||||
[:td
|
||||
[:div.tag.is-danger.is-light
|
||||
[:span
|
||||
(format-value (:removed c))]]]
|
||||
[:td
|
||||
[:div.tag.is-primary.is-light
|
||||
[:span
|
||||
(format-value (:added c))]]]])]
|
||||
]]
|
||||
[:div.column.is-3
|
||||
[:div#inspector]]]])
|
||||
|
||||
(defn history-search [{:keys [form-params params] :as request}]
|
||||
(try
|
||||
(let [entity-id (Long/parseLong (or (some-> (:entity-id form-params) not-empty)
|
||||
(let [entity-id (Long/parseLong (or (some-> (:entity-id form-params) not-empty)
|
||||
(:entity-id params)
|
||||
(get params "entity-id")
|
||||
(get form-params "entity-id")))
|
||||
history (->>
|
||||
(d/q '[:find ?a2 ?v (pull ?tx [:db/txInstant :audit/user :db/id]) ?ad
|
||||
:in $ $$ ?i
|
||||
:where
|
||||
[$$ ?i ?a ?v ?tx ?ad]
|
||||
[$ ?a :db/ident ?a2]]
|
||||
(d/db conn)
|
||||
(d/history (d/db conn))
|
||||
entity-id )
|
||||
inline-add-deletes
|
||||
(sort-by (comp :db/id first))
|
||||
vec)
|
||||
history (->>
|
||||
(d/q '[:find ?a2 ?v (pull ?tx [:db/txInstant :audit/user :db/id]) ?ad
|
||||
:in $ $$ ?i
|
||||
:where
|
||||
[$$ ?i ?a ?v ?tx ?ad]
|
||||
[$ ?a :db/ident ?a2]]
|
||||
(d/db conn)
|
||||
(d/history (d/db conn))
|
||||
entity-id )
|
||||
tx-rows->changes
|
||||
(sort-by (comp :db/id first))
|
||||
vec)
|
||||
best-guess-entity (or (->> history
|
||||
(group-by
|
||||
(comp
|
||||
@@ -185,99 +144,55 @@
|
||||
(sort-by second)
|
||||
last
|
||||
first)
|
||||
"?")
|
||||
table [:div [:h1.title "History for "
|
||||
(str/capitalize best-guess-entity)
|
||||
" "
|
||||
entity-id]
|
||||
[:div.columns
|
||||
[:div.column.is-9
|
||||
[:table.table.compact.grid {:style "width: 100%"}
|
||||
[:thead
|
||||
[:tr
|
||||
[:td {:style "width: 14em"} "Date"]
|
||||
[:td {:style "width: 14em"} "User"]
|
||||
[:td {:style "width: 18em"} "Field"]
|
||||
[:td "From"]
|
||||
[:td "To"]]]
|
||||
[:tbody
|
||||
(for [[tx a c] history]
|
||||
[:tr
|
||||
[:td [:div [:div (some-> (:db/txInstant tx)
|
||||
coerce/to-date-time
|
||||
atime/localize
|
||||
(atime/unparse atime/standard-time))
|
||||
]
|
||||
[:div.tag (:db/id tx)]]]
|
||||
[:td (str (:audit/user tx))]
|
||||
[:td (namespace a) ": " (name a)]
|
||||
"?")]
|
||||
|
||||
[:td
|
||||
[:div.tag.is-danger.is-light
|
||||
[:span
|
||||
(format-value (:removed c))]]]
|
||||
[:td
|
||||
[:div.tag.is-primary.is-light
|
||||
[:span
|
||||
(format-value (:added c))]]]])]
|
||||
]]
|
||||
[:div.column.is-3
|
||||
[:div#inspector.box {:style {:position "sticky"
|
||||
:display "inline-block"
|
||||
:vertical-align "top"
|
||||
:overflow-y "auto"
|
||||
:max-height "100vh"
|
||||
:top "0px"
|
||||
:bottom "0px"}}]]]]]
|
||||
|
||||
(alog/info ::trace
|
||||
:bge best-guess-entity
|
||||
:headers (:headers request))
|
||||
(if (get (:headers request) "hx-request")
|
||||
(html-response
|
||||
table)
|
||||
(base-page (page-template :table table
|
||||
:entity-id entity-id))))
|
||||
(catch NumberFormatException e
|
||||
(table entity-id best-guess-entity history))
|
||||
(base-page (page-template :table (table entity-id best-guess-entity history)
|
||||
:entity-id entity-id)
|
||||
(admin-side-bar :admin-history))))
|
||||
(catch NumberFormatException _
|
||||
(html-response
|
||||
(str [:div.notification.is-danger.is-light
|
||||
"Cannot parse the entity-id " (or (:entity-id form-params)
|
||||
(:entity-id params))
|
||||
[:div.notification.is-danger.is-light
|
||||
"Cannot parse the entity-id " (or (:entity-id form-params)
|
||||
(:entity-id params))
|
||||
|
||||
". It should be a number."])))))
|
||||
". It should be a number."]))))
|
||||
|
||||
(defn inspect [{{:keys [entity-id]} :params identity :identity :as request}]
|
||||
(defn inspect [{{:keys [entity-id]} :params :as request}]
|
||||
(alog/info ::inspect
|
||||
:request request)
|
||||
(assert-admin identity)
|
||||
(try
|
||||
(let [entity-id (Long/parseLong entity-id)
|
||||
data (d/pull (d/db conn)
|
||||
'[*]
|
||||
entity-id
|
||||
) ]
|
||||
entity-id)]
|
||||
|
||||
(html-response
|
||||
[:div {:style {:display "inline-block"}}
|
||||
[:h1.title "Snapshot of "
|
||||
entity-id]
|
||||
[:ul
|
||||
(for [[k v] data]
|
||||
[:li [:strong k] ":" v]
|
||||
)]]))
|
||||
(catch NumberFormatException e
|
||||
[:div.box {:style {:position "sticky"
|
||||
:display "inline-block"
|
||||
:vertical-align "top"
|
||||
:overflow-y "auto"
|
||||
:max-height "100vh"
|
||||
:top "0px"
|
||||
:bottom "0px"}}
|
||||
[:div {:style {:display "inline-block"}}
|
||||
[:h1.title "Snapshot of "
|
||||
entity-id]
|
||||
[:ul
|
||||
(for [[k v] data]
|
||||
[:li [:strong k] ":" (format-value v)]
|
||||
)]]]))
|
||||
(catch NumberFormatException _
|
||||
(html-response
|
||||
[:div.notification.is-danger.is-light
|
||||
"Cannot parse the entity-id " entity-id ". It should be a number."]))))
|
||||
|
||||
(defn history [{:keys [identity] :as request}]
|
||||
(base-page (page-template )))
|
||||
(defn history [{:keys [matched-route]}]
|
||||
(base-page (page-template )
|
||||
(admin-side-bar matched-route)))
|
||||
|
||||
|
||||
|
||||
|
||||
(defroutes admin-routes
|
||||
(routes
|
||||
(context "/admin" []
|
||||
(GET "/history" [] history)
|
||||
(GET "/history/" [] history)
|
||||
(POST "/history/search" [] history-search)
|
||||
(GET "/history/:entity-id" [entity-id] history-search)
|
||||
(GET "/history/inspect/:entity-id" [entity-id] inspect))))
|
||||
|
||||
14
src/clj/auto_ap/ssr/core.clj
Normal file
14
src/clj/auto_ap/ssr/core.clj
Normal file
@@ -0,0 +1,14 @@
|
||||
(ns auto-ap.ssr.core
|
||||
(:require
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated wrap-secure]]
|
||||
[auto-ap.ssr.admin :as admin]
|
||||
[auto-ap.ssr-routes]))
|
||||
|
||||
|
||||
|
||||
;; from auto-ap.ssr-routes, because they're shared
|
||||
(def key->handler {:admin-history (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin admin/history)))
|
||||
:admin-history-search (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin admin/history-search)))
|
||||
:admin-history-inspect (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin admin/inspect)))})
|
||||
|
||||
108
src/clj/auto_ap/ssr/ui.clj
Normal file
108
src/clj/auto_ap/ssr/ui.clj
Normal file
@@ -0,0 +1,108 @@
|
||||
(ns auto-ap.ssr.ui
|
||||
(:require
|
||||
[auto-ap.logging :as alog]
|
||||
[config.core :refer [env]]
|
||||
[hiccup2.core :as hiccup]))
|
||||
|
||||
(defn html-page [hiccup]
|
||||
{:status 200
|
||||
:headers {"Content-Type" "text/html"}
|
||||
:body (str
|
||||
"<!DOCTYPE html>"
|
||||
(hiccup/html
|
||||
{}
|
||||
hiccup))})
|
||||
|
||||
(defn base-page [contents side-bar-contents]
|
||||
(html-page
|
||||
[:html.has-navbar-fixed-top
|
||||
[:head
|
||||
[:meta {:charset "utf-8"}]
|
||||
[:meta {:http-equiv "X-UA-Compatible", :content "IE=edge"}]
|
||||
[:meta {:name "viewport", :content "width=device-width, initial-scale=1"}]
|
||||
[:title "Integreat"]
|
||||
[:link {:rel "stylesheet", :href "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css", :integrity "sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=", :crossorigin "anonymous"}]
|
||||
[:link {:href "/css/font.min.css", :rel "stylesheet"}]
|
||||
[:link {:rel "stylesheet", :href "/css/bulma.min.css"}]
|
||||
[:link {:rel "stylesheet", :href "/css/bulma-calendar.min.css"}]
|
||||
[:link {:rel "stylesheet", :href "/css/bulma-badge.min.css"}]
|
||||
[:link {:rel "stylesheet", :href "/css/react-datepicker.min.inc.css"}]
|
||||
[:link {:rel "stylesheet", :href "/css/animate.css"}]
|
||||
[:link {:rel "stylesheet", :href "/finance-font/style.css"}]
|
||||
[:link {:rel "stylesheet", :href "/css/main.css"}]
|
||||
[:link {:rel "stylesheet", :href "https://unpkg.com/placeholder-loading/dist/css/placeholder-loading.min.css"}]
|
||||
[:script {:src "https://unpkg.com/hyperscript.org@0.9.7"}]
|
||||
[:script {:src "https://unpkg.com/htmx.org@1.8.4"
|
||||
:integrity "sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV"
|
||||
:crossorigin= "anonymous"}]
|
||||
[:script {:type "text/javascript", :src "https://cdn.yodlee.com/fastlink/v4/initialize.js", :async "async" }]]
|
||||
|
||||
|
||||
[:body
|
||||
[:div {:id "app"}
|
||||
[:div
|
||||
[:nav {:class "navbar has-shadow is-fixed-top is-grey"}
|
||||
|
||||
[:div {:class "container"}
|
||||
[:div {:class "navbar-brand"}
|
||||
[:a {:class "navbar-item", :href "../"}
|
||||
[:img {:src "/img/logo.png"}]]]
|
||||
[:div.navbar-menu {:id "navMenu"}
|
||||
[:div.navbar-start
|
||||
[:a.navbar-item {:href "/"}
|
||||
"Home" ]
|
||||
[:a.navbar-item {:href "/invoices/"}
|
||||
"Invoices" ]
|
||||
[:a.navbar-item {:href "/payments/"}
|
||||
"Payments" ]
|
||||
[:a.navbar-item {:href "/pos/sales-orders/"}
|
||||
"POS" ]
|
||||
[:a.navbar-item {:href "/transactions/"}
|
||||
"Transactions" ]
|
||||
|
||||
[:a.navbar-item {:href "/ledger/"}
|
||||
"Ledger" ]]]]]
|
||||
[:div {:class "columns has-shadow", :id "mail-app", :style "margin-bottom: 0px; height: calc(100vh - 46px);"}
|
||||
[:aside {:class "column aside menu is-2 "}
|
||||
[:div {:class "main left-nav"}
|
||||
side-bar-contents]]
|
||||
[:div {:class "column messages hero ", :id "message-feed", :style "overflow: auto;"}
|
||||
[:div {:class "inbox-messages"}
|
||||
contents]]]
|
||||
[:div]
|
||||
[:div {:id "dz-hidden"}]]]]]))
|
||||
|
||||
(defn html-response [hiccup & {:keys [status] :or {status 200}}]
|
||||
{:status status
|
||||
:headers {"Content-Type" "text/html"}
|
||||
:body (str
|
||||
(hiccup/html
|
||||
{}
|
||||
hiccup))})
|
||||
|
||||
(defn wrap-error-response [handler]
|
||||
(fn [request]
|
||||
(try
|
||||
(handler request)
|
||||
(catch Exception e
|
||||
(if-let [v (or (:validation-error (ex-data e))
|
||||
(:validation-error (ex-data (.getCause e))))]
|
||||
|
||||
(do
|
||||
(alog/warn ::request-validation-error
|
||||
:exception e)
|
||||
(html-response
|
||||
[:div.notification.is-warning.is-light
|
||||
v]
|
||||
:status 400))
|
||||
(do
|
||||
(alog/error ::request-error
|
||||
:exception e)
|
||||
(when (= "dev" (:dd-env env))
|
||||
(println e))
|
||||
(html-response
|
||||
[:div.notification.is-danger.is-light
|
||||
"Server error occured."
|
||||
(ex-message e)]
|
||||
:status 500)))))))
|
||||
|
||||
Reference in New Issue
Block a user