From fac827fab93037d810d4dced55eadc9119ccb7c5 Mon Sep 17 00:00:00 2001
From: Jerry Huang <34140255+qwef@users.noreply.github.com>
Date: Wed, 16 Aug 2023 19:31:05 -0700
Subject: [PATCH] Hide columns from SQL Query such that they will take effect
 in dashboard subscription email/slack (#32991)

* initial columns

* fix tests

* only filter when exists

* add test

* add whitespace

* add noah code

* fix test

* add column reordering

* fix issues with column ordering

* rename test

* Update src/metabase/pulse/render/body.clj

Co-authored-by: Noah Moss <32746338+noahmoss@users.noreply.github.com>

---------

Co-authored-by: Noah Moss <32746338+noahmoss@users.noreply.github.com>
---
 src/metabase/pulse/render.clj              | 13 +++-
 src/metabase/pulse/render/body.clj         | 90 +++++++++++-----------
 src/metabase/query_processor/streaming.clj |  2 +-
 test/metabase/pulse/render/table_test.clj  | 57 ++++++++++++++
 4 files changed, 114 insertions(+), 48 deletions(-)

diff --git a/src/metabase/pulse/render.clj b/src/metabase/pulse/render.clj
index 32001ce9e38..b2c3cfaeee2 100644
--- a/src/metabase/pulse/render.clj
+++ b/src/metabase/pulse/render.clj
@@ -8,6 +8,7 @@
    [metabase.pulse.render.image-bundle :as image-bundle]
    [metabase.pulse.render.png :as png]
    [metabase.pulse.render.style :as style]
+   [metabase.shared.models.visualization-settings :as mb.viz]
    [metabase.util.i18n :refer [trs tru]]
    [metabase.util.log :as log]
    [metabase.util.urls :as urls]
@@ -149,11 +150,17 @@
 - render/text : raw text suitable for substituting on clients when text is preferable. (Currently slack uses this for
   scalar results where text is preferable to an image of a div of a single result."
   [render-type timezone-id :- (s/maybe s/Str) card dashcard results]
-  (let [{title :content, title-attachments :attachments} (make-title-if-needed render-type card dashcard)
-        {description :content}                           (make-description-if-needed dashcard card)
+  (let [{title             :content
+         title-attachments :attachments} (make-title-if-needed render-type card dashcard)
+        {description :content}           (make-description-if-needed dashcard card)
+        results                          (update-in results
+                                                    [:data :viz-settings]
+                                                    (fn [viz-settings]
+                                                      (merge viz-settings (mb.viz/db->norm
+                                                                           (:visualization_settings dashcard)))))
         {pulse-body       :content
          body-attachments :attachments
-         text             :render/text}                  (render-pulse-card-body render-type timezone-id card dashcard results)]
+         text             :render/text}  (render-pulse-card-body render-type timezone-id card dashcard results)]
     (cond-> {:attachments (merge title-attachments body-attachments)
              :content [:p
                        ;; Provide a horizontal scrollbar for tables that overflow container width.
diff --git a/src/metabase/pulse/render/body.clj b/src/metabase/pulse/render/body.clj
index 6ba87d85889..cb0ec3186ec 100644
--- a/src/metabase/pulse/render/body.clj
+++ b/src/metabase/pulse/render/body.clj
@@ -12,6 +12,7 @@
    [metabase.pulse.render.style :as style]
    [metabase.pulse.render.table :as table]
    [metabase.pulse.util :as pu]
+   [metabase.query-processor.streaming :as qp.streaming]
    [metabase.shared.models.visualization-settings :as mb.viz]
    [metabase.types :as types]
    [metabase.util :as u]
@@ -213,15 +214,27 @@
   {:arglists '([chart-type render-type timezone-id card dashcard data])}
   (fn [chart-type _ _ _ _ _] chart-type))
 
+(defn- filter-rows [data table-columns rows]
+  (let [column-order      (qp.streaming/export-column-order (:cols data) table-columns)
+        keep-filtered-idx (fn [col] (map #(nth col %) column-order))
+        filtered-rows     (map #(update % :row keep-filtered-idx) rows)]
+    filtered-rows))
+
 (s/defmethod render :table :- common/RenderedPulseCard
-  [_ render-type timezone-id :- (s/maybe s/Str) card dashcard {:keys [rows viz-settings] :as data}]
-  (let [viz-settings (merge viz-settings (:visualization_settings dashcard))
-        table-body   [:div
-                      (table/render-table
-                       (color/make-color-selector data viz-settings)
-                       (mapv :name (:cols data))
-                       (prep-for-html-rendering timezone-id card data))
-                      (render-truncation-warning rows-limit (count rows))]]
+  [_ render-type timezone-id :- (s/maybe s/Str) card _dashcard {:keys [rows viz-settings] :as data}]
+  (let [table-columns          (::mb.viz/table-columns viz-settings)
+        filter-columns?        (some? table-columns)
+        filtered-columns-names (if filter-columns?
+                                 (mapv ::mb.viz/table-column-name (filter ::mb.viz/table-column-enabled table-columns))
+                                 (mapv :name (:cols data)))
+        filtered-rows          (cond->> (prep-for-html-rendering timezone-id card data)
+                                 filter-columns? (filter-rows data table-columns))
+        table-body             [:div
+                                (table/render-table
+                                 (color/make-color-selector data viz-settings)
+                                 filtered-columns-names
+                                 filtered-rows)
+                                (render-truncation-warning rows-limit (count rows))]]
     {:attachments
      nil
 
@@ -460,12 +473,10 @@
         rows))
 
 (s/defmethod render :categorical/donut :- common/RenderedPulseCard
-  [_ render-type timezone-id :- (s/maybe s/Str) card dashcard {:keys [rows cols viz-settings] :as data}]
-  (let [rows                        (replace-nils rows)
-        viz-settings                (merge viz-settings (:visualization_settings dashcard))
-        [x-axis-rowfn y-axis-rowfn] (common/graphing-column-row-fns card data)
+  [_ render-type timezone-id :- (s/maybe s/Str) card _dashcard {:keys [rows cols viz-settings] :as data}]
+  (let [[x-axis-rowfn y-axis-rowfn] (common/graphing-column-row-fns card data)
         rows                        (map (juxt (comp str x-axis-rowfn) y-axis-rowfn)
-                                         (common/row-preprocess x-axis-rowfn y-axis-rowfn rows))
+                                         (common/row-preprocess x-axis-rowfn y-axis-rowfn (replace-nils rows)))
         slice-threshold             (or (get viz-settings :pie.slice_threshold)
                                         2.5)
         {:keys [rows percentages]}  (donut-info slice-threshold rows)
@@ -500,9 +511,8 @@
              rows))]}))
 
 (s/defmethod render :progress :- common/RenderedPulseCard
-  [_ render-type _timezone-id _card dashcard {:keys [cols rows viz-settings] :as _data}]
-  (let [viz-settings (merge viz-settings (:visualization_settings dashcard))
-        value        (ffirst rows)
+  [_ render-type _timezone-id _card _dashcard {:keys [cols rows viz-settings] :as _data}]
+  (let [value        (ffirst rows)
         goal         (:progress.goal viz-settings)
         color        (:progress.color viz-settings)
         settings     (assoc
@@ -640,8 +650,7 @@
 (defn- render-multiple-scalars
   "When multiple scalar cards are combined, they render as a bar chart"
   [render-type card dashcard {:keys [viz-settings] :as data}]
-  (let [viz-settings (merge viz-settings (:visualization_settings dashcard))
-        multi-res    (pu/execute-multi-card card dashcard)
+  (let [multi-res    (pu/execute-multi-card card dashcard)
         cards        (cons card (map :card multi-res))
         multi-data   (cons data (map #(get-in % [:result :data]) multi-res))
         x-rows       (map :name cards) ;; Bar labels
@@ -779,10 +788,8 @@
 
 (defn- render-multiple-lab-chart
   "When multiple non-scalar cards are combined, render them as a line, area, or bar chart"
-  [render-type card dashcard {:keys [viz-settings]
-                              :as   data}]
-  (let [viz-settings      (merge viz-settings (:visualization_settings dashcard))
-        multi-res         (pu/execute-multi-card card dashcard)
+  [render-type card dashcard {:keys [viz-settings] :as data}]
+  (let [multi-res         (pu/execute-multi-card card dashcard)
         ;; multi-res gets the other results from the set of multis.
         ;; we shove cards and data here all together below for uniformity's sake
         viz-settings      (set-default-stacked viz-settings card)
@@ -799,9 +806,8 @@
   "Generate an image-bundle for a Line Area Bar chart (LAB)
 
   Use the combo charts for every chart-type in line area bar because we get multiple chart series for cheaper this way."
-  [chart-type render-type _timezone-id card dashcard {:keys [cols rows viz-settings] :as data}]
+  [chart-type render-type _timezone-id card {:keys [cols rows viz-settings] :as data}]
   (let [rows            (replace-nils rows)
-        viz-settings    (merge viz-settings (:visualization_settings dashcard))
         x-axis-rowfn    (or (ui-logic/mult-x-axis-rowfn card data) #(vector (first %)))
         y-axis-rowfn    (or (ui-logic/mult-y-axis-rowfn card data) #(vector (second %)))
         x-rows          (filter some? (map x-axis-rowfn rows))
@@ -831,20 +837,20 @@
    render-type card dashcard data))
 
 (s/defmethod render :line :- common/RenderedPulseCard
-  [_ render-type timezone-id card dashcard data]
-  (attach-image-bundle (lab-image-bundle :line render-type timezone-id card dashcard data)))
+  [_ render-type timezone-id card _dashcard data]
+  (attach-image-bundle (lab-image-bundle :line render-type timezone-id card data)))
 
 (s/defmethod render :area :- common/RenderedPulseCard
-  [_ render-type timezone-id card dashcard data]
-  (attach-image-bundle (lab-image-bundle :area render-type timezone-id card dashcard data)))
+  [_ render-type timezone-id card _dashcard data]
+  (attach-image-bundle (lab-image-bundle :area render-type timezone-id card data)))
 
 (s/defmethod render :bar :- common/RenderedPulseCard
-  [_chart-type render-type timezone-id :- (s/maybe s/Str) card dashcard data]
-  (attach-image-bundle (lab-image-bundle :bar render-type timezone-id card dashcard data)))
+  [_chart-type render-type timezone-id :- (s/maybe s/Str) card _dashcard data]
+  (attach-image-bundle (lab-image-bundle :bar render-type timezone-id card data)))
 
 (s/defmethod render :combo :- common/RenderedPulseCard
-  [_chart-type render-type timezone-id :- (s/maybe s/Str) card dashcard data]
-  (attach-image-bundle (lab-image-bundle :combo render-type timezone-id card dashcard data)))
+  [_chart-type render-type timezone-id :- (s/maybe s/Str) card _dashcard data]
+  (attach-image-bundle (lab-image-bundle :combo render-type timezone-id card data)))
 
 (s/defmethod render :gauge :- common/RenderedPulseCard
   [_chart-type render-type _timezone-id :- (s/maybe s/Str) card _dashcard data]
@@ -878,9 +884,8 @@
              :src   (:image-src image-bundle)}]]}))
 
 (s/defmethod render :scalar :- common/RenderedPulseCard
-  [_chart-type _render-type timezone-id _card dashcard {:keys [cols rows viz-settings]}]
-  (let [viz-settings (merge viz-settings (:visualization_settings dashcard))
-        value        (format-cell timezone-id (ffirst rows) (first cols) viz-settings)]
+  [_chart-type _render-type timezone-id _card _dashcard {:keys [cols rows viz-settings]}]
+  (let [value (format-cell timezone-id (ffirst rows) (first cols) viz-settings)]
     {:attachments
      nil
 
@@ -890,7 +895,7 @@
      :render/text (str value)}))
 
 (s/defmethod render :smartscalar :- common/RenderedPulseCard
-  [_chart-type _render-type timezone-id _card dashcard {:keys [cols insights viz-settings]}]
+  [_chart-type _render-type timezone-id _card _dashcard {:keys [cols insights viz-settings]}]
   (letfn [(col-of-type [t c] (or (isa? (:effective_type c) t)
                                  ;; computed and agg columns don't have an effective type
                                  (isa? (:base_type c) t)))
@@ -899,8 +904,7 @@
                               (format-percentage arg)
                               " - "))
           (format-unit [unit] (str/replace (name unit) "-" " "))]
-    (let [viz-settings           (merge viz-settings (:visualization_settings dashcard))
-          [_time-col metric-col] (if (col-of-type :type/Temporal (first cols)) cols (reverse cols))
+    (let [[_time-col metric-col] (if (col-of-type :type/Temporal (first cols)) cols (reverse cols))
 
           {:keys [last-value previous-value unit last-change] :as _insight}
           (where (comp #{(:name metric-col)} :col) insights)]
@@ -938,9 +942,8 @@
                            "\n" (trs "Nothing to compare to."))}))))
 
 (s/defmethod render :waterfall :- common/RenderedPulseCard
-  [_ render-type _timezone-id card dashcard {:keys [rows cols viz-settings] :as data}]
-  (let [viz-settings   (merge viz-settings (:visualization_settings dashcard))
-        [x-axis-rowfn
+  [_ render-type _timezone-id card _dashcard {:keys [rows cols viz-settings] :as data}]
+  (let [[x-axis-rowfn
          y-axis-rowfn] (common/graphing-column-row-fns card data)
         [x-col y-col]  ((juxt x-axis-rowfn y-axis-rowfn) cols)
         rows           (map (juxt x-axis-rowfn y-axis-rowfn)
@@ -975,9 +978,8 @@
              :src   (:image-src image-bundle)}]]}))
 
 (s/defmethod render :funnel :- common/RenderedPulseCard
-  [_ render-type _timezone-id card dashcard {:keys [rows cols viz-settings] :as data}]
-  (let [viz-settings   (merge viz-settings (:visualization_settings dashcard))
-        [x-axis-rowfn
+  [_ render-type _timezone-id card _dashcard {:keys [rows cols viz-settings] :as data}]
+  (let [[x-axis-rowfn
          y-axis-rowfn] (common/graphing-column-row-fns card data)
         rows           (map (juxt x-axis-rowfn y-axis-rowfn)
                             (common/row-preprocess x-axis-rowfn y-axis-rowfn rows))
diff --git a/src/metabase/query_processor/streaming.clj b/src/metabase/query_processor/streaming.clj
index 74d2d97a3bf..373e70c1854 100644
--- a/src/metabase/query_processor/streaming.clj
+++ b/src/metabase/query_processor/streaming.clj
@@ -50,7 +50,7 @@
                   table-columns)
       table-columns)))
 
-(defn- export-column-order
+(defn export-column-order
   "For each entry in `table-columns` that is enabled, finds the index of the corresponding
   entry in `cols` by name or id. If a col has been remapped, uses the index of the new column.
 
diff --git a/test/metabase/pulse/render/table_test.clj b/test/metabase/pulse/render/table_test.clj
index c3d9af6683c..81e71dec307 100644
--- a/test/metabase/pulse/render/table_test.clj
+++ b/test/metabase/pulse/render/table_test.clj
@@ -1,6 +1,7 @@
 (ns metabase.pulse.render.table-test
   (:require
    [clojure.test :refer :all]
+   [metabase.pulse.render :as render]
    [metabase.pulse.render.color :as color]
    [metabase.pulse.render.common :as common]
    [metabase.pulse.render.table :as table]
@@ -155,3 +156,59 @@
                  render.tu/remove-attrs
                  (render.tu/nodes-with-tag :td)
                  (->> (map second)))))))))
+
+(defn- render-table [dashcard results]
+  (render/render-pulse-card :attachment "America/Los_Angeles" render.tu/test-card dashcard results))
+
+(deftest render-table-columns-test
+  (testing "Disabling a column has the same effect as not having the column at all."
+    (is (=
+         (render-table
+          {:visualization_settings {:table.columns
+                                    [{:name "b" :enabled true}]}}
+          {:data {:cols [{:name         "b",
+                          :display_name "b",
+                          :base_type    :type/BigInteger
+                          :semantic_type nil}]
+                  :rows [[2] [4]]}})
+         (render-table
+          {:visualization_settings {:table.columns
+                                    [{:name "a" :enabled false}
+                                     {:name "b" :enabled true}]}}
+          {:data {:cols [{:name         "a",
+                          :display_name "a",
+                          :base_type    :type/BigInteger
+                          :semantic_type nil}
+                         {:name         "b",
+                          :display_name "b",
+                          :base_type    :type/BigInteger
+                          :semantic_type nil}]
+                  :rows [[1 2] [3 4]]}}))))
+  (testing "Column order in table.columns is respected."
+    (is (=
+         (render-table
+          {:visualization_settings {:table.columns
+                                    [{:name "b" :enabled true}
+                                     {:name "a" :enabled true}]}}
+          {:data {:cols [{:name         "a",
+                          :display_name "a",
+                          :base_type    :type/BigInteger
+                          :semantic_type nil}
+                         {:name         "b",
+                          :display_name "b",
+                          :base_type    :type/BigInteger
+                          :semantic_type nil}]
+                  :rows [[1 2] [3 4]]}})
+         (render-table
+          {:visualization_settings {:table.columns
+                                    [{:name "b" :enabled true}
+                                     {:name "a" :enabled true}]}}
+          {:data {:cols [{:name         "b",
+                          :display_name "b",
+                          :base_type    :type/BigInteger
+                          :semantic_type nil}
+                         {:name         "a",
+                          :display_name "a",
+                          :base_type    :type/BigInteger
+                          :semantic_type nil}]
+                  :rows [[2 1] [4 3]]}})))))
-- 
GitLab