Fork me on GitHub
#fulcro
<
2022-06-26
>
roklenarcic10:06:52

looking at fulcro-rad-datomic, how would you solve paging results and how would you solve filtering results based on what the current user can “see”? I am looking to write something similar for another EQL query supporting provider

tony.kay15:06:19

I’ve written several versions of this…there is the start of this in the lib. https://github.com/fulcrologic/fulcro-rad-datomic/blob/develop/src/main/com/fulcrologic/rad/database_adapters/indexed_access.clj and then you just need a modified state machine. There is one that does a “more” style of loading at https://github.com/fulcrologic/fulcro-rad/blob/develop/src/main/com/fulcrologic/rad/state_machines/incrementally_loaded_report.cljc

tony.kay15:06:54

but if you’re confident you can get a count of the docs, it’s not hard (I have one locally) to show the full count and pagination control even though you only load a single page.

tony.kay15:06:39

Main thing is you need to be able to use datoms or index-pull to get index-sorted results so that you can get fast and accurate loads of pages.

tony.kay15:06:49

In terms of filtering I’ve found an adaptive approach works best: You can use tuples to get results that are sorted and grouped in a particular way, but then you need to do lazy take-while kinds of things (if the filter has frequent hits), I adapt over to using q once the filters have enough constraints and just load all those results at once. So, the state machine has to be aware of that possibility.

tony.kay15:06:03

So your resolver ends up looking a bit complicated “If it’s this kind of sort/filter do it this way, else this, else that”. And return a page if you can, but return them all once it gets down to a relatively tight constraint (because once you’re using q you’d have to sort/filter them, and at that point might as well push all that to the client).

tony.kay16:06:39

Here’s the non-released one that can do one page at a time:

(ns app.lib.rad.machines.indexed-report-machine
  (:require
    [com.fulcrologic.fulcro.components :as comp]
    [com.fulcrologic.fulcro.raw.components :as rc]
    [com.fulcrologic.fulcro.ui-state-machines :as uism :refer [defstatemachine]]
    [com.fulcrologic.rad.attributes :as attr]
    [com.fulcrologic.rad.options-util :refer [?!]]
    [com.fulcrologic.rad.report :as report]
    [com.fulcrologic.rad.report-options :as ro]
    [com.fulcrologic.rad.type-support.date-time :as dt]
    [taoensso.timbre :as log]))

(defn report-cache-expired?
  "Helper for state machines. Returns true if the report data looks like it has expired according to configured
   caching parameters."
  [{::uism/keys [state-map] :as uism-env}]
  (let [Report              (uism/actor-class uism-env :actor/report)
        {::report/keys [load-cache-seconds
                        load-cache-expired?]} (comp/component-options Report)
        now-ms              (inst-ms (dt/now))
        last-load-time      (uism/retrieve uism-env :last-load-time)
        cache-expiration-ms (* 1000 (or load-cache-seconds 0))
        cache-looks-stale?  (or
                              (nil? last-load-time)
                              (< last-load-time (- now-ms cache-expiration-ms)))]
    cache-looks-stale?))

(def Q (memoize
         (fn [nm query]
           (rc/nc query {:componentName nm}))))

(defn- load-page
  "Load a page of items for the report."
  [{::uism/keys [state-map] :as env} page-number]
  (let [Report     (uism/actor-class env :actor/report)
        all-params (report/current-control-parameters env)
        page-size  (or (rc/component-options Report ro/page-size) 20)
        offset     (* page-size (dec page-number))
        source     (rc/component-options Report ro/source-attribute)
        BodyItem   (rc/component-options Report ro/BodyItem)
        Query      (Q ::Page [:next-offset :total {:results (rc/get-query BodyItem state-map)}])]
    (-> env
      (uism/load source Query
        {:params             (merge all-params {:offset offset
                                                :limit  page-size})
         ::uism/target-alias :loaded-data
         ::uism/ok-event     :event/loaded
         ::uism/ok-data      {:page-number page-number}
         ::uism/error-event  :event/failed})
      (uism/activate :state/loading))))

(defn load-more!
  "Ask a server-paginated report to load more data at the end of the results. "
  [this] (uism/trigger! this (comp/get-ident this) :event/load-more))

(defn clear-report-data [env]
  (uism/assoc-aliased env :current-rows [] :pages {} :loaded-data {} :busy? false :page-count 0 :current-page 1))

(defn goto-page [env page-number]
  (let [pages       (uism/alias-value env :pages)
        page-count  (uism/alias-value env :page-count)
        ;; we have to load page 1 first in order to ensure we get the total number of items
        page-number (if (pos? page-count)
                      (max 1 (min page-count page-number))
                      1)
        page        (get pages page-number)
        loaded?     (seq page)]
    (if loaded?
      (-> env
        (uism/assoc-aliased :current-page page-number)
        (uism/assoc-aliased :current-rows page))
      (load-page env page-number))))

