(ns metabase.revisions.impl.dashboard (:require [clojure.data :refer [diff]] [clojure.set :as set] [clojure.string :as str] [metabase.models.dashboard :as dashboard] [metabase.models.dashboard-card :as dashboard-card] [metabase.models.dashboard-tab :as dashboard-tab] [metabase.revisions.models.revision :as revision] [metabase.util :as u] [metabase.util.i18n :refer [deferred-tru deferred-trun]] [toucan2.core :as t2] [toucan2.realize :as t2.realize])) | |
(def ^:private excluded-columns-for-dashboard-revision
#{:id :created_at :updated_at :creator_id :points_of_interest :caveats :show_in_getting_started :entity_id
;; not sure what position is for, from the column remark:
;; > The position this Dashboard should appear in the Dashboards list,
;; lower-numbered positions appearing before higher numbered ones.
;; TODO: querying on stats we don't have any dashboard that has a position, maybe we could just drop it?
:public_uuid :made_public_by_id
:position :initially_published_at :view_count
:last_viewed_at}) | |
(def ^:private excluded-columns-for-dashcard-revision [:entity_id :created_at :updated_at :collection_authority_level]) | |
(def ^:private excluded-columns-for-dashboard-tab-revision [:created_at :updated_at :entity_id]) | |
(defmethod revision/serialize-instance :model/Dashboard
[_model _id dashboard]
(let [dashcards (or (:dashcards dashboard)
(:dashcards (t2/hydrate dashboard :dashcards)))
dashcards (when (seq dashcards)
(if (contains? (first dashcards) :series)
dashcards
(t2/hydrate dashcards :series)))
tabs (or (:tabs dashboard)
(:tabs (t2/hydrate dashboard :tabs)))]
(-> (apply dissoc dashboard excluded-columns-for-dashboard-revision)
(assoc :cards (vec (for [dashboard-card dashcards]
(-> (apply dissoc dashboard-card excluded-columns-for-dashcard-revision)
(assoc :series (mapv :id (:series dashboard-card)))))))
(assoc :tabs (map #(apply dissoc % excluded-columns-for-dashboard-tab-revision) tabs))))) | |
(defn- revert-dashcards
[dashboard-id serialized-cards]
(let [current-cards (t2/select-fn-vec #(apply dissoc (t2.realize/realize %) excluded-columns-for-dashcard-revision)
:model/DashboardCard
:dashboard_id dashboard-id)
id->current-card (zipmap (map :id current-cards) current-cards)
{:keys [to-create to-update to-delete]} (u/row-diff current-cards serialized-cards)]
(when (seq to-delete)
(dashboard-card/delete-dashboard-cards! (map :id to-delete)))
(when (seq to-create)
(dashboard-card/create-dashboard-cards! (map #(assoc % :dashboard_id dashboard-id) to-create)))
(when (seq to-update)
(doseq [update-card to-update]
(dashboard-card/update-dashboard-card! update-card (id->current-card (:id update-card))))))) | |
Given a list of dashcards, remove any dashcard that references cards that are archived, do not exist, or now belong to other dashboards . | (defn- remove-invalid-dashcards
[dashboard-id dashcards]
(let [card-ids (set (keep :card_id dashcards))
active-card-ids (when-let [card-ids (seq card-ids)]
(t2/select-pks-set :model/Card
{:where [:and
[:in :id card-ids]
;; skip when archived
[:= :archived false]
;; belong to this dashboard, or are not Dashboard Questions
[:or
[:= :dashboard_id dashboard-id]
[:= :dashboard_id nil]]]}))
inactive-card-ids (set/difference card-ids active-card-ids)]
(remove #(contains? inactive-card-ids (:card_id %)) dashcards))) |
(defmethod revision/revert-to-revision! :model/Dashboard
[model dashboard-id user-id serialized-dashboard]
;; Update the dashboard description / name / permissions
((get-method revision/revert-to-revision! :default) model dashboard-id user-id (dissoc serialized-dashboard :cards :tabs))
;; Now update the tabs and cards as needed
(let [serialized-dashcards (:cards serialized-dashboard)
current-tabs (t2/select-fn-vec #(dissoc (t2.realize/realize %) :created_at :updated_at :entity_id :dashboard_id)
:model/DashboardTab :dashboard_id dashboard-id)
{:keys [old->new-tab-id]} (dashboard-tab/do-update-tabs! dashboard-id current-tabs (:tabs serialized-dashboard))
_ (dashboard/archive-or-unarchive-internal-dashboard-questions! dashboard-id serialized-dashcards)
serialized-dashcards (cond->> serialized-dashcards
true
(remove-invalid-dashcards dashboard-id)
;; in case reverting result in new tabs being created,
;; we need to remap the tab-id
(seq old->new-tab-id)
(map (fn [card]
(if-let [new-tab-id (get old->new-tab-id (:dashboard_tab_id card))]
(assoc card :dashboard_tab_id new-tab-id)
card))))]
(revert-dashcards dashboard-id serialized-dashcards))
serialized-dashboard) | |
(defmethod revision/diff-strings :model/Dashboard
[_model prev-dashboard dashboard]
(let [[removals changes] (diff prev-dashboard dashboard)
check-series-change (fn [idx card-changes]
(when (and (:series card-changes)
(get-in prev-dashboard [:cards idx :card_id]))
(let [num-seriesā (count (get-in prev-dashboard [:cards idx :series]))
num-seriesā (count (get-in dashboard [:cards idx :series]))]
(cond
(< num-seriesā num-seriesā)
(deferred-tru "added some series to card {0}" (get-in prev-dashboard [:cards idx :card_id]))
(> num-seriesā num-seriesā)
(deferred-tru "removed some series from card {0}" (get-in prev-dashboard [:cards idx :card_id]))
:else
(deferred-tru "modified the series on card {0}" (get-in prev-dashboard [:cards idx :card_id]))))))]
(-> [(when-let [default-description (u/build-sentence ((get-method revision/diff-strings :default) :model/Dashboard prev-dashboard dashboard))]
(cond-> default-description
(str/ends-with? default-description ".") (subs 0 (dec (count default-description)))))
(when (:cache_ttl changes)
(cond
(nil? (:cache_ttl prev-dashboard)) (deferred-tru "added a cache ttl")
(nil? (:cache_ttl dashboard)) (deferred-tru "removed the cache ttl")
:else (deferred-tru "changed the cache ttl from \"{0}\" to \"{1}\""
(:cache_ttl prev-dashboard) (:cache_ttl dashboard))))
(when (or (:cards changes) (:cards removals))
(let [prev-card-ids (set (map :id (:cards prev-dashboard)))
num-prev-cards (count prev-card-ids)
new-card-ids (set (map :id (:cards dashboard)))
num-new-cards (count new-card-ids)
num-cards-diff (abs (- num-prev-cards num-new-cards))
keys-changes (set (flatten (concat (map keys (:cards changes))
(map keys (:cards removals)))))]
(cond
(and
(set/subset? prev-card-ids new-card-ids)
(< num-prev-cards num-new-cards)) (deferred-trun "added a card" "added {0} cards" num-cards-diff)
(and
(set/subset? new-card-ids prev-card-ids)
(> num-prev-cards num-new-cards)) (deferred-trun "removed a card" "removed {0} cards" num-cards-diff)
(set/subset? keys-changes #{:row :col :size_x :size_y}) (deferred-tru "rearranged the cards")
:else (deferred-tru "modified the cards"))))
(when (or (:tabs changes) (:tabs removals))
(let [prev-tabs (:tabs prev-dashboard)
new-tabs (:tabs dashboard)
prev-tab-ids (set (map :id prev-tabs))
num-prev-tabs (count prev-tab-ids)
new-tab-ids (set (map :id new-tabs))
num-new-tabs (count new-tab-ids)
num-tabs-diff (abs (- num-prev-tabs num-new-tabs))]
(cond
(and
(set/subset? prev-tab-ids new-tab-ids)
(< num-prev-tabs num-new-tabs)) (deferred-trun "added a tab" "added {0} tabs" num-tabs-diff)
(and
(set/subset? new-tab-ids prev-tab-ids)
(> num-prev-tabs num-new-tabs)) (deferred-trun "removed a tab" "removed {0} tabs" num-tabs-diff)
(= (set (map #(dissoc % :position) prev-tabs))
(set (map #(dissoc % :position) new-tabs))) (deferred-tru "rearranged the tabs")
:else (deferred-tru "modified the tabs"))))
(let [f (comp boolean :auto_apply_filters)]
(when (not= (f prev-dashboard) (f dashboard))
(deferred-tru "set auto apply filters to {0}" (str (f dashboard)))))]
(concat (map-indexed check-series-change (:cards changes)))
(->> (filter identity))))) | |