(ns auto-ap.ssr.admin.history (:require [auto-ap.datomic :refer [conn]] [auto-ap.logging :as alog] [auto-ap.ssr.ui :refer [base-page]] [auto-ap.ssr.utils :refer [html-response]] [auto-ap.time :as atime] [clj-time.coerce :as coerce] [clojure.string :as str] [datomic.api :as dc] [auto-ap.ssr.components :as com] [auto-ap.ssr-routes :as ssr-routes] [bidi.bidi :as bidi])) (defn tx-rows->changes [history] (->> history (group-by (fn [[a _ t]] [a t])) (map (fn [[[a t] changes]] (let [changes (-> (reduce (fn [acc [_ v _ added]] (if added (assoc acc :added v) (assoc acc :removed v))) {} changes))] [t a changes]))))) (defn format-value [v] (cond (inst? v) (-> v coerce/to-date-time atime/localize (atime/unparse atime/normal-date)) (nil? v) [:em "(none)"] (and (integer? v) (> v 1000000)) [:span (com/link {:href "#" :hx-get (str "/admin/history/" v) :hx-swap "innerHTML" :hx-push-url "true" :hx-select "#history-table" :hx-target "#history-table"} v) " [" (com/link {:href "#" :hx-get (str "/admin/history/inspect/" v) :hx-swap "innerHTML" :hx-target "#inspector" :hx-trigger "click"} "snapshot") "]"] :else (pr-str v))) (defn inspect [{{:keys [entity-id]} :params :as request}] (alog/info ::inspect :request request) (try (let [entity-id (Long/parseLong entity-id) data (dc/pull (dc/db conn) '[*] entity-id)] (html-response [:section.py-3.sm:py-5.max-w-lg (com/card {:class "p-2"} [: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 result-table [{:keys [entity-id]}] (try (let [history (->> (dc/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]] (dc/db conn) (dc/history (dc/db conn)) entity-id) tx-rows->changes (sort-by (comp :db/id first)) vec) best-guess-entity (or (->> history (group-by (comp namespace second)) (map (fn [[k v]] [k v])) (sort-by second) last first) "?")] (com/data-grid-card {:id "history-table" :title (format "History for %s: %d" (str/capitalize best-guess-entity) entity-id) :route :history-table :paginate? false :total (count history) :subtitle nil :action-buttons nil :rows (for [[tx a c] history] (com/data-grid-row {} (com/data-grid-cell {} [:div [:div (some-> (:db/txInstant tx) coerce/to-date-time atime/localize (atime/unparse atime/standard-time))] [:div.tag (:db/id tx)]]) (com/data-grid-cell {} (str (:audit/user tx))) (com/data-grid-cell {} (namespace a) ": " (name a)) (com/data-grid-cell {} (com/pill {:color :red} (format-value (:removed c)))) (com/data-grid-cell {} [:div.tag.is-primary.is-light [:span (format-value (:added c))]]))) :headers [(com/data-grid-header {} "Date") (com/data-grid-header {} "User") (com/data-grid-header {} "Field") (com/data-grid-header {} "From") (com/data-grid-header {} "To")]})) (catch NumberFormatException e (throw e)))) (defn search-box [{:keys [entity-id]}] [:div.mt-4 [:form.flex.gap-2 {"hx-target" "#history-table" "hx-get" (bidi/path-for ssr-routes/only-routes :admin-history) "hx-select" "#history-table" "hx-swap" "innerHTML" "hx-ext" "debug" "hx-push-url" "true"} (com/text-input {:name "entity-id" :placeholder "Entity Id" :value entity-id :style {:width "300px"}}) (com/button {:color :primary} "DIG")]]) (defn page [{:keys [matched-route route-params query-params] :as request}] (let [entity-id (or (some-> query-params (get "entity-id") Long/parseLong) (some-> route-params (get :entity-id) Long/parseLong))] (base-page request (com/page {:nav (com/admin-aside-nav) :client-selection (:client-selection (:session request)) :client (:client request) :identity (:identity request) :app-params {:hx-get (bidi/path-for ssr-routes/only-routes :admin-history) :hx-trigger "clientSelected from:body" :hx-select "#app-contents" :hx-swap "outerHTML swap:300ms"}} (com/breadcrumbs {} [:a {:href (bidi/path-for ssr-routes/only-routes :admin)} "Admin"] [:a {:href (bidi/path-for ssr-routes/only-routes :admin-history)} "History"]) (search-box {:entity-id entity-id}) [:div.flex.gap-4.flex-col.lg:flex-row (if entity-id (result-table {:entity-id entity-id}) [:div#history-table]) [:div#inspector] ]) "History")))