(defn populate-page-map [env]
  (let [{:keys [page-number]} (::uism/event-data env)
        Report      (uism/actor-class env :actor/report)
        {:keys [loaded-total loaded-page]} (log/spy :debug (uism/aliased-data env))
        page-size   (or (rc/component-options Report ro/page-size) 20)
        page-count  (when loaded-total (inc (int (/ loaded-total page-size))))
        page-number (max 1 (min (log/spy :debug page-count) (log/spy :debug page-number)))
        {::report/keys [row-pk report-loaded]} (comp/component-options Report)
        table-name  (::attr/qualified-key row-pk)
        pages-path  (uism/resolve-alias env :pages)]
    ;; It is possible (when using more than one filter) for the results to exceed the page size. This happens because
    ;; the multi-filter always returns all the results. So, we have to push that into multiple pages when it happens
    (if (and (= 1 page-number) (> (count loaded-page) page-size))
      (let [pages    (partition-all page-size loaded-page)
            page-map (into {} (map (fn [i pgs] [i (vec pgs)]) (range 1 1000) pages))]
        (uism/assoc-aliased env :pages page-map :loaded-total loaded-total :page-count page-count
          :current-page 1 :current-rows (get page-map 1)))
      (as-> env $
        (uism/apply-action $ assoc-in (conj pages-path page-number) (log/spy :debug loaded-page))
        (cond-> $
          (and (= 1 page-number) (pos? loaded-total)) (uism/assoc-aliased
                                                        :loaded-total (log/spy :debug loaded-total)
                                                        :page-count page-count))
        (uism/assoc-aliased $ :current-page page-number :current-rows loaded-page)))))

(defn page-loaded [{::uism/keys [event-data state-map] :as env}]
  (let [{:keys [page-number]} event-data
        Report       (uism/actor-class env :actor/report)
        loaded-total (uism/alias-value env :loaded-total)
        page-size    (or (rc/component-options Report ro/page-size) 20)
        page-count   (inc (int (/ loaded-total page-size)))
        page-number  (max 1 (min (log/spy :debug page-count) (log/spy :debug page-number)))
        {::report/keys [report-loaded]} (comp/component-options Report)]
    (-> env
      (populate-page-map)
      (report/page-number-changed)
      (uism/activate :state/gathering-parameters)
      (cond->
        (and (= 1 page-number) report-loaded) report-loaded))))

(defn reload! [env]
  (-> env
    (clear-report-data)
    (goto-page 1)))

(defstatemachine indexed-report-machine
  {::uism/actors #{:actor/report}

   ::uism/aliases
   {:parameters   [:actor/report :ui/parameters]
    :current-page [:actor/report :ui/parameters ::report/current-page]
    :sort-params  [:actor/report :ui/parameters ::sort]     ; unused, but here to prevent warning from inherited code
    :loaded-data  [:actor/report :ui/loaded-data]
    :next-offset  [:actor/report :ui/loaded-data :next-offset]
    :loaded-total [:actor/report :ui/loaded-data :total]
    :loaded-page  [:actor/report :ui/loaded-data :results]
    :pages        [:actor/report :ui/cache ::report/pages]
    :current-rows [:actor/report :ui/current-rows]
    :page-count   [:actor/report :ui/page-count]
    :busy?        [:actor/report :ui/busy?]}

   ::uism/states
   {:initial
    {::uism/handler (fn [env]
                      (let [{::uism/keys [fulcro-app event-data]} env]
                        (-> env
                          (uism/store :route-params (:route-params event-data))
                          (clear-report-data)
                          (report/initialize-parameters)
                          (goto-page 1))))}

    :state/loading
    {::uism/events
     (merge report/global-events
       {:event/loaded {::uism/handler page-loaded}
        :event/failed {::uism/handler (fn [env] (log/error "Report failed to load.")
                                        (uism/activate env :state/gathering-parameters))}})}

    :state/gathering-parameters
    {::uism/events
     (merge report/global-events
       {:event/goto-page         {::uism/handler (fn [{::uism/keys [event-data] :as env}]
                                                   (let [{:keys [page]} event-data]
                                                     (goto-page env page)))}
        :event/next-page         {::uism/handler (fn [env]
                                                   (let [page (uism/alias-value env :current-page)]
                                                     (goto-page env (inc page))))}
        :event/prior-page        {::uism/handler (fn [env]
                                                   (let [page (uism/alias-value env :current-page)]
                                                     (goto-page env (dec page))))}

        :event/filter            {::uism/handler reload!}

        :event/set-ui-parameters {::uism/handler report/initialize-parameters}

        :event/run               {::uism/handler reload!}

        :event/resume            {::uism/handler (fn [env]
                                                   (when (report-cache-expired? env)
                                                     (let [env (report/initialize-parameters env)]
                                                       (reload! env))))}})}}})

tony.kay16:06:02

That one expects you to return :total and :results.

tony.kay00:06:40

@U0522TWDA feel free to refine it and submit it as an MR 😄

thinking-face 1
tony.kay00:06:57

It could use some touch-ups to make it lib-ready

tony.kay18:06:51

merge request

tony.kay18:06:59

same as PR basically 😛

👍 1