diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 41aa178b07b910a21f484290b9251dae62dc8ab8..94d8d7549acbbd922ee30be7d0197fc37c142a8a 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -299,9 +299,7 @@ metabase.bootstrap #{metabase.bootstrap} metabase.cmd #{} ; there are no namespaces here since you shouldn't be using this in any other module. metabase.channel #{metabase.channel.core - metabase.channel.render.core - metabase.channel.render.preview - metabase.channel.shared} + metabase.channel.render.core} metabase.config #{metabase.config} metabase.core #{metabase.core.initialization-status} ; TODO -- only namespace used outside of EE, this probably belongs in `metabase.server` anyway since that's the only place it's used. metabase.db #{metabase.db @@ -330,7 +328,7 @@ metabase.models :any ; TODO -- scream, 62 namespaces used elsewhere, but to be fair a lot of these don't *need* to be required. metabase.moderation #{metabase.moderation} metabase.notification #{metabase.notification.core - metabase.notification.payload.execute} + metabase.notification.payload.core} metabase.permissions #{metabase.permissions.util} ; TODO -- this is currently the only namespace in this module. Give it a real API namespace? metabase.plugins #{metabase.plugins metabase.plugins.classloader} ; TODO -- not 100% sure the classloader belongs here @@ -550,6 +548,7 @@ metabase.api.util api.util metabase.async.streaming-response.thread-pool thread-pool metabase.channel.core channel + metabase.channel.render.core channel.render metabase.cmd.copy.h2 copy.h2 metabase.config config metabase.config.file config.file diff --git a/dev/src/dev/render_png.clj b/dev/src/dev/render_png.clj index 342b69536e2293990dd77bfd5830097b54499f3b..f5517b7652f0cc70d6e05638e34826e79e1e087f 100644 --- a/dev/src/dev/render_png.clj +++ b/dev/src/dev/render_png.clj @@ -6,11 +6,10 @@ [clojure.java.io :as io] [dev.util :as dev.u] [hiccup.core :as hiccup] - [metabase.channel.render.core :as render] + [metabase.channel.render.core :as channel.render] [metabase.channel.render.image-bundle :as img] [metabase.channel.render.png :as png] [metabase.channel.render.style :as style] - [metabase.channel.shared :as channel.shared] [metabase.email.result-attachment :as email.result-attachment] [metabase.models :refer [Card]] [metabase.models.card :as card] @@ -42,10 +41,10 @@ (cond-> dataset_query (= card-type :model) (assoc-in [:info :metadata/model-metadata] result_metadata))) - png-bytes (render/render-pulse-card-to-png (channel.shared/defaulted-timezone card) - card - query-results - 1000)] + png-bytes (channel.render/render-pulse-card-to-png (channel.render/defaulted-timezone card) + card + query-results + 1000)] (open-png-bytes png-bytes))) (defn render-pulse-card @@ -53,8 +52,8 @@ [card-id] (let [{:keys [dataset_query] :as card} (t2/select-one card/Card :id card-id) query-results (qp/process-query dataset_query)] - (render/render-pulse-card - :inline (channel.shared/defaulted-timezone card) + (channel.render/render-pulse-card + :inline (channel.render/defaulted-timezone card) card nil query-results))) @@ -81,7 +80,7 @@ dashboard-results (notification.payload.execute/execute-dashboard (:id dashboard) (:id user) nil)] (doseq [{:keys [card dashcard result] :as dashboard-result} dashboard-results] (let [render (if card - (render/render-pulse-card :inline (channel.shared/defaulted-timezone card) card dashcard result) + (channel.render/render-pulse-card :inline (channel.render/defaulted-timezone card) card dashcard result) {:content [:div {:style (style/style {:font-family "Lato" :font-size "0.875em" :font-weight "400" @@ -133,7 +132,7 @@ [:td {:style (style/style (merge table-style-map {:max-width "400px"}))} content])] (if card - (let [base-render (render/render-pulse-card :inline (channel.shared/defaulted-timezone card) card dashcard result) + (let [base-render (channel.render/render-pulse-card :inline (channel.render/defaulted-timezone card) card dashcard result) html-src (-> base-render :content) img-src (-> base-render (png/render-html-to-png 1200) diff --git a/src/metabase/api/embed/common.clj b/src/metabase/api/embed/common.clj index 421b51179182e551b6239228208f1b8519bc15aa..80f2a1ec4c9d0eb7d2ea3cde74b33ccad95a6694 100644 --- a/src/metabase/api/embed/common.clj +++ b/src/metabase/api/embed/common.clj @@ -16,7 +16,7 @@ [metabase.models.card :as card] [metabase.models.params :as params] [metabase.models.setting :as setting :refer [defsetting]] - [metabase.notification.payload.execute :as notification.execute] + [metabase.notification.payload.core :as notification.payload] [metabase.query-processor.card :as qp.card] [metabase.query-processor.middleware.constraints :as qp.constraints] [metabase.util :as u] @@ -249,7 +249,7 @@ (map (fn [card] (if (-> card :visualization_settings :virtual_card) - (notification.execute/process-virtual-dashcard card params-with-values) + (notification.payload/process-virtual-dashcard card params-with-values) card)) dashcards)))) diff --git a/src/metabase/api/pulse.clj b/src/metabase/api/pulse.clj index b1cd8da7dc86baefc15e9ad77c077fde70339014..0247d8b665b6bdfc478ebf431571eda0a9508604 100644 --- a/src/metabase/api/pulse.clj +++ b/src/metabase/api/pulse.clj @@ -1,6 +1,7 @@ (ns metabase.api.pulse "`/api/pulse` endpoints. These are all authenticated. For unauthenticated `/api/pulse/unsubscribe` endpoints, see [[metabase.api.pulse.unsubscribe]]." + #_{:clj-kondo/ignore [:deprecated-namespace]} (:require [clojure.set :refer [difference]] [compojure.core :refer [GET POST PUT]] @@ -10,8 +11,6 @@ [metabase.api.common :as api] [metabase.api.common.validation :as validation] [metabase.channel.render.core :as channel.render] - [metabase.channel.render.preview :as channel.preview] - [metabase.channel.shared :as channel.shared] [metabase.config :as config] [metabase.email :as email] [metabase.events :as events] @@ -277,7 +276,7 @@ :body (html5 [:html [:body {:style "margin: 0;"} - (channel.render/render-pulse-card-for-display (channel.shared/defaulted-timezone card) + (channel.render/render-pulse-card-for-display (channel.render/defaulted-timezone card) card result {:channel.render/include-title? true, :channel.render/include-buttons? true})]])})) @@ -285,7 +284,7 @@ (api/defendpoint GET "/preview_dashboard/:id" "Get HTML rendering of a Dashboard with `id`. - This endpoint relies on a custom middleware defined in `metabase.channel.render.preview/style-tag-nonce-middleware` to + This endpoint relies on a custom middleware defined in `metabase.channel.render.core/style-tag-nonce-middleware` to allow the style tag to render properly, given our Content Security Policy setup. This middleware is attached to these routes at the bottom of this namespace using `metabase.api.common/define-routes`." [id] @@ -293,7 +292,7 @@ (api/read-check :model/Dashboard id) {:status 200 :headers {"Content-Type" "text/html"} - :body (channel.preview/style-tag-from-inline-styles + :body (channel.render/style-tag-from-inline-styles (html5 [:head [:meta {:charset "utf-8"}] @@ -301,7 +300,7 @@ :rel "stylesheet" :href "https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"}]] [:body [:h2 (format "Backend Artifacts Preview for Dashboard %s" id)] - (channel.preview/render-dashboard-to-html id)]))}) + (channel.render/render-dashboard-to-html id)]))}) (api/defendpoint GET "/preview_card_info/:id" "Get JSON object containing HTML rendering of a Card with `id` and other information." @@ -311,7 +310,7 @@ result (pulse-card-query-results card) data (:data result) card-type (channel.render/detect-pulse-chart-type card nil data) - card-html (html (channel.render/render-pulse-card-for-display (channel.shared/defaulted-timezone card) + card-html (html (channel.render/render-pulse-card-for-display (channel.render/defaulted-timezone card) card result {:channel.render/include-title? true}))] @@ -331,7 +330,7 @@ {id ms/PositiveInt} (let [card (api/read-check Card id) result (pulse-card-query-results card) - ba (channel.render/render-pulse-card-to-png (channel.shared/defaulted-timezone card) + ba (channel.render/render-pulse-card-to-png (channel.render/defaulted-timezone card) card result preview-card-width @@ -372,6 +371,6 @@ api/generic-204-no-content) (def ^:private style-nonce-middleware - (partial channel.preview/style-tag-nonce-middleware "/api/pulse/preview_dashboard")) + (partial channel.render/style-tag-nonce-middleware "/api/pulse/preview_dashboard")) (api/define-routes style-nonce-middleware) diff --git a/src/metabase/channel/impl/email.clj b/src/metabase/channel/impl/email.clj index 25535fff9295490854f4fa649f910fcf4fbaa067..e98f5c5946732179e1113eeb828f0dd3164c7dc9 100644 --- a/src/metabase/channel/impl/email.clj +++ b/src/metabase/channel/impl/email.clj @@ -7,8 +7,6 @@ [metabase.channel.core :as channel] [metabase.channel.params :as channel.params] [metabase.channel.render.core :as channel.render] - [metabase.channel.render.js-svg :as render.js-svg] - [metabase.channel.shared :as channel.shared] [metabase.email :as email] [metabase.email.messages :as messages] [metabase.email.result-attachment :as email.result-attachment] @@ -116,10 +114,10 @@ (defn- icon-bundle "Bundle an icon. - The available icons are defined in [[js-svg/icon-paths]]." + The available icons are defined in [[render.js.svg/icon-paths]]." [icon-name] (let [color (channel.render/primary-color) - png-bytes (render.js-svg/icon icon-name color)] + png-bytes (channel.render/icon icon-name color)] (-> (channel.render/make-image-bundle :attachment png-bytes) (channel.render/image-bundle->attachment)))) @@ -180,7 +178,7 @@ (let [{:keys [card_part alert card]} payload - timezone (channel.shared/defaulted-timezone card) + timezone (channel.render/defaulted-timezone card) rendered-card (render-part timezone card_part {:channel.render/include-title? true}) icon-attachment (apply make-message-attachment (icon-bundle :bell)) attachments (concat [icon-attachment] @@ -256,7 +254,7 @@ dashboard_subscription parameters dashboard]} payload - timezone (some->> dashboard_parts (some :card) channel.shared/defaulted-timezone) + timezone (some->> dashboard_parts (some :card) channel.render/defaulted-timezone) rendered-cards (mapv #(render-part timezone % {:channel.render/include-title? true}) dashboard_parts) icon-attachment (apply make-message-attachment (icon-bundle :dashboard)) attachments (concat diff --git a/src/metabase/channel/impl/http.clj b/src/metabase/channel/impl/http.clj index e796ae8d7f2a87f470928f48231b00fe8772d43e..6c436f81c22747684d5fbb31e68444ae83173e6c 100644 --- a/src/metabase/channel/impl/http.clj +++ b/src/metabase/channel/impl/http.clj @@ -89,7 +89,7 @@ :question_url (urls/card-url (:id card)) :visualization (let [{:keys [card dashcard result]} card_part] (channel.render/render-pulse-card-to-base64 - (channel.shared/defaulted-timezone card) card dashcard result image-width)) + (channel.render/defaulted-timezone card) card dashcard result image-width)) :raw_data (qp-result->raw-data (:result card_part))} :sent_at (t/offset-date-time)}] [{:body request-body}])) diff --git a/src/metabase/channel/impl/slack.clj b/src/metabase/channel/impl/slack.clj index d49f3a8c46d8b3fd7e6873247de9b2a391f018e5..2453c4952736332a89cc31f1ab8692116778e675 100644 --- a/src/metabase/channel/impl/slack.clj +++ b/src/metabase/channel/impl/slack.clj @@ -3,7 +3,6 @@ [clojure.string :as str] [metabase.channel.core :as channel] [metabase.channel.render.core :as channel.render] - [metabase.channel.shared :as channel.shared] ;; TODO: integrations.slack should be migrated to channel.slack [metabase.integrations.slack :as slack] [metabase.models.params.shared :as shared.params] @@ -41,7 +40,7 @@ {card-id :id card-name :name :as card} card] {:title (or (-> dashcard :visualization_settings :card.title) card-name) - :rendered-info (channel.render/render-pulse-card :inline (channel.shared/defaulted-timezone card) card dashcard result) + :rendered-info (channel.render/render-pulse-card :inline (channel.render/defaulted-timezone card) card dashcard result) :title_link (urls/card-url card-id) :attachment-name "image.png" :channel-id channel-id diff --git a/src/metabase/channel/render/body.clj b/src/metabase/channel/render/body.clj index bfa3480e806f28741011e0c41217f359724bdb58..8806a7a711041fb2e0734905bdf7425bf36aa995 100644 --- a/src/metabase/channel/render/body.clj +++ b/src/metabase/channel/render/body.clj @@ -3,9 +3,9 @@ [clojure.string :as str] [hiccup.core :refer [h]] [medley.core :as m] - [metabase.channel.render.color :as color] [metabase.channel.render.image-bundle :as image-bundle] - [metabase.channel.render.js-svg :as js-svg] + [metabase.channel.render.js.color :as js.color] + [metabase.channel.render.js.svg :as js.svg] [metabase.channel.render.style :as style] [metabase.channel.render.table :as table] [metabase.formatter :as formatter] @@ -18,8 +18,10 @@ [metabase.util :as u] [metabase.util.i18n :refer [deferred-trs trs tru]] [metabase.util.malli :as mu] + [metabase.util.malli.schema :as ms] [toucan2.core :as t2]) (:import + (java.net URL) (java.text DecimalFormat DecimalFormatSymbols))) (set! *warn-on-reflection* true) @@ -205,8 +207,15 @@ ;;; | render | ;;; +----------------------------------------------------------------------------------------------------------------+ +(def RenderedPartCard + "Schema used for functions that operate on pulse card contents and their attachments" + [:map + [:attachments [:maybe [:map-of :string (ms/InstanceOfClass URL)]]] + [:content [:sequential :any]] + [:render/text {:optional true} [:maybe :string]]]) + (defmulti render - "Render a Pulse as `chart-type` (e.g. `:bar`, `:scalar`, etc.) and `render-type` (either `:inline` or `:attachment`)." + "Render a Part as `chart-type` (e.g. `:bar`, `:scalar`, etc.) and `render-type` (either `:inline` or `:attachment`)." {:arglists '([chart-type render-type timezone-id card dashcard data])} (fn [chart-type _render-type _timezone-id _card _dashcard _data] chart-type)) @@ -222,7 +231,7 @@ [ordered-cols ordered-rows]) [(:cols data) (:rows data)])) -(mu/defmethod render :table :- formatter/RenderedPulseCard +(mu/defmethod render :table :- RenderedPartCard [_chart-type _render-type timezone-id :- [:maybe :string] @@ -235,7 +244,7 @@ (assoc :cols ordered-cols)) table-body [:div (table/render-table - (color/make-color-selector unordered-data viz-settings) + (js.color/make-color-selector unordered-data viz-settings) {:cols-for-color-lookup (mapv :name ordered-cols) :col-names (common/column-titles ordered-cols (::mb.viz/column-settings viz-settings) format-rows?)} (prep-for-html-rendering timezone-id card data)) @@ -330,7 +339,7 @@ (DecimalFormat. base))] (.format fmt value)))) -(mu/defmethod render :progress :- formatter/RenderedPulseCard +(mu/defmethod render :progress :- RenderedPartCard [_chart-type render-type _timezone-id @@ -347,7 +356,7 @@ settings (assoc settings :format (:x settings)) image-bundle (image-bundle/make-image-bundle render-type - (js-svg/progress value goal settings))] + (js.svg/progress value goal settings))] {:attachments (when image-bundle (image-bundle/image-bundle->attachment image-bundle)) @@ -373,11 +382,11 @@ (assoc card-with-data :timeline_events timeline-events) card-with-data)) -(mu/defmethod render :gauge :- formatter/RenderedPulseCard +(mu/defmethod render :gauge :- RenderedPartCard [_chart-type render-type _timezone-id :- [:maybe :string] card _dashcard data] (let [image-bundle (image-bundle/make-image-bundle render-type - (js-svg/gauge card data))] + (js.svg/gauge card data))] {:attachments (when image-bundle (image-bundle/image-bundle->attachment image-bundle)) @@ -387,14 +396,14 @@ [:img {:style (style/style {:display :block :width :100%}) :src (:image-src image-bundle)}]]})) -(mu/defmethod render :row :- formatter/RenderedPulseCard +(mu/defmethod render :row :- RenderedPartCard [_chart-type render-type _timezone-id card _dashcard {:keys [rows cols] :as _data}] (let [viz-settings (get card :visualization_settings) data {:rows rows :cols cols} image-bundle (image-bundle/make-image-bundle render-type - (js-svg/row-chart viz-settings data))] + (js.svg/row-chart viz-settings data))] {:attachments (when image-bundle (image-bundle/image-bundle->attachment image-bundle)) @@ -411,7 +420,7 @@ (when (= col-name (:name col)) [idx col]))))) -(mu/defmethod render :scalar :- formatter/RenderedPulseCard +(mu/defmethod render :scalar :- RenderedPartCard [_chart-type _render-type timezone-id _card _dashcard {:keys [cols rows viz-settings]}] (let [field-name (:scalar.field viz-settings) [row-idx col] (or (when field-name @@ -441,7 +450,7 @@ ;; As of 2024-03-21, isomorphic chart types include: line, area, bar (LAB), and trend charts ;; Because this effort began with LAB charts, this method is written to handle multi-series dashcards. ;; Trend charts were added more recently and will not have multi-series. -(mu/defmethod render :javascript_visualization :- formatter/RenderedPulseCard +(mu/defmethod render :javascript_visualization :- RenderedPartCard [_chart-type render-type _timezone-id card dashcard data] (let [series-cards-results (:series-results dashcard) cards-with-data (->> series-cards-results @@ -451,12 +460,12 @@ (m/distinct-by #(get-in % [:card :id]))) viz-settings (or (get dashcard :visualization_settings) (get card :visualization_settings)) - {rendered-type :type content :content} (js-svg/javascript-visualization cards-with-data viz-settings)] + {rendered-type :type content :content} (js.svg/javascript-visualization cards-with-data viz-settings)] (case rendered-type :svg (let [image-bundle (image-bundle/make-image-bundle render-type - (js-svg/svg-string->bytes content))] + (js.svg/svg-string->bytes content))] {:attachments (when image-bundle (image-bundle/image-bundle->attachment image-bundle)) @@ -468,7 +477,7 @@ :html {:content [:div content] :attachments nil}))) -(mu/defmethod render :smartscalar :- formatter/RenderedPulseCard +(mu/defmethod render :smartscalar :- RenderedPartCard [_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 @@ -555,7 +564,7 @@ [k value])) funnel-viz raw-rows)] (remove nil? rows-data)))) -(mu/defmethod render :funnel_normal :- formatter/RenderedPulseCard +(mu/defmethod render :funnel_normal :- RenderedPartCard [_chart-type render-type _timezone-id card _dashcard {:keys [rows cols viz-settings] :as data}] (let [[x-axis-rowfn y-axis-rowfn] (formatter/graphing-column-row-fns card data) @@ -570,7 +579,7 @@ (assoc jsviz-settings :step {:name (:display_name x-col) :format (:x jsviz-settings)} :measure {:format (:y jsviz-settings)})) - svg (js-svg/funnel rows settings) + svg (js.svg/funnel rows settings) image-bundle (image-bundle/make-image-bundle render-type svg)] {:attachments (image-bundle/image-bundle->attachment image-bundle) @@ -580,14 +589,14 @@ [:img {:style (style/style {:display :block :width :100%}) :src (:image-src image-bundle)}]]})) -(mu/defmethod render :funnel :- formatter/RenderedPulseCard +(mu/defmethod render :funnel :- RenderedPartCard [_chart-type render-type timezone-id card dashcard data] (let [viz-settings (get card :visualization_settings)] (if (= (get viz-settings :funnel.type) "bar") (render :javascript_visualization render-type timezone-id card dashcard data) (render :funnel_normal render-type timezone-id card dashcard data)))) -(mu/defmethod render :empty :- formatter/RenderedPulseCard +(mu/defmethod render :empty :- RenderedPartCard [_chart-type render-type _timezone-id _card _dashcard _data] (let [image-bundle (image-bundle/no-results-image-bundle render-type)] {:attachments @@ -604,7 +613,7 @@ (trs "No results")]] :render/text (trs "No results")})) -(mu/defmethod render :attached :- formatter/RenderedPulseCard +(mu/defmethod render :attached :- RenderedPartCard [_chart-type render-type _timezone-id _card _dashcard _data] (let [image-bundle (image-bundle/attached-image-bundle render-type)] {:attachments @@ -620,7 +629,7 @@ :color style/color-gray-4})} (trs "This question has been included as a file attachment")]]})) -(mu/defmethod render :unknown :- formatter/RenderedPulseCard +(mu/defmethod render :unknown :- RenderedPartCard [_chart-type _render-type _timezone-id _card _dashcard _data] {:attachments nil @@ -634,10 +643,10 @@ [:br] (trs "Please view this card in Metabase.")]}) -(mu/defmethod render :card-error :- formatter/RenderedPulseCard +(mu/defmethod render :card-error :- RenderedPartCard [_chart-type _render-type _timezone-id _card _dashcard _data] @card-error-rendered-info) -(mu/defmethod render :render-error :- formatter/RenderedPulseCard +(mu/defmethod render :render-error :- RenderedPartCard [_chart-type _render-type _timezone-id _card _dashcard _data] @error-rendered-info) diff --git a/src/metabase/channel/render/card.clj b/src/metabase/channel/render/card.clj new file mode 100644 index 0000000000000000000000000000000000000000..2ed571aa32e1997130e09a6efc14a32882ae1813 --- /dev/null +++ b/src/metabase/channel/render/card.clj @@ -0,0 +1,247 @@ +(ns metabase.channel.render.card + (:require + [hiccup.core :refer [h]] + [metabase.channel.render.body :as body] + [metabase.channel.render.image-bundle :as image-bundle] + [metabase.channel.render.png :as png] + [metabase.channel.render.style :as style] + [metabase.models.dashboard-card :as dashboard-card] + [metabase.query-processor.timezone :as qp.timezone] + [metabase.util :as u] + [metabase.util.i18n :refer [tru]] + [metabase.util.log :as log] + [metabase.util.malli :as mu] + [metabase.util.malli.registry :as mr] + [metabase.util.markdown :as markdown] + [metabase.util.urls :as urls] + [toucan2.core :as t2])) + +;;; I gave these keys below namespaces to make them easier to find usages for but didn't use `metabase.channel.render` so +;;; we can keep this as an internal namespace you don't need to know about outside of the module. +(mr/def ::options + "Options for Pulse (i.e. Alert/Dashboard Subscription) rendering." + [:map + [:channel.render/include-buttons? {:description "default: false", :optional true} :boolean] + [:channel.render/include-title? {:description "default: false", :optional true} :boolean] + [:channel.render/include-description? {:description "default: false", :optional true} :boolean]]) + +(defn- card-href + [card] + (h (urls/card-url (u/the-id card)))) + +(mu/defn- make-title-if-needed :- [:maybe body/RenderedPartCard] + [render-type card dashcard options :- [:maybe ::options]] + (when (:channel.render/include-title? options) + (let [card-name (or (-> dashcard :visualization_settings :card.title) + (-> card :name)) + image-bundle (when (:channel.render/include-buttons? options) + (image-bundle/external-link-image-bundle render-type))] + {:attachments (when image-bundle + (image-bundle/image-bundle->attachment image-bundle)) + :content [:table {:style (style/style {:margin-bottom :2px + :border-collapse :collapse + :width :100%})} + [:tbody + [:tr + [:td {:style (style/style {:padding :0 + :margin :0})} + [:a {:style (style/style (style/header-style)) + :href (card-href card) + :target "_blank" + :rel "noopener noreferrer"} + (h card-name)]] + [:td {:style (style/style {:text-align :right})} + (when (:channel.render/include-buttons? options) + [:img {:style (style/style {:width :16px}) + :width 16 + :src (:image-src image-bundle)}])]]]]}))) + +(mu/defn- make-description-if-needed :- [:maybe body/RenderedPartCard] + [dashcard card options :- [:maybe ::options]] + (when (:channel.render/include-description? options) + (when-let [description (or (get-in dashcard [:visualization_settings :card.description]) + (:description card))] + {:attachments {} + :content [:div {:style (style/style {:color style/color-text-medium + :font-size :12px + :margin-bottom :8px})} + (markdown/process-markdown description :html)]}))) + +(defn detect-pulse-chart-type + "Determine the pulse (visualization) type of a `card`, e.g. `:scalar` or `:bar`." + [{display-type :display card-name :name} maybe-dashcard {:keys [cols rows] :as data}] + (let [col-sample-count (delay (count (take 3 cols))) + row-sample-count (delay (count (take 2 rows)))] + (letfn [(chart-type [tyype reason & args] + (log/tracef "Detected chart type %s for Card %s because %s" + tyype (pr-str card-name) (apply format reason args)) + tyype)] + (cond + (or (empty? rows) + ;; Many aggregations result in [[nil]] if there are no rows to aggregate after filters + (= [[nil]] (-> data :rows))) + (chart-type :empty "there are no rows in results") + + (#{:pin_map :state :country} display-type) + (chart-type nil "display-type is %s" display-type) + + (and (some? maybe-dashcard) + (pos? (count (dashboard-card/dashcard->multi-cards maybe-dashcard)))) + (chart-type :javascript_visualization "result has multiple card semantics, a multiple chart") + + ;; for scalar/smartscalar, the display-type might actually be :line, so we can't have line above + (and (not (contains? #{:progress :gauge} display-type)) + (= @col-sample-count @row-sample-count 1)) + (chart-type :scalar "result has one row and one column") + + (#{:scalar + :row + :progress + :gauge + :table + :funnel} display-type) + (chart-type display-type "display-type is %s" display-type) + + (#{:smartscalar + :scalar + :pie + :scatter + :waterfall + :line + :area + :bar + :combo} display-type) + (chart-type :javascript_visualization "display-type is javascript_visualization") + + :else + (chart-type :table "no other chart types match"))))) + +(defn- is-attached? + [card] + ((some-fn :include_csv :include_xls) card)) + +(mu/defn- render-pulse-card-body :- body/RenderedPartCard + [render-type + timezone-id :- [:maybe :string] + card + dashcard + {:keys [data error] :as results}] + (try + (when error + (throw (ex-info (tru "Card has errors: {0}" error) (assoc results :card-error true)))) + (let [chart-type (or (detect-pulse-chart-type card dashcard data) + (when (is-attached? card) + :attached) + :unknown)] + (log/debugf "Rendering pulse card with chart-type %s and render-type %s" chart-type render-type) + (body/render chart-type render-type timezone-id card dashcard data)) + (catch Throwable e + (if (:card-error (ex-data e)) + (do + (log/error e "Pulse card query error") + (body/render :card-error nil nil nil nil nil)) + (do + (log/error e "Pulse card render error") + (body/render :render-error nil nil nil nil nil)))))) + +(mu/defn render-pulse-card :- body/RenderedPartCard + "Render a single `card` for a `Pulse` to Hiccup HTML. `result` is the QP results. Returns a map with keys + + - attachments + - content (a hiccup form suitable for rendering on rich clients or rendering into an image) + - 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 card dashcard results] + (render-pulse-card render-type timezone-id card dashcard results nil)) + + ([render-type + timezone-id :- [:maybe :string] + card + dashcard + results + options :- [:maybe ::options]] + (let [{title :content + title-attachments :attachments} (make-title-if-needed render-type card dashcard options) + {description :content} (make-description-if-needed dashcard card options) + {pulse-body :content + body-attachments :attachments + 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. + ;; Surrounding <p> element prevents buggy behavior when dragging scrollbar. + [:div + [:a {:href (card-href card) + :target "_blank" + :rel "noopener noreferrer" + :style (style/style + (style/section-style) + {:display :block + :text-decoration :none})} + title + description + [:div {:class "pulse-body" + :style (style/style {:overflow-x :auto ;; when content is wide enough, automatically show a horizontal scrollbar + :display :block + :margin :16px})} + (if-let [more-results-message (body/attached-results-text render-type card)] + (conj more-results-message (list pulse-body)) + pulse-body)]]]]} + text (assoc :render/text text))))) + +(mu/defn render-pulse-card-for-display + "Same as `render-pulse-card` but isn't intended for an email, rather for previewing so there is no need for + attachments" + ([timezone-id card results] + (render-pulse-card-for-display timezone-id card results nil)) + + ([timezone-id card results options :- [:maybe ::options]] + (:content (render-pulse-card :inline timezone-id card nil results options)))) + +(mu/defn render-pulse-section :- body/RenderedPartCard + "Render a single Card section of a Pulse to a Hiccup form (representating HTML)." + ([timezone-id part] + (render-pulse-section timezone-id part)) + + ([timezone-id + {card :card, dashcard :dashcard, result :result, :as _part} + options :- [:maybe ::options]] + (let [options (merge {:channel.render/include-title? true + :channel.render/include-description? true} + options) + {:keys [attachments content]} (render-pulse-card :attachment timezone-id card dashcard result options)] + {:attachments attachments + :content [:div {:style (style/style {:margin-top :20px + :margin-bottom :20px})} + content]}))) + +(mu/defn render-pulse-card-to-png :- bytes? + "Render a `pulse-card` as a PNG. `data` is the `:data` from a QP result." + (^bytes [timezone-id pulse-card result width] + (render-pulse-card-to-png timezone-id pulse-card result width nil)) + + (^bytes [timezone-id :- [:maybe :string] + pulse-card + result + width + options :- [:maybe ::options]] + (png/render-html-to-png (render-pulse-card :inline timezone-id pulse-card nil result options) width))) + +(mu/defn render-pulse-card-to-base64 :- string? + "Render a `pulse-card` as a PNG and return it as a base64 encoded string." + ^String [timezone-id card dashcard result width] + (-> (render-pulse-card :inline timezone-id card dashcard result) + (png/render-html-to-png width) + image-bundle/render-img-data-uri)) + +(mu/defn png-from-render-info :- bytes? + "Create a PNG file (as a byte array) from rendering info." + ^bytes [rendered-info :- body/RenderedPartCard width] + ;; TODO huh? why do we need this indirection? + (png/render-html-to-png rendered-info width)) + +(mu/defn defaulted-timezone :- :string + "Returns the timezone ID for the given `card`. Either the report timezone (if applicable) or the JVM timezone." + [card] + (or (some->> card :database_id (t2/select-one :model/Database :id) qp.timezone/results-timezone-id) + (qp.timezone/system-timezone-id))) diff --git a/src/metabase/channel/render/core.clj b/src/metabase/channel/render/core.clj index cb730190a98713432f2a3dc639a36509128acd9f..63178da6209d752482b99ca9e99af2d8a8279a33 100644 --- a/src/metabase/channel/render/core.clj +++ b/src/metabase/channel/render/core.clj @@ -1,254 +1,35 @@ (ns metabase.channel.render.core (:require - [hiccup.core :refer [h]] - [metabase.channel.render.body :as body] + [metabase.channel.render.card :as render.card] [metabase.channel.render.image-bundle :as image-bundle] - [metabase.channel.render.png :as png] + [metabase.channel.render.js.svg :as js.svg] + [metabase.channel.render.preview :as render.preview] [metabase.channel.render.style :as style] - [metabase.formatter :as formatter] - [metabase.models.dashboard-card :as dashboard-card] - [metabase.util :as u] - [metabase.util.i18n :refer [tru]] - [metabase.util.log :as log] - [metabase.util.malli :as mu] - [metabase.util.malli.registry :as mr] - [metabase.util.markdown :as markdown] - [metabase.util.urls :as urls] [potemkin :as p])) (p/import-vars [image-bundle image-bundle->attachment make-image-bundle] - ;; TODO -- this stuff is also used by emails, it probably should belong in some sort of common place [style color-text-light color-text-medium color-text-dark primary-color section-style - style]) - -;;; I gave these keys below namespaces to make them easier to find usages for but didn't use `metabase.channel.render` so -;;; we can keep this as an internal namespace you don't need to know about outside of the module. -(mr/def ::options - "Options for Pulse (i.e. Alert/Dashboard Subscription) rendering." - [:map - [:channel.render/include-buttons? {:description "default: false", :optional true} :boolean] - [:channel.render/include-title? {:description "default: false", :optional true} :boolean] - [:channel.render/include-description? {:description "default: false", :optional true} :boolean]]) - -(defn- card-href - [card] - (h (urls/card-url (u/the-id card)))) - -(mu/defn- make-title-if-needed :- [:maybe formatter/RenderedPulseCard] - [render-type card dashcard options :- [:maybe ::options]] - (when (:channel.render/include-title? options) - (let [card-name (or (-> dashcard :visualization_settings :card.title) - (-> card :name)) - image-bundle (when (:channel.render/include-buttons? options) - (image-bundle/external-link-image-bundle render-type))] - {:attachments (when image-bundle - (image-bundle/image-bundle->attachment image-bundle)) - :content [:table {:style (style/style {:margin-bottom :2px - :border-collapse :collapse - :width :100%})} - [:tbody - [:tr - [:td {:style (style/style {:padding :0 - :margin :0})} - [:a {:style (style/style (style/header-style)) - :href (card-href card) - :target "_blank" - :rel "noopener noreferrer"} - (h card-name)]] - [:td {:style (style/style {:text-align :right})} - (when (:channel.render/include-buttons? options) - [:img {:style (style/style {:width :16px}) - :width 16 - :src (:image-src image-bundle)}])]]]]}))) - -(mu/defn- make-description-if-needed :- [:maybe formatter/RenderedPulseCard] - [dashcard card options :- [:maybe ::options]] - (when (:channel.render/include-description? options) - (when-let [description (or (get-in dashcard [:visualization_settings :card.description]) - (:description card))] - {:attachments {} - :content [:div {:style (style/style {:color style/color-text-medium - :font-size :12px - :margin-bottom :8px})} - (markdown/process-markdown description :html)]}))) - -(defn detect-pulse-chart-type - "Determine the pulse (visualization) type of a `card`, e.g. `:scalar` or `:bar`." - [{display-type :display card-name :name} maybe-dashcard {:keys [cols rows] :as data}] - (let [col-sample-count (delay (count (take 3 cols))) - row-sample-count (delay (count (take 2 rows)))] - (letfn [(chart-type [tyype reason & args] - (log/tracef "Detected chart type %s for Card %s because %s" - tyype (pr-str card-name) (apply format reason args)) - tyype)] - (cond - (or (empty? rows) - ;; Many aggregations result in [[nil]] if there are no rows to aggregate after filters - (= [[nil]] (-> data :rows))) - (chart-type :empty "there are no rows in results") - - (#{:pin_map :state :country} display-type) - (chart-type nil "display-type is %s" display-type) - - (and (some? maybe-dashcard) - (pos? (count (dashboard-card/dashcard->multi-cards maybe-dashcard)))) - (chart-type :javascript_visualization "result has multiple card semantics, a multiple chart") - - ;; for scalar/smartscalar, the display-type might actually be :line, so we can't have line above - (and (not (contains? #{:progress :gauge} display-type)) - (= @col-sample-count @row-sample-count 1)) - (chart-type :scalar "result has one row and one column") - - (#{:scalar - :row - :progress - :gauge - :table - :funnel} display-type) - (chart-type display-type "display-type is %s" display-type) - - (#{:smartscalar - :scalar - :pie - :scatter - :waterfall - :line - :area - :bar - :combo} display-type) - (chart-type :javascript_visualization "display-type is javascript_visualization") - - :else - (chart-type :table "no other chart types match"))))) - -(defn- is-attached? - [card] - ((some-fn :include_csv :include_xls) card)) - -(mu/defn- render-pulse-card-body :- formatter/RenderedPulseCard - [render-type - timezone-id :- [:maybe :string] - card - dashcard - {:keys [data error] :as results}] - (try - (when error - (throw (ex-info (tru "Card has errors: {0}" error) (assoc results :card-error true)))) - (let [chart-type (or (detect-pulse-chart-type card dashcard data) - (when (is-attached? card) - :attached) - :unknown)] - (log/debugf "Rendering pulse card with chart-type %s and render-type %s" chart-type render-type) - (body/render chart-type render-type timezone-id card dashcard data)) - (catch Throwable e - (if (:card-error (ex-data e)) - (do - (log/error e "Pulse card query error") - (body/render :card-error nil nil nil nil nil)) - (do - (log/error e "Pulse card render error") - (body/render :render-error nil nil nil nil nil)))))) - -(mu/defn render-pulse-card :- formatter/RenderedPulseCard - "Render a single `card` for a `Pulse` to Hiccup HTML. `result` is the QP results. Returns a map with keys - - - attachments - - content (a hiccup form suitable for rendering on rich clients or rendering into an image) - - 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 card dashcard results] - (render-pulse-card render-type timezone-id card dashcard results nil)) - - ([render-type - timezone-id :- [:maybe :string] - card - dashcard - results - options :- [:maybe ::options]] - (let [{title :content - title-attachments :attachments} (make-title-if-needed render-type card dashcard options) - {description :content} (make-description-if-needed dashcard card options) - {pulse-body :content - body-attachments :attachments - 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. - ;; Surrounding <p> element prevents buggy behavior when dragging scrollbar. - [:div - [:a {:href (card-href card) - :target "_blank" - :rel "noopener noreferrer" - :style (style/style - (style/section-style) - {:display :block - :text-decoration :none})} - title - description - [:div {:class "pulse-body" - :style (style/style {:overflow-x :auto ;; when content is wide enough, automatically show a horizontal scrollbar - :display :block - :margin :16px})} - (if-let [more-results-message (body/attached-results-text render-type card)] - (conj more-results-message (list pulse-body)) - pulse-body)]]]]} - text (assoc :render/text text))))) - -(mu/defn render-pulse-card-for-display - "Same as `render-pulse-card` but isn't intended for an email, rather for previewing so there is no need for - attachments" - ([timezone-id card results] - (render-pulse-card-for-display timezone-id card results nil)) - - ([timezone-id card results options :- [:maybe ::options]] - (:content (render-pulse-card :inline timezone-id card nil results options)))) - -(mu/defn render-pulse-section :- formatter/RenderedPulseCard - "Render a single Card section of a Pulse to a Hiccup form (representating HTML)." - ([timezone-id part] - (render-pulse-section timezone-id part)) - - ([timezone-id - {card :card, dashcard :dashcard, result :result, :as _part} - options :- [:maybe ::options]] - (let [options (merge {:channel.render/include-title? true - :channel.render/include-description? true} - options) - {:keys [attachments content]} (render-pulse-card :attachment timezone-id card dashcard result options)] - {:attachments attachments - :content [:div {:style (style/style {:margin-top :20px - :margin-bottom :20px})} - content]}))) - -(mu/defn render-pulse-card-to-png :- bytes? - "Render a `pulse-card` as a PNG. `data` is the `:data` from a QP result." - (^bytes [timezone-id pulse-card result width] - (render-pulse-card-to-png timezone-id pulse-card result width nil)) - - (^bytes [timezone-id :- [:maybe :string] - pulse-card - result - width - options :- [:maybe ::options]] - (png/render-html-to-png (render-pulse-card :inline timezone-id pulse-card nil result options) width))) - -(mu/defn render-pulse-card-to-base64 :- string? - "Render a `pulse-card` as a PNG and return it as a base64 encoded string." - ^String [timezone-id card dashcard result width] - (-> (render-pulse-card :inline timezone-id card dashcard result) - (png/render-html-to-png width) - image-bundle/render-img-data-uri)) - -(mu/defn png-from-render-info :- bytes? - "Create a PNG file (as a byte array) from rendering info." - ^bytes [rendered-info :- formatter/RenderedPulseCard width] - ;; TODO huh? why do we need this indirection? - (png/render-html-to-png rendered-info width)) + style] + [render.preview + render-dashboard-to-html + style-tag-from-inline-styles + style-tag-nonce-middleware] + [render.card + detect-pulse-chart-type + defaulted-timezone + render-pulse-card + render-pulse-card-for-display + render-pulse-section + render-pulse-card-to-png + render-pulse-card-to-base64 + png-from-render-info] + [js.svg + icon]) diff --git a/src/metabase/channel/render/color.clj b/src/metabase/channel/render/js/color.clj similarity index 82% rename from src/metabase/channel/render/color.clj rename to src/metabase/channel/render/js/color.clj index 52cce11d9439eb3d8ea99309ba4d01d7c2129bfc..34afed887c2ca91a139207e12d6469cf17157e99 100644 --- a/src/metabase/channel/render/color.clj +++ b/src/metabase/channel/render/js/color.clj @@ -1,10 +1,10 @@ -(ns metabase.channel.render.color +(ns metabase.channel.render.js.color "Namespaces that uses the Nashorn javascript engine to invoke some shared javascript code that we use to determine the background color of pulse table cells" (:require [cheshire.core :as json] [clojure.java.io :as io] - [metabase.channel.render.js-engine :as js] + [metabase.channel.render.js.engine :as js.engine] [metabase.formatter] [metabase.util.i18n :refer [trs]] [metabase.util.malli :as mu]) @@ -17,12 +17,12 @@ (def ^:private ^{:arglists '([])} js-engine ;; As of 2024/05/13, a single color selector js engine takes 3.5 MiB of memory - (js/threadlocal-fifo-memoizer + (js.engine/threadlocal-fifo-memoizer (fn [] (let [file-url (io/resource js-file-path)] (assert file-url (trs "Can''t find JS color selector at ''{0}''" js-file-path)) - (doto (js/context) - (js/load-resource js-file-path)))) + (doto (js.engine/context) + (js.engine/load-resource js-file-path)))) 5)) (def ^:private QueryResults @@ -46,10 +46,10 @@ ;; expensive. The JS code is written to deal with `rows` in it's native Nashorn format but since `cols` and ;; `viz-settings` are small, pass those as JSON so that they can be deserialized to pure JS objects once in JS ;; code - (js/execute-fn-name (js-engine) "makeCellBackgroundGetter" - rows - (json/generate-string cols) - (json/generate-string viz-settings))) + (js.engine/execute-fn-name (js-engine) "makeCellBackgroundGetter" + rows + (json/generate-string cols) + (json/generate-string viz-settings))) (defn get-background-color "Get the correct color for a cell in a pulse table. Returns color as string suitable for use CSS, e.g. a hex string or @@ -59,4 +59,4 @@ (let [cell-value (if (instance? NumericWrapper cell-value) (:num-value cell-value) cell-value)] - (.asString (js/execute-fn color-selector cell-value row-index column-name)))) + (.asString (js.engine/execute-fn color-selector cell-value row-index column-name)))) diff --git a/src/metabase/channel/render/js_engine.clj b/src/metabase/channel/render/js/engine.clj similarity index 98% rename from src/metabase/channel/render/js_engine.clj rename to src/metabase/channel/render/js/engine.clj index 3785cdf14e1c51a940140dff65b7c2d0e0b6976b..cc3b09d77386c2cc5efd0511b9f5fd76fc5d5edd 100644 --- a/src/metabase/channel/render/js_engine.clj +++ b/src/metabase/channel/render/js/engine.clj @@ -1,4 +1,4 @@ -(ns metabase.channel.render.js-engine +(ns metabase.channel.render.js.engine "Graal polyglot context suitable for executing javascript code. We run the js in interpreted mode and turn off the warning with the `(option \"engine.WarnInterpreterOnly\" diff --git a/src/metabase/channel/render/js_svg.clj b/src/metabase/channel/render/js/svg.clj similarity index 82% rename from src/metabase/channel/render/js_svg.clj rename to src/metabase/channel/render/js/svg.clj index 059621f901f03cfd8a091dead8832761cce336b7..e280e1ae94c84496ca55effcd7fe3d466c101a06 100644 --- a/src/metabase/channel/render/js_svg.clj +++ b/src/metabase/channel/render/js/svg.clj @@ -1,4 +1,4 @@ -(ns metabase.channel.render.js-svg +(ns metabase.channel.render.js.svg "Functions to render charts as svg strings by using graal's js engine. A bundle is built by `yarn build-static-viz` which has charting library. This namespace has some wrapper functions to invoke those functions. Interop is very strange, as the jvm datastructures, not just serialized versions are used. This is why we have the `toJSArray` and @@ -6,7 +6,7 @@ (:require [cheshire.core :as json] [clojure.string :as str] - [metabase.channel.render.js-engine :as js] + [metabase.channel.render.js.engine :as js.engine] [metabase.channel.render.style :as style] [metabase.config :as config] [metabase.public-settings :as public-settings]) @@ -32,20 +32,20 @@ (defn- load-viz-bundle [^Context context] (doto context - (js/load-resource bundle-path) - (js/load-resource interface-path))) + (js.engine/load-resource bundle-path) + (js.engine/load-resource interface-path))) (def ^:private static-viz-context-delay "Delay containing a graal js context. It has the chart bundle and the above `src-api` in its environment suitable for creating charts." - (delay (load-viz-bundle (js/context)))) + (delay (load-viz-bundle (js.engine/context)))) (defn- context "Returns a static viz context. In dev mode, this will be a new context each time. In prod or test modes, it will return the derefed contents of `static-viz-context-delay`." ^Context [] (if config/is-dev? - (load-viz-bundle (js/context)) + (load-viz-bundle (js.engine/context)) @static-viz-context-delay)) (defn- post-process @@ -141,17 +141,17 @@ "Clojure entrypoint to render a funnel chart. Data should be vec of [[Step Measure]] where Step is {:name name :format format-options} and Measure is {:format format-options} and you go and look to frontend/src/metabase/static-viz/components/FunnelChart/types.ts for the actual format options. Returns a byte array of a png file." [data settings] - (let [svg-string (.asString (js/execute-fn-name (context) "funnel" (json/generate-string data) - (json/generate-string settings)))] + (let [svg-string (.asString (js.engine/execute-fn-name (context) "funnel" (json/generate-string data) + (json/generate-string settings)))] (svg-string->bytes svg-string))) (defn javascript-visualization "Clojure entrypoint to render javascript visualizations." [cards-with-data dashcard-viz-settings] - (let [response (.asString (js/execute-fn-name (context) "javascript_visualization" - (json/generate-string cards-with-data) - (json/generate-string dashcard-viz-settings) - (json/generate-string (public-settings/application-colors))))] + (let [response (.asString (js.engine/execute-fn-name (context) "javascript_visualization" + (json/generate-string cards-with-data) + (json/generate-string dashcard-viz-settings) + (json/generate-string (public-settings/application-colors))))] (-> response (json/parse-string true) (update :type (fnil keyword "unknown"))))) @@ -159,28 +159,28 @@ (defn row-chart "Clojure entrypoint to render a row chart." [settings data] - (let [svg-string (.asString (js/execute-fn-name (context) "row_chart" - (json/generate-string settings) - (json/generate-string data) - (json/generate-string (public-settings/application-colors))))] + (let [svg-string (.asString (js.engine/execute-fn-name (context) "row_chart" + (json/generate-string settings) + (json/generate-string data) + (json/generate-string (public-settings/application-colors))))] (svg-string->bytes svg-string))) (defn gauge "Clojure entrypoint to render a gauge chart. Returns a byte array of a png file" [card data] - (let [js-res (js/execute-fn-name (context) "gauge" - (json/generate-string card) - (json/generate-string data)) + (let [js-res (js.engine/execute-fn-name (context) "gauge" + (json/generate-string card) + (json/generate-string data)) svg-string (.asString js-res)] (svg-string->bytes svg-string))) (defn progress "Clojure entrypoint to render a progress bar. Returns a byte array of a png file" [value goal settings] - (let [js-res (js/execute-fn-name (context) "progress" - (json/generate-string {:value value :goal goal}) - (json/generate-string settings) - (json/generate-string (public-settings/application-colors))) + (let [js-res (js.engine/execute-fn-name (context) "progress" + (json/generate-string {:value value :goal goal}) + (json/generate-string settings) + (json/generate-string (public-settings/application-colors))) svg-string (.asString js-res)] (svg-string->bytes svg-string))) diff --git a/src/metabase/channel/render/png.clj b/src/metabase/channel/render/png.clj index f4be755b12039f0d552b441906e8f252956b3d31..fa003cb0625fa0480be2e7eb1821bad785c89471 100644 --- a/src/metabase/channel/render/png.clj +++ b/src/metabase/channel/render/png.clj @@ -9,8 +9,8 @@ (:require [clojure.walk :as walk] [hiccup.core :refer [html]] + [metabase.channel.render.body :as body] [metabase.channel.render.style :as style] - [metabase.formatter :as formatter] [metabase.util.log :as log] [metabase.util.malli :as mu]) (:import @@ -123,7 +123,7 @@ (mu/defn render-html-to-png :- bytes? "Render the Hiccup HTML `content` of a Pulse to a PNG image, returning a byte array." - ^bytes [{:keys [content]} :- formatter/RenderedPulseCard + ^bytes [{:keys [content]} :- body/RenderedPartCard width] (try (let [html (html [:html diff --git a/src/metabase/channel/render/preview.clj b/src/metabase/channel/render/preview.clj index 5a2d0a29e8e5a2f8b93cea09cb10d3099ef86c6c..19b57bd97ae616722e3e9cbb7e333132d24dfbf2 100644 --- a/src/metabase/channel/render/preview.clj +++ b/src/metabase/channel/render/preview.clj @@ -8,11 +8,10 @@ [hickory.core :as hik] [hickory.render :as hik.r] [hickory.zip :as hik.z] - [metabase.channel.render.core :as render] + [metabase.channel.render.card :as render.card] [metabase.channel.render.image-bundle :as img] [metabase.channel.render.png :as png] [metabase.channel.render.style :as style] - [metabase.channel.shared :as channel.shared] [metabase.email.result-attachment :as email.result-attachment] [metabase.util.markdown :as markdown] [toucan2.core :as t2])) @@ -53,7 +52,7 @@ [:td {:style (style/style (merge table-style-map {:max-width "400px"}))} content])] (if card - (let [base-render (render/render-pulse-card :inline (channel.shared/defaulted-timezone card) card dashcard result) + (let [base-render (render.card/render-pulse-card :inline (render.card/defaulted-timezone card) card dashcard result) html-src (-> base-render :content) img-src (-> base-render (png/render-html-to-png 1200) @@ -78,7 +77,7 @@ (def ^:private execute-dashboard (requiring-resolve 'metabase.notification.payload.execute/execute-dashboard)) -(defn render-dashboard-to-hiccup +(defn- render-dashboard-to-hiccup "Given a dashboard ID, renders all of the dashcards to hiccup datastructure." [dashboard-id] (let [user (t2/select-one :model/User) diff --git a/src/metabase/channel/render/table.clj b/src/metabase/channel/render/table.clj index 17496cce60075371a326590897e711beadd6d942..3b61519279d70a2f57003bf16c51972680b709e4 100644 --- a/src/metabase/channel/render/table.clj +++ b/src/metabase/channel/render/table.clj @@ -3,7 +3,7 @@ [clojure.string :as str] [hiccup.core :refer [h]] [medley.core :as m] - [metabase.channel.render.color :as color] + [metabase.channel.render.js.color :as js.color] [metabase.channel.render.style :as style] [metabase.formatter]) (:import @@ -169,4 +169,4 @@ :cellpadding "0" :cellspacing "0"} (render-table-head (vec col-names) header) - (render-table-body (partial color/get-background-color color-selector) normalized-zero cols-for-color-lookup rows)]))) + (render-table-body (partial js.color/get-background-color color-selector) normalized-zero cols-for-color-lookup rows)]))) diff --git a/src/metabase/channel/shared.clj b/src/metabase/channel/shared.clj index 0cae0274e908fa213d4118aaf86d2e9ad52f743d..54cbb0cb7635b7e27ef26d8aa1c87b3d840648e5 100644 --- a/src/metabase/channel/shared.clj +++ b/src/metabase/channel/shared.clj @@ -1,17 +1,9 @@ (ns metabase.channel.shared + "Shared functions for channel implementations." (:require [malli.core :as mc] [malli.error :as me] - [metabase.query-processor.timezone :as qp.timezone] - [metabase.util.i18n :refer [tru]] - [metabase.util.malli :as mu] - [toucan2.core :as t2])) - -(mu/defn defaulted-timezone :- :string - "Returns the timezone ID for the given `card`. Either the report timezone (if applicable) or the JVM timezone." - [card] - (or (some->> card :database_id (t2/select-one :model/Database :id) qp.timezone/results-timezone-id) - (qp.timezone/system-timezone-id))) + [metabase.util.i18n :refer [tru]])) (defn validate-channel-details "Validate a value against a schema and throw an exception if it's invalid. diff --git a/src/metabase/formatter.clj b/src/metabase/formatter.clj index 7a5f9b9f21adfc2439351ad62e840c4e8d9e47fb..37ae4aaac23ee6b013a1eae4adc653f9b363e358 100644 --- a/src/metabase/formatter.clj +++ b/src/metabase/formatter.clj @@ -20,7 +20,6 @@ [potemkin.types :as p.types]) (:import (java.math RoundingMode) - (java.net URL) (java.text DecimalFormat DecimalFormatSymbols))) (set! *warn-on-reflection* true) @@ -33,13 +32,6 @@ format-temporal-str temporal-string?]) -(def RenderedPulseCard - "Schema used for functions that operate on pulse card contents and their attachments" - [:map - [:attachments [:maybe [:map-of :string (ms/InstanceOfClass URL)]]] - [:content [:sequential :any]] - [:render/text {:optional true} [:maybe :string]]]) - (p.types/defrecord+ NumericWrapper [^String num-str ^Number num-value] hiccup.util/ToString (to-str [_] num-str) diff --git a/src/metabase/notification/payload/core.clj b/src/metabase/notification/payload/core.clj index 1f371fbfa15a9d091f9a6bb69735850aa8e14c29..87ec06abf20532870bb70a0ba202014408725775 100644 --- a/src/metabase/notification/payload/core.clj +++ b/src/metabase/notification/payload/core.clj @@ -1,15 +1,19 @@ (ns metabase.notification.payload.core - #_{:clj-kondo/ignore [:metabase/ns-module-checker]} (:require - [metabase.channel.render.style :as style] + [metabase.channel.render.core :as channel.render] [metabase.models.notification :as models.notification] [metabase.notification.payload.execute :as notification.payload.execute] [metabase.public-settings :as public-settings] [metabase.util :as u] [metabase.util.malli :as mu] [metabase.util.malli.schema :as ms] + [potemkin :as p] [toucan2.core :as t2])) +(p/import-vars + [notification.payload.execute + process-virtual-dashcard]) + (def Notification "Schema for the notification." ;; TODO: how do we make this schema closed after :merge? @@ -95,8 +99,7 @@ [:alert :map]]]]] [:notification/testing :map]]]) -;; TODO: from metabase.email.messages -(defn logo-url +(defn- logo-url "Return the URL for the application logo. If the logo is the default, return a URL to the Metabase logo." [] (let [url (public-settings/application-logo-url)] @@ -107,8 +110,7 @@ ;; (data-uri-svg? url) (themed-image-url url color) :else nil))) -;; TODO: from metabase.email.messages -(defn button-style +(defn- button-style "Return a CSS style string for a button with the given color." [color] (str "display: inline-block; " @@ -132,7 +134,7 @@ :site_name (public-settings/site-name) :site_url (public-settings/site-url) :admin_email (public-settings/admin-email) - :style {:button (button-style (style/primary-color))}}) + :style {:button (button-style (channel.render/primary-color))}}) (defmulti payload "Given a notification info, return the notification payload." diff --git a/src/metabase/notification/payload/execute.clj b/src/metabase/notification/payload/execute.clj index 7f0fa9a6640a082d64b824993f527eb091be39cd..4490164a3d65256ce7520a2bc9c9ffa769a23906 100644 --- a/src/metabase/notification/payload/execute.clj +++ b/src/metabase/notification/payload/execute.clj @@ -1,5 +1,4 @@ (ns metabase.notification.payload.execute - #_{:clj-kondo/ignore [:metabase/ns-module-checker]} (:require [malli.core :as mc] [metabase.api.common :as api] diff --git a/src/metabase/notification/payload/impl/alert.clj b/src/metabase/notification/payload/impl/alert.clj index 9914fdf3cde06c0e3c3ddb5eb983fc740b45e3a2..892258ae2fa5183558cb152b2307f6dd44b8d276 100644 --- a/src/metabase/notification/payload/impl/alert.clj +++ b/src/metabase/notification/payload/impl/alert.clj @@ -1,7 +1,6 @@ (ns metabase.notification.payload.impl.alert - #_{:clj-kondo/ignore [:metabase/ns-module-checker]} (:require - [metabase.channel.render.style :as style] + [metabase.channel.render.core :as channel.render] [metabase.notification.payload.core :as notification.payload] [metabase.notification.payload.execute :as notification.execute] [metabase.util.malli :as mu] @@ -16,9 +15,9 @@ ;; TODO: check whether we can remove this or name it? :pulse-id (:id alert)) :card (t2/select-one :model/Card card_id) - :style {:color_text_dark style/color-text-dark - :color_text_light style/color-text-light - :color_text_medium style/color-text-medium} + :style {:color_text_dark channel.render/color-text-dark + :color_text_light channel.render/color-text-light + :color_text_medium channel.render/color-text-medium} :alert alert})) (defn- goal-met? [{:keys [alert_above_goal], :as alert} card_part] diff --git a/src/metabase/notification/payload/impl/dashboard_subscription.clj b/src/metabase/notification/payload/impl/dashboard_subscription.clj index 779effdbcaa0fa110c9fd3cb290694b6d388f689..01cb26263d13a09e54e59f2edfd1a964dff2abcc 100644 --- a/src/metabase/notification/payload/impl/dashboard_subscription.clj +++ b/src/metabase/notification/payload/impl/dashboard_subscription.clj @@ -1,7 +1,6 @@ (ns metabase.notification.payload.impl.dashboard-subscription - #_{:clj-kondo/ignore [:metabase/ns-module-checker]} (:require - [metabase.channel.render.style :as style] + [metabase.channel.render.core :as channel.render] [metabase.models.params.shared :as shared.params] [metabase.notification.payload.core :as notification.payload] [metabase.notification.payload.execute :as notification.execute] @@ -35,9 +34,9 @@ (= part-type :card) (zero? (get-in part [:result :row_count] 0)))))) :dashboard dashboard - :style {:color_text_dark style/color-text-dark - :color_text_light style/color-text-light - :color_text_medium style/color-text-medium} + :style {:color_text_dark channel.render/color-text-dark + :color_text_light channel.render/color-text-light + :color_text_medium channel.render/color-text-medium} :parameters parameters :dashboard_subscription dashboard_subscription})) diff --git a/src/metabase/pulse/core.clj b/src/metabase/pulse/core.clj index b09bc939746f1c0c7bec8b9ca0adae529b09945d..a7cf272f10e00ec2aaeebe2cd427c29c4cd93f7d 100644 --- a/src/metabase/pulse/core.clj +++ b/src/metabase/pulse/core.clj @@ -1,5 +1,7 @@ -(ns metabase.pulse.core - "API namespace for the `metabase.pulse` module." +(ns ^:deprecated metabase.pulse.core + "API namespace for the `metabase.pulse` module. + + This namespace is deprecated, soon everything will be migrated to notifications." (:require [metabase.pulse.send] [potemkin :as p])) @@ -9,5 +11,4 @@ (p/import-vars [metabase.pulse.send - defaulted-timezone send-pulse!]) diff --git a/src/metabase/pulse/send.clj b/src/metabase/pulse/send.clj index 4b6bf4e7b60aac9104480a16858fb68e7938b6e5..1a3bc94eae49800ce8b5d397160f73e913e8e34c 100644 --- a/src/metabase/pulse/send.clj +++ b/src/metabase/pulse/send.clj @@ -1,28 +1,13 @@ (ns metabase.pulse.send "Code related to sending Pulses (Alerts or Dashboard Subscriptions)." (:require - [metabase.models.dashboard :as dashboard :refer [Dashboard]] - [metabase.models.database :refer [Database]] [metabase.models.interface :as mi] - [metabase.models.pulse :as models.pulse :refer [Pulse]] - [metabase.query-processor.timezone :as qp.timezone] + [metabase.models.pulse :as models.pulse] [metabase.util.log :as log] - [metabase.util.malli :as mu] - [metabase.util.malli.schema :as ms] [toucan2.core :as t2])) (set! *warn-on-reflection* true) -(defn- database-id [card] - (or (:database_id card) - (get-in card [:dataset_query :database]))) - -(mu/defn defaulted-timezone :- :string - "Returns the timezone ID for the given `card`. Either the report timezone (if applicable) or the JVM timezone." - [card :- (ms/InstanceOf :model/Card)] - (or (some->> card database-id (t2/select-one Database :id) qp.timezone/results-timezone-id) - (qp.timezone/system-timezone-id))) - ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | Creating Notifications To Send | ;;; +----------------------------------------------------------------------------------------------------------------+ @@ -108,6 +93,8 @@ (select-keys (-> pulse :cards first) [:include_xls :include_csv :pivot_results :format_rows])) :handlers [(get-notification-handler pulse-channel :notification/alert)]})) +(def ^:private send-notification! (requiring-resolve 'metabase.notification.core/*send-notification!*)) + (defn- send-pulse!* [{:keys [channels channel-ids] :as pulse} dashboard] (let [;; `channel-ids` is the set of channels to send to now, so only send to those. Note the whole set of channels @@ -116,7 +103,7 @@ channels)] (doseq [pulse-channel channels] (try - ((requiring-resolve 'metabase.notification.core/*send-notification!*) (notification-info pulse dashboard pulse-channel)) + (send-notification! (notification-info pulse dashboard pulse-channel)) (catch Exception e (log/errorf e "[Pulse %d] Error sending to %s channel" (:id pulse) (:channel_type pulse-channel))))) nil)) @@ -134,8 +121,8 @@ (send-pulse! pulse :channel-ids [312]) ; Send only to Channel with :id = 312" [{:keys [dashboard_id], :as pulse} & {:keys [channel-ids]}] {:pre [(map? pulse) (integer? (:creator_id pulse))]} - (let [dashboard (t2/select-one Dashboard :id dashboard_id) - pulse (-> (mi/instance Pulse pulse) + (let [dashboard (t2/select-one :model/Dashboard :id dashboard_id) + pulse (-> (mi/instance :model/Pulse pulse) ;; This is usually already done by this step, in the `send-pulses` task which uses `retrieve-pulse` ;; to fetch the Pulse. models.pulse/hydrate-notification diff --git a/src/metabase/task/send_pulses.clj b/src/metabase/task/send_pulses.clj index 2d27027bb95c913935be31f991df494c1e7a8cd8..24a6799661e806244e552f9d5ff2dc2c4e745c98 100644 --- a/src/metabase/task/send_pulses.clj +++ b/src/metabase/task/send_pulses.clj @@ -4,6 +4,7 @@ `SendPulse` job will send a pulse to all channels that are scheduled to run at the same time. For example if you have an Alert that has scheduled to send to both slack and emails at 6am, this job will be triggered and send the pulse to both channels. " + #_{:clj-kondo/ignore [:deprecated-namespace]} (:require [clojure.set :as set] [clojure.string :as str] diff --git a/test/metabase/api/downloads_exports_test.clj b/test/metabase/api/downloads_exports_test.clj index 7a55a0fc8ff736165e9823ae49da56168f1fa3f3..c54690639cc54179b5a767dddc67f45f0f31f885 100644 --- a/test/metabase/api/downloads_exports_test.clj +++ b/test/metabase/api/downloads_exports_test.clj @@ -9,6 +9,7 @@ - Static Embedding Dashboard/dashcard downloads - Dashboard Subscription Attachments - Alert attachments" + #_{:clj-kondo/ignore [:deprecated-namespace]} (:require [cheshire.core :as json] [clojure.data.csv :as csv] diff --git a/test/metabase/channel/render/body_test.clj b/test/metabase/channel/render/body_test.clj index 60f65402229b569fdfd10b63d25424564b386173..61c15c07759b855481cc520307c3bf87fb2c11de 100644 --- a/test/metabase/channel/render/body_test.clj +++ b/test/metabase/channel/render/body_test.clj @@ -7,11 +7,11 @@ [hiccup.core :refer [html]] [hickory.select :as hik.s] [metabase.channel.render.body :as body] + [metabase.channel.render.core :as channel.render] [metabase.formatter :as formatter] [metabase.models :refer [Card]] [metabase.notification.payload.execute :as notification.execute] [metabase.pulse.render.test-util :as render.tu] - [metabase.pulse.send :as pulse.send] [metabase.query-processor :as qp] [metabase.test :as mt] [metabase.test.data.interface :as tx] @@ -847,7 +847,7 @@ (defn- render-card [render-type card data] - (body/render render-type :attachment (pulse.send/defaulted-timezone card) card nil data)) + (body/render render-type :attachment (channel.render/defaulted-timezone card) card nil data)) (deftest render-cards-are-thread-safe-test-for-js-visualization (mt/with-temp [:model/Card card {:dataset_query (mt/mbql-query orders diff --git a/test/metabase/channel/render/core_test.clj b/test/metabase/channel/render/card_test.clj similarity index 65% rename from test/metabase/channel/render/core_test.clj rename to test/metabase/channel/render/card_test.clj index 3018da52a23ed56c62bb7b72f8c3a6662f97f746..b9345a79e46783c9d5a9b466fdd9252cd230652e 100644 --- a/test/metabase/channel/render/core_test.clj +++ b/test/metabase/channel/render/card_test.clj @@ -1,15 +1,15 @@ -(ns metabase.channel.render.core-test +(ns metabase.channel.render.card-test (:require [clojure.test :refer :all] [hiccup.core :as hiccup] [hickory.core :as hik] [hickory.select :as hik.s] - [metabase.channel.render.core :as render] + [metabase.channel.render.card :as channel.render.card] + [metabase.channel.render.core :as channel.render] [metabase.lib.util.match :as lib.util.match] [metabase.models :refer [Card Dashboard DashboardCard DashboardCardSeries]] [metabase.pulse.render.test-util :as render.tu] - [metabase.pulse.send :as pulse] [metabase.query-processor :as qp] [metabase.test :as mt] [metabase.util :as u] @@ -24,7 +24,7 @@ (render-pulse-card card (qp/process-query query))) ([card results] - (render/render-pulse-card-for-display (pulse/defaulted-timezone card) card results))) + (channel.render/render-pulse-card-for-display (channel.render/defaulted-timezone card) card results))) (defn- hiccup->hickory [content] @@ -51,7 +51,7 @@ (deftest ^:parallel render-error-test (testing "gives us a proper error if we have erroring card" - (let [rendered-card (render/render-pulse-card-for-display nil {:id 1} {:error "some error"})] + (let [rendered-card (channel.render/render-pulse-card-for-display nil {:id 1} {:error "some error"})] (is (= "There was a problem with this question." (-> (render.tu/nodes-with-text rendered-card "There was a problem with this question.") first @@ -59,10 +59,10 @@ (deftest ^:parallel detect-pulse-chart-type-test (testing "Currently unsupported chart types for static-viz return `nil`." - (are [tyype] (nil? (render/detect-pulse-chart-type {:display tyype} - {} - {:cols [{:base_type :type/Number}] - :rows [[2]]})) + (are [tyype] (nil? (channel.render/detect-pulse-chart-type {:display tyype} + {} + {:cols [{:base_type :type/Number}] + :rows [[2]]})) :pin_map :state :country))) @@ -70,64 +70,64 @@ (deftest ^:parallel detect-pulse-chart-type-test-2 (testing "Queries resulting in no rows return `:empty`." (is (= :empty - (render/detect-pulse-chart-type {:display :line} - {} - {:cols [{:base_type :type/Number}] - :rows [[nil]]}))))) + (channel.render/detect-pulse-chart-type {:display :line} + {} + {:cols [{:base_type :type/Number}] + :rows [[nil]]}))))) (deftest ^:parallel detect-pulse-chart-type-test-3 (testing "Unrecognized display-types with otherwise valid results return `:table`." (is (= :table - (render/detect-pulse-chart-type {:display :unrecognized} - {} - {:cols [{:base_type :type/Text} - {:base_type :type/Number}] - :rows [["A" 2] - ["B" 4]]}))))) + (channel.render/detect-pulse-chart-type {:display :unrecognized} + {} + {:cols [{:base_type :type/Text} + {:base_type :type/Number}] + :rows [["A" 2] + ["B" 4]]}))))) (deftest ^:parallel detect-pulse-chart-type-test-4 (testing "Scalar and Smartscalar charts are correctly identified" (is (= :scalar - (render/detect-pulse-chart-type {:display :line} - {} - {:cols [{:base_type :type/Number}] - :rows [[3]]}))) + (channel.render/detect-pulse-chart-type {:display :line} + {} + {:cols [{:base_type :type/Number}] + :rows [[3]]}))) (is (= :scalar - (render/detect-pulse-chart-type {:display :scalar} - {} - {:cols [{:base_type :type/Number}] - :rows [[6]]}))) + (channel.render/detect-pulse-chart-type {:display :scalar} + {} + {:cols [{:base_type :type/Number}] + :rows [[6]]}))) (is (= :javascript_visualization - (render/detect-pulse-chart-type {:display :smartscalar} - {} - {:cols [{:base_type :type/Temporal - :name "month"} - {:base_type :type/Number - :name "apples"}] - :rows [[#t "2020" 2] - [#t "2021" 3]] - :insights [{:name "apples" - :last-value 3 - :previous-value 2 - :last-change 50.0}]}))))) + (channel.render/detect-pulse-chart-type {:display :smartscalar} + {} + {:cols [{:base_type :type/Temporal + :name "month"} + {:base_type :type/Number + :name "apples"}] + :rows [[#t "2020" 2] + [#t "2021" 3]] + :insights [{:name "apples" + :last-value 3 + :previous-value 2 + :last-change 50.0}]}))))) (deftest ^:parallel detect-pulse-chart-type-test-5 (testing "Progress charts are correctly identified" (is (= :progress - (render/detect-pulse-chart-type {:display :progress} - {} - {:cols [{:base_type :type/Number}] - :rows [[6]]}))))) + (channel.render/detect-pulse-chart-type {:display :progress} + {} + {:cols [{:base_type :type/Number}] + :rows [[6]]}))))) (deftest ^:parallel detect-pulse-chart-type-test-6 (testing "The isomorphic display-types return correct chart-type." (are [chart-type] (= :javascript_visualization - (render/detect-pulse-chart-type {:display chart-type} - {} - {:cols [{:base_type :type/Text} - {:base_type :type/Number}] - :rows [["A" 2] - ["B" 3]]})) + (channel.render/detect-pulse-chart-type {:display chart-type} + {} + {:cols [{:base_type :type/Text} + {:base_type :type/Number}] + :rows [["A" 2] + ["B" 3]]})) :line :area :bar @@ -136,12 +136,12 @@ (deftest ^:parallel detect-pulse-chart-type-test-7 (testing "Various Single-Series display-types return correct chart-types." (are [chart-type] (= chart-type - (render/detect-pulse-chart-type {:display chart-type} - {} - {:cols [{:base_type :type/Text} - {:base_type :type/Number}] - :rows [["A" 2] - ["B" 3]]})) + (channel.render/detect-pulse-chart-type {:display chart-type} + {} + {:cols [{:base_type :type/Text} + {:base_type :type/Number}] + :rows [["A" 2] + ["B" 3]]})) :row :funnel :progress @@ -150,12 +150,12 @@ (deftest ^:parallel detect-pulse-chart-type-test-8 (testing "Pie charts are correctly identified and return `:javascript_visualization`." (is (= :javascript_visualization - (render/detect-pulse-chart-type {:display :pie} - {} - {:cols [{:base_type :type/Text} - {:base_type :type/Number}] - :rows [["apple" 3] - ["banana" 4]]}))))) + (channel.render/detect-pulse-chart-type {:display :pie} + {} + {:cols [{:base_type :type/Text} + {:base_type :type/Number}] + :rows [["apple" 3] + ["banana" 4]]}))))) (deftest ^:parallel detect-pulse-chart-type-test-9 (testing "Dashboard Cards can return `:multiple`." @@ -165,24 +165,24 @@ Dashboard dashboard {} DashboardCard dc1 {:dashboard_id (u/the-id dashboard) :card_id (u/the-id card1)} DashboardCardSeries _ {:dashboardcard_id (u/the-id dc1) :card_id (u/the-id card2)}] - (render/detect-pulse-chart-type card1 - dc1 - {:cols [{:base_type :type/Temporal} - {:base_type :type/Number}] - :rows [[#t "2020" 2] - [#t "2021" 3]]})))) + (channel.render/detect-pulse-chart-type card1 + dc1 + {:cols [{:base_type :type/Temporal} + {:base_type :type/Number}] + :rows [[#t "2020" 2] + [#t "2021" 3]]})))) (is (= :javascript_visualization (mt/with-temp [Card card1 {:display :line} Card card2 {:display :funnel} Dashboard dashboard {} DashboardCard dc1 {:dashboard_id (u/the-id dashboard) :card_id (u/the-id card1)} DashboardCardSeries _ {:dashboardcard_id (u/the-id dc1) :card_id (u/the-id card2)}] - (render/detect-pulse-chart-type card1 - dc1 - {:cols [{:base_type :type/Temporal} - {:base_type :type/Number}] - :rows [[#t "2020" 2] - [#t "2021" 3]]})))))) + (channel.render/detect-pulse-chart-type card1 + dc1 + {:cols [{:base_type :type/Temporal} + {:base_type :type/Number}] + :rows [[#t "2020" 2] + [#t "2021" 3]]})))))) (deftest ^:parallel make-description-if-needed-test (testing "Use Visualization Settings's description if it exists" @@ -191,7 +191,7 @@ DashboardCard dc1 {:dashboard_id (:id dashboard) :card_id (:id card) :visualization_settings {:card.description "Visualization description"}}] (is (= "<p>Visualization description</p>\n" - (last (:content (#'render/make-description-if-needed dc1 card {:channel.render/include-description? true})))))))) + (last (:content (#'channel.render.card/make-description-if-needed dc1 card {:channel.render/include-description? true})))))))) (deftest ^:parallel make-description-if-needed-test-2 (testing "Fallback to Card's description if Visualization Settings's description not exists" @@ -199,7 +199,7 @@ Dashboard dashboard {} DashboardCard dc1 {:dashboard_id (:id dashboard) :card_id (:id card)}] (is (= "<p>Card description</p>\n" - (last (:content (#'render/make-description-if-needed dc1 card {:channel.render/include-description? true})))))))) + (last (:content (#'channel.render.card/make-description-if-needed dc1 card {:channel.render/include-description? true})))))))) (deftest ^:parallel make-description-if-needed-test-3 (testing "Test markdown converts to html" @@ -207,7 +207,7 @@ Dashboard dashboard {} DashboardCard dc1 {:dashboard_id (:id dashboard) :card_id (:id card)}] (is (= "<h1>Card description</h1>\n" - (last (:content (#'render/make-description-if-needed dc1 card {:channel.render/include-description? true})))))))) + (last (:content (#'channel.render.card/make-description-if-needed dc1 card {:channel.render/include-description? true})))))))) (deftest ^:parallel table-rendering-of-percent-types-test (testing "If a column is marked as a :type/Percentage semantic type it should render as a percent" @@ -251,7 +251,7 @@ (let [expected (mapv (fn [row] (format "%.2f%%" (* 100 (peek row)))) (get-in query-results [:data :rows])) - rendered-card (render/render-pulse-card :inline (pulse/defaulted-timezone card) card nil query-results) + rendered-card (channel.render/render-pulse-card :inline (channel.render/defaulted-timezone card) card nil query-results) doc (hiccup->hickory (:content rendered-card)) rows (hik.s/select (hik.s/tag :tr) doc) tax-rate-col 2] @@ -274,11 +274,11 @@ (mt/with-temp [Card card {:name "A Card" :dataset_query (mt/mbql-query venues {:limit 1})}] (mt/with-temp-env-var-value! [mb-site-url "https://mb.com"] - (let [rendered-card-content (:content (render/render-pulse-card :inline - (pulse/defaulted-timezone card) - card - nil - (qp/process-query (:dataset_query card)) - {:channel.render/include-title? true}))] + (let [rendered-card-content (:content (channel.render/render-pulse-card :inline + (channel.render/defaulted-timezone card) + card + nil + (qp/process-query (:dataset_query card)) + {:channel.render/include-title? true}))] (is (some? (lib.util.match/match-one rendered-card-content [:a (_ :guard #(= (format "https://mb.com/question/%d" (:id card)) (:href %))) "A Card"])))))))) diff --git a/test/metabase/channel/render/color_test.clj b/test/metabase/channel/render/js/color_test.clj similarity index 53% rename from test/metabase/channel/render/color_test.clj rename to test/metabase/channel/render/js/color_test.clj index 6e5d35ecf05d25f433e553d6110e6a36a0c01bde..1db7c38780ddae1fa341c7adb58f03097073d5de 100644 --- a/test/metabase/channel/render/color_test.clj +++ b/test/metabase/channel/render/js/color_test.clj @@ -1,8 +1,8 @@ -(ns metabase.channel.render.color-test +(ns metabase.channel.render.js.color-test (:require [clojure.test :refer :all] - [metabase.channel.render.color :as color] - [metabase.channel.render.js-engine :as js] + [metabase.channel.render.js.color :as js.color] + [metabase.channel.render.js.engine :as js.engine] [metabase.test :as mt])) (def ^:private red "#ff0000") @@ -26,43 +26,43 @@ "Setup a javascript engine with a stubbed script useful making sure `get-background-color` works independently from the real color picking script" [script & body] - `(with-redefs [color/js-engine (let [delay# (delay (doto (js/context) - (js/load-js-string ~script ~(name (gensym "color-src")))))] - (fn [] @delay#))] + `(with-redefs [js.color/js-engine (let [delay# (delay (doto (js.engine/context) + (js.engine/load-js-string ~script ~(name (gensym "color-src")))))] + (fn [] @delay#))] ~@body)) (deftest color-test (testing "The test script above should return red on even rows, green on odd rows" (with-test-js-engine! test-script - (let [color-selector (color/make-color-selector {:cols [{:name "test"}] - :rows [[1] [2] [3] [4]]} - {"even" red, "odd" green})] + (let [color-selector (js.color/make-color-selector {:cols [{:name "test"}] + :rows [[1] [2] [3] [4]]} + {"even" red, "odd" green})] (is (= [red green red green] (for [row-index (range 0 4)] - (color/get-background-color color-selector "any value" "any column" row-index)))))))) + (js.color/get-background-color color-selector "any value" "any column" row-index)))))))) (deftest convert-keywords-test (testing (str "Same test as above, but make sure we convert any keywords as keywords don't get converted to " "strings automatically when passed to a JavaScript function") (with-test-js-engine! test-script - (let [color-selector (color/make-color-selector {:cols [{:name "test"}] - :rows [[1] [2] [3] [4]]} - {:even red, :odd green})] + (let [color-selector (js.color/make-color-selector {:cols [{:name "test"}] + :rows [[1] [2] [3] [4]]} + {:even red, :odd green})] (is (= [red green red green] (for [row-index (range 0 4)] - (color/get-background-color color-selector "any value" "any column" row-index)))))))) + (js.color/get-background-color color-selector "any value" "any column" row-index)))))))) (deftest render-color-is-thread-safe-test (is (every? some? (mt/repeat-concurrently 3 (fn [] - (color/get-background-color (color/make-color-selector {:cols [{:name "test"}] - :rows [[5] [5]]} - {:table.column_formatting [{:columns ["test"], - :type :single, - :operator "=", - :value 5, - :color "#ff0000", - :highlight_row true}]}) - "any value" "test" 1)))))) + (js.color/get-background-color (js.color/make-color-selector {:cols [{:name "test"}] + :rows [[5] [5]]} + {:table.column_formatting [{:columns ["test"], + :type :single, + :operator "=", + :value 5, + :color "#ff0000", + :highlight_row true}]}) + "any value" "test" 1)))))) diff --git a/test/metabase/channel/render/js_engine_test.clj b/test/metabase/channel/render/js/engine_test.clj similarity index 92% rename from test/metabase/channel/render/js_engine_test.clj rename to test/metabase/channel/render/js/engine_test.clj index 7219b39b2ccf85e4f5175c14f6f49b5a70192d2c..43b9a8a4fa158e2f4411db1fa081bf7ce4dd8626 100644 --- a/test/metabase/channel/render/js_engine_test.clj +++ b/test/metabase/channel/render/js/engine_test.clj @@ -1,7 +1,7 @@ -(ns metabase.channel.render.js-engine-test +(ns metabase.channel.render.js.engine-test (:require [clojure.test :refer :all] - [metabase.channel.render.js-engine :as js] + [metabase.channel.render.js.engine :as js] [metabase.test :as mt])) (set! *warn-on-reflection* true) diff --git a/test/metabase/channel/render/js_svg_test.clj b/test/metabase/channel/render/js/svg_test.clj similarity index 89% rename from test/metabase/channel/render/js_svg_test.clj rename to test/metabase/channel/render/js/svg_test.clj index e4d191642ae08499cb1fa5df55186b21c83e3cbd..3de94769df30921fe9ead20a79dc23b01ccdf201 100644 --- a/test/metabase/channel/render/js_svg_test.clj +++ b/test/metabase/channel/render/js/svg_test.clj @@ -1,4 +1,4 @@ -(ns ^:mb/once metabase.channel.render.js-svg-test +(ns ^:mb/once metabase.channel.render.js.svg-test "Testing of the svgs produced by the graal js engine and the static-viz bundle. The model is query-results -> js engine with bundle -> svg-string -> svg png renderer @@ -9,8 +9,8 @@ [cheshire.core :as json] [clojure.set :as set] [clojure.test :refer :all] - [metabase.channel.render.js-engine :as js] - [metabase.channel.render.js-svg :as js-svg]) + [metabase.channel.render.js.engine :as js.engine] + [metabase.channel.render.js.svg :as js.svg]) (:import (org.apache.batik.anim.dom SVGOMDocument) (org.graalvm.polyglot Context Value) @@ -18,7 +18,7 @@ (set! *warn-on-reflection* true) -(def parse-svg #'js-svg/parse-svg-string) +(def parse-svg #'js.svg/parse-svg-string) (use-fixtures :each (fn warn-possible-rebuild @@ -29,7 +29,7 @@ (deftest post-process-test (let [svg "<svg xmlns=\"http://www.w3.org/2000/svg\"><g><line/></g><g><rect/></g><g><circle/></g></svg>" nodes (atom [])] - (#'js-svg/post-process (parse-svg svg) + (#'js.svg/post-process (parse-svg svg) (fn [^Node node] (swap! nodes conj (.getNodeName node)))) (is (= ["svg" "g" "line" "g" "rect" "g" "circle"] @nodes)))) @@ -45,12 +45,12 @@ (is (= (.getAttribute line "fill") "transparent")) ;; unfortunately these objects are mutable. It does return the line but want to emphasize that is works by ;; mutation - (#'js-svg/fix-fill line) + (#'js.svg/fix-fill line) (is (not (.hasAttribute line "fill"))) (is (.hasAttribute line "fill-opacity")) (is (= (.getAttribute line "fill-opacity") "0.0")))) -(def ^Context context (#'js-svg/context)) +(def ^Context context (#'js.svg/context)) (defn document-tag-seq [^SVGOMDocument document] (map #(.getNodeName ^Node %) @@ -83,10 +83,10 @@ goal 1337 settings {:color "#333333"}] (testing "It returns bytes" - (let [svg-bytes (js-svg/progress value goal settings)] + (let [svg-bytes (js.svg/progress value goal settings)] (is (bytes? svg-bytes)))) (let [svg-string (.asString ^Value - (js/execute-fn-name + (js.engine/execute-fn-name context "progress" (json/generate-string {:value value :goal goal}) @@ -96,5 +96,5 @@ (deftest parse-svg-sanitizes-characters-test (testing "Characters discouraged or not permitted by the xml 1.0 specification are removed. (#" - (#'js-svg/parse-svg-string + (#'js.svg/parse-svg-string "<svg xmlns=\"http://www.w3.org/2000/svg\">\u001F</svg>"))) diff --git a/test/metabase/channel/render/table_test.clj b/test/metabase/channel/render/table_test.clj index 5ad9015fe4111c7a4b25628a1171800310ee2c51..23acd98e3cf6a3b2ff76644562e486601485eada 100644 --- a/test/metabase/channel/render/table_test.clj +++ b/test/metabase/channel/render/table_test.clj @@ -2,8 +2,8 @@ (:require [clojure.test :refer :all] [hickory.select :as hik.s] - [metabase.channel.render.color :as color] - [metabase.channel.render.core :as render] + [metabase.channel.render.core :as channel.render] + [metabase.channel.render.js.color :as js.color] [metabase.channel.render.table :as table] [metabase.formatter :as formatter] [metabase.pulse.render.test-util :as render.tu] @@ -81,7 +81,7 @@ "9" "rgba(0, 0, 255, 0.75)" "1.001,5" "rgba(0, 0, 255, 0.75)" "1,001.5" "rgba(0, 0, 255, 0.75)"} - (-> (color/make-color-selector query-results (:visualization_settings render.tu/test-card)) + (-> (js.color/make-color-selector query-results (:visualization_settings render.tu/test-card)) (#'table/render-table 0 {:col-names ["a" "b" "c"] :cols-for-color-lookup ["a" "b" "c"]} (query-results->header+rows query-results)) find-table-body @@ -156,7 +156,7 @@ :visibility_type :sensitive :semantic_type nil}] :rows [[1 2]]}} - hiccup-render (:content (render/render-pulse-card :attachment "UTC" card nil data-map)) + hiccup-render (:content (channel.render/render-pulse-card :attachment "UTC" card nil data-map)) header-els (render.tu/nodes-with-tag hiccup-render :th)] (is (= ["A"] (map last header-els)))))) @@ -172,7 +172,7 @@ (mapv (fn [row-el] (mapcat :content (:content row-el))) row-els))))))))) (defn- render-table [dashcard results] - (render/render-pulse-card :attachment "America/Los_Angeles" render.tu/test-card dashcard results)) + (channel.render/render-pulse-card :attachment "America/Los_Angeles" render.tu/test-card dashcard results)) (deftest attachment-rows-limit-test (doseq [[test-explanation env-var-value expected] diff --git a/test/metabase/pulse/render/test_util.clj b/test/metabase/pulse/render/test_util.clj index 25b4a523bedbe2ee9b36625d348b4da048f821db..7fa12b51f80f689850edf2922241fe233de5a6ab 100644 --- a/test/metabase/pulse/render/test_util.clj +++ b/test/metabase/pulse/render/test_util.clj @@ -9,9 +9,9 @@ [clojure.zip :as zip] [hiccup.core :as hiccup] [hickory.core :as hik] - [metabase.channel.render.core :as render] + [metabase.channel.render.core :as channel.render] [metabase.channel.render.image-bundle :as image-bundle] - [metabase.channel.render.js-svg :as js-svg] + [metabase.channel.render.js.svg :as js.svg] [metabase.notification.payload.execute :as notification.execute] [metabase.query-processor :as qp] [metabase.query-processor.card :as qp.card] @@ -97,7 +97,7 @@ (= tag :img) (str/starts-with? src "<svg")))) -(def ^:private parse-svg #'js-svg/parse-svg-string) +(def ^:private parse-svg #'js.svg/parse-svg-string) (defn- img-node->svg-node "Modifies an intentionally malformed [:img {:src \"<svg>...</svg>\"}] node by parsing the svg string and replacing the @@ -142,11 +142,11 @@ (let [{:keys [visualization_settings] :as card} (t2/select-one :model/Card :id card-id) query (qp.card/query-for-card card [] nil {:process-viz-settings? true} nil) results (qp/process-query (assoc query :viz-settings visualization_settings))] - (with-redefs [js-svg/svg-string->bytes identity + (with-redefs [js.svg/svg-string->bytes identity image-bundle/make-image-bundle (fn [_ s] {:image-src s :render-type :inline})] - (let [content (-> (render/render-pulse-card :inline "UTC" card nil results) + (let [content (-> (channel.render/render-pulse-card :inline "UTC" card nil results) :content)] (-> content (edit-nodes img-node-with-svg? img-node->svg-node) ;; replace the :img tag with its parsed SVG. @@ -162,11 +162,11 @@ (let [{:keys [visualization_settings] :as card} (t2/select-one :model/Card :id card-id) query (qp.card/query-for-card card [] nil {:process-viz-settings? true} nil) results (qp.pivot/run-pivot-query (assoc query :viz-settings visualization_settings))] - (with-redefs [js-svg/svg-string->bytes identity + (with-redefs [js.svg/svg-string->bytes identity image-bundle/make-image-bundle (fn [_ s] {:image-src s :render-type :inline})] - (let [content (-> (render/render-pulse-card :inline "UTC" card nil results) + (let [content (-> (channel.render/render-pulse-card :inline "UTC" card nil results) :content)] (-> content (edit-nodes img-node-with-svg? img-node->svg-node) ;; replace the :img tag with its parsed SVG. @@ -183,11 +183,11 @@ (let [dashcard (t2/select-one :model/DashboardCard :id dashcard-id) card (t2/select-one :model/Card :id (:card_id dashcard)) {:keys [result dashcard]} (notification.execute/execute-dashboard-subscription-card dashcard parameters)] - (with-redefs [js-svg/svg-string->bytes identity + (with-redefs [js.svg/svg-string->bytes identity image-bundle/make-image-bundle (fn [_ s] {:image-src s :render-type :inline})] - (let [content (-> (render/render-pulse-card :inline "UTC" card dashcard result) + (let [content (-> (channel.render/render-pulse-card :inline "UTC" card dashcard result) :content)] (-> content (edit-nodes img-node-with-svg? img-node->svg-node) ;; replace the :img tag with its parsed SVG. diff --git a/test/metabase/pulse/send_test.clj b/test/metabase/pulse/send_test.clj index f3e4776d6e5db7717b58e0cfd8e46422fc84de5d..dcdfae8969cf843f4ea313e12ec61f7a70ce7784 100644 --- a/test/metabase/pulse/send_test.clj +++ b/test/metabase/pulse/send_test.clj @@ -1,6 +1,7 @@ (ns metabase.pulse.send-test "These are mostly Alerts test, dashboard subscriptions could be found in [[metabase.dashboard-subscription-test]]." + #_{:clj-kondo/ignore [:deprecated-namespace]} (:require [clojure.java.io :as io] [clojure.string :as str] @@ -8,7 +9,7 @@ [metabase.channel.core :as channel] [metabase.channel.http-test :as channel.http-test] [metabase.channel.render.body :as body] - [metabase.channel.render.core :as render] + [metabase.channel.render.core :as channel.render] [metabase.email :as email] [metabase.integrations.slack :as slack] [metabase.models :refer [Card Collection Pulse PulseCard PulseChannel PulseChannelRecipient]] @@ -167,7 +168,7 @@ (defn- produces-bytes? [{:keys [rendered-info]}] (when rendered-info - (pos? (alength (or (render/png-from-render-info rendered-info 500) + (pos? (alength (or (channel.render/png-from-render-info rendered-info 500) (byte-array 0)))))) ;;; +----------------------------------------------------------------------------------------------------------------+ diff --git a/test/metabase/query_processor/middleware/update_used_cards_test.clj b/test/metabase/query_processor/middleware/update_used_cards_test.clj index 69ed33a9731d90a537d7814a6fa91acab55a2e86..25976b165a69a88c75e5995689644d5f63cc53c4 100644 --- a/test/metabase/query_processor/middleware/update_used_cards_test.clj +++ b/test/metabase/query_processor/middleware/update_used_cards_test.clj @@ -1,4 +1,5 @@ (ns metabase.query-processor.middleware.update-used-cards-test + #_{:clj-kondo/ignore [:deprecated-namespace]} (:require [clojure.test :refer :all] [java-time.api :as t]