Compare commits

...

3 Commits

Author SHA1 Message Date
Ngoc Khuat
524937539b tidy 2024-06-22 07:11:58 +07:00
Ngoc Khuat
e2ea96e06c correclty render png 2024-06-21 17:21:53 +07:00
Ngoc Khuat
f5cdc252c2 POC http channel 2024-06-21 15:49:39 +07:00
4 changed files with 115 additions and 9 deletions

View File

@@ -50,6 +50,15 @@
:unix (sh/sh "xdg-open" (str f)))
nil))
(defn open-png-bytes
"Given a byte array, writes it to a temporary file, then opens that file in the default application for png files."
[bytes]
(let [tmp-file (File/createTempFile "card-png" ".png")]
(with-open [w (java.io.FileOutputStream. tmp-file)]
(.write w ^bytes bytes))
(.deleteOnExit tmp-file)
(open tmp-file)))
(defn render-card-to-png
"Given a card ID, renders the card to a png and opens it. Be aware that the png rendered on a dev machine may not
match what's rendered on another system, like a docker container."
@@ -62,12 +71,8 @@
png-bytes (render/render-pulse-card-to-png (pulse/defaulted-timezone card)
card
query-results
1000)
tmp-file (File/createTempFile "card-png" ".png")]
(with-open [w (java.io.FileOutputStream. tmp-file)]
(.write w ^bytes png-bytes))
(.deleteOnExit tmp-file)
(open tmp-file)))
1000)]
(open-png-bytes png-bytes)))
(defn render-pulse-card
"Render a pulse card as a data structure"

View File

@@ -0,0 +1,91 @@
(ns metabase.channel.http
(:require
[cheshire.core :as json]
[clj-http.client :as http]
[metabase.channel.core :as channel]
[metabase.channel.shared :as channel.shared]
[metabase.models.setting :as setting]
[metabase.pulse.render :as render]
[metabase.util.i18n :refer [deferred-tru]]
[metabase.util.malli :as mu]
[metabase.util.urls :as urls]))
(def ^:private image-width
"Maximum width of the rendered PNG of HTML to be sent to HTTP Content that exceeds this width (e.g. a table with
many columns) is truncated."
1200)
(def ^:private string-or-keyword
[:or :string :keyword])
(setting/defsetting channel-http-url
(deferred-tru "The endpoint for HTTP channel")
:visibility :settings-manager)
(setting/defsetting channel-http-request-method
(deferred-tru "The request method for HTTP channel")
:visibility :settings-manager
:type :keyword
:setter (fn [value]
(if (#{:get :post :put :delete} value)
(setting/set-value-of-type! :keyword :channel-http-request-method value)
(throw (IllegalArgumentException. "Invalid request method")))))
(setting/defsetting channel-http-auth-method
(deferred-tru "Methods for authentication, Could be :none :header :query-params")
:visibility :settings-manager
:type :keyword
:setter (fn [value]
(if (#{:none :header :query-params} value)
(setting/set-value-of-type! :keyword :channel-http-auth-method value)
(throw (IllegalArgumentException. "Invalid authentication method")))))
(setting/defsetting channel-http-authentication-details
(deferred-tru "The authentication details for HTTP channel")
:visibility :settings-manager
:type :json)
(def ^:private HTTPPayLoad
[:map
[:url :string]
[:method [:enum :get :post :put :delete]]
[:query-params {:optional true} [:maybe [:map-of string-or-keyword string-or-keyword]]]
[:headers {:optional true} [:maybe [:map-of string-or-keyword string-or-keyword]]]
[:body {:optional true} :string]])
(mu/defmethod channel/send! :channel/http
[_channel-type request :- HTTPPayLoad]
(http/request (merge
{:accept :json
:content-type :json}
request)))
;; ------------------------------------------------------------------------------------------------;;
;; Alerts ;;
;; ------------------------------------------------------------------------------------------------;;
(defn- qp-result->raw-data
[qp-result]
(let [data (:data qp-result)]
{:cols (map :name (:cols data))
:rows (:rows data)}))
(mu/defmethod channel/render-notification [:channel/http :notification/alert] :- [:sequential HTTPPayLoad]
[_channel-type {:keys [card pulse payload]} _recipients]
(let [request-body {:question_id (:id card)
:question_name (:name card)
:question_url (urls/card-url (:id card))
:alert_creator_id (get-in pulse [:creator :id])
:alert_creator_name (get-in pulse [:creator :common_name])
:visualization (let [{:keys [card dashcard result]} payload]
(render/render-pulse-card-to-base64 (channel.shared/defaulted-timezone card) card dashcard result image-width))
:raw_data (qp-result->raw-data (:result payload))}]
[(merge (case (channel-http-auth-method)
:none {}
:header {:headers (channel-http-authentication-details)}
:query-params {:query-params (channel-http-authentication-details)})
{:url (channel-http-url)
:method (channel-http-request-method)
:body (json/generate-string request-body)})]))

View File

@@ -314,6 +314,10 @@
:when part]
part)))
(defn- namespaced-channel-type
[channel-type]
(keyword "channel" (name channel-type)))
(defn- send-pulse!*
[{:keys [channels channel-ids] pulse-id :id :as pulse} dashboard]
(let [parts (execute-pulse pulse dashboard)
@@ -331,9 +335,7 @@
:filters (:parameters pulse)}})
(u/prog1 (doseq [channel channels]
(try
(let [channel-type (if (= :email (keyword (:channel_type channel)))
:channel/email
:channel/slack)
(let [channel-type (namespaced-channel-type (:channel_type channel))
messages (channel/render-notification channel-type
(get-notification-info pulse parts channel)
(channels-to-channel-recipients channel))]

View File

@@ -221,7 +221,15 @@
^bytes [timezone-id :- [:maybe :string] pulse-card result width]
(png/render-html-to-png (render-pulse-card :inline timezone-id pulse-card nil result) 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))