;; This Source Code Form is subject to the terms of the Mozilla Public ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; ;; Copyright (c) KALEIDOS INC (ns app.main.ui.static (:require-macros [app.main.style :as stl]) (:require ["rxjs" :as rxjs] [app.common.data :as d] [app.common.pprint :as pp] [app.common.uri :as u] [app.common.uuid :as uuid] [app.config :as cf] [app.main.data.auth :refer [is-authenticated?]] [app.main.data.common :as dcm] [app.main.data.event :as ev] [app.main.errors :as errors] [app.main.refs :as refs] [app.main.repo :as rp] [app.main.router :as rt] [app.main.store :as st] [app.main.ui.auth.login :refer [login-dialog*]] [app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]] [app.main.ui.auth.register :as register] [app.main.ui.dashboard.sidebar :refer [sidebar*]] [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]] [app.main.ui.icons :as deprecated-icon] [app.main.ui.viewer.header :as viewer.header] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [app.util.timers :as tm] [app.util.webapi :as wapi] [beicon.v2.core :as rx] [cuerdas.core :as str] [potok.v2.core :as ptk] [rumext.v2 :as mf])) ;; FIXME: this is a workaround until we export this class on beicon library (def TimeoutError rxjs/TimeoutError) (mf/defc error-container* {::mf/props :obj} [{:keys [children]}] (let [profile-id (:profile-id @st/state) on-nav-root (mf/use-fn #(st/emit! (rt/nav-root)))] [:section {:class (stl/css :exception-layout)} [:button {:class (stl/css :exception-header) :on-click on-nav-root} [:> raw-svg* {:id "penpot-logo-icon" :class (stl/css :penpot-logo)}] (when profile-id [:div {:class (stl/css :go-back-wrapper)} [:> icon* {:icon-id i/arrow :class (stl/css :back-arrow)}] [:span (tr "not-found.no-permission.go-dashboard")]])] [:div {:class (stl/css :deco-before)} deprecated-icon/logo-error-screen] (when-not profile-id [:button {:class (stl/css :login-header) :on-click on-nav-root} (tr "labels.login")]) [:div {:class (stl/css :exception-content)} [:div {:class (stl/css :container)} children]] [:div {:class (stl/css :deco-after2)} [:span (tr "labels.copyright-period")] deprecated-icon/logo-error-screen [:span (tr "not-found.made-with-love")]]])) (mf/defc invalid-token [] [:> error-container* {} [:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")] [:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]]) (mf/defc login-modal* {::mf/private true} [] (let [current-section (mf/use-state :login) user-email (mf/use-state "") register-token (mf/use-state "") set-section (mf/use-fn (fn [event] (let [section (-> (dom/get-current-target event) (dom/get-data "section") (keyword))] (reset! current-section section)))) set-section-recovery (mf/use-fn #(reset! current-section :recovery-request)) set-section-login (mf/use-fn #(reset! current-section :login)) success-login (mf/use-fn #(st/emit! (rt/reload true))) success-register (mf/use-fn (fn [data] (reset! register-token (:token data)) (reset! current-section :register-validate))) register-email-sent (mf/use-fn (fn [email] (reset! user-email email) (reset! current-section :register-email-sent))) recovery-email-sent (mf/use-fn (fn [email] (reset! user-email email) (reset! current-section :recovery-email-sent))) on-nav-root (mf/use-fn #(st/emit! (rt/nav :auth-login {})))] [:div {:class (stl/css :overlay)} [:div {:class (stl/css :dialog-login)} [:div {:class (stl/css :modal-close)} [:button {:class (stl/css :modal-close-button) :on-click on-nav-root} deprecated-icon/close]] [:div {:class (stl/css :login)} [:div {:class (stl/css :logo)} deprecated-icon/logo] (case @current-section :login [:* [:div {:class (stl/css :logo-title)} (tr "labels.login")] [:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.free")] [:> login-dialog* {:on-recovery-request set-section-recovery :on-success-callback success-login :handle-redirect true}] [:hr {:class (stl/css :separator)}] [:div {:class (stl/css :change-section)} (tr "auth.register") " " [:a {:data-section "register" :on-click set-section} (tr "auth.register-submit")]]] :register [:* [:div {:class (stl/css :logo-title)} (tr "not-found.login.signup-free")] [:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.start-using")] [:& register/register-methods {:on-success-callback success-register :hide-separator true}] #_[:hr {:class (stl/css :separator)}] [:div {:class (stl/css :separator)}] [:div {:class (stl/css :change-section)} (tr "auth.already-have-account") " " [:a {:data-section "login" :on-click set-section} (tr "auth.login-here")]] [:div {:class (stl/css :links)} [:hr {:class (stl/css :separator)}] [:& register/terms-register]]] :register-validate [:div {:class (stl/css :form-container)} [:& register/register-form {:params {:token @register-token} :on-success-callback register-email-sent}] [:div {:class (stl/css :links)} [:div {:class (stl/css :register)} [:a {:data-section "register" :on-click set-section} (tr "labels.go-back")]]]] :register-email-sent [:div {:class (stl/css :form-container)} [:& register/register-success-page {:params {:email @user-email :hide-logo true}}]] :recovery-request [:& recovery-request-page {:go-back-callback set-section-login :on-success-callback recovery-email-sent}] :recovery-email-sent [:div {:class (stl/css :form-container)} [:& recovery-sent-page {:email @user-email}]])]]])) (mf/defc request-dialog* {::mf/props :obj} [{:keys [title content button-text on-button-click cancel-text on-close]}] (let [on-click (or on-button-click on-close)] [:div {:class (stl/css :overlay)} [:div {:class (stl/css :dialog)} [:div {:class (stl/css :modal-close)} [:button {:class (stl/css :modal-close-button) :on-click on-close} deprecated-icon/close]] [:div {:class (stl/css :dialog-title)} title] (for [[index content] (d/enumerate content)] [:div {:key index} content]) [:div {:class (stl/css :sign-info)} (when cancel-text [:button {:class (stl/css :cancel-button) :on-click on-close} cancel-text]) [:button {:on-click on-click} button-text]]]])) (mf/defc request-access* [{:keys [file-id team-id is-default is-workspace profile]}] (let [requested* (mf/use-state {:sent false :already-requested false}) requested (deref requested*) on-close (mf/use-fn (mf/deps profile) (fn [] (let [team-id (:default-team-id profile)] (st/emit! (dcm/go-to-dashboard-recent :team-id team-id))))) on-success (mf/use-fn #(reset! requested* {:sent true :already-requested false})) on-error (mf/use-fn #(reset! requested* {:sent true :already-requested true})) on-request-access (mf/use-fn (mf/deps file-id team-id is-workspace) (fn [] (let [params (if (some? file-id) {:file-id file-id :is-viewer (not is-workspace)} {:team-id team-id}) mdata {:on-success on-success :on-error on-error}] (st/emit! (dcm/create-team-access-request (with-meta params mdata))))))] (cond is-default [:> request-dialog* {:title (tr "not-found.no-permission.project") :button-text (tr "not-found.no-permission.go-dashboard") :on-close on-close}] (and (some? file-id) (:already-requested requested)) [:> request-dialog* {:title (tr "not-found.no-permission.already-requested.file") :content [(tr "not-found.no-permission.already-requested.or-others.file")] :button-text (tr "not-found.no-permission.go-dashboard") :on-close on-close}] (:already-requested requested) [:> request-dialog* {:title (tr "not-found.no-permission.already-requested.project") :content [(tr "not-found.no-permission.already-requested.or-others.project")] :button-text (tr "not-found.no-permission.go-dashboard") :on-close on-close}] (:sent requested) [:> request-dialog* {:title (tr "not-found.no-permission.done.success") :content [(tr "not-found.no-permission.done.remember")] :button-text (tr "not-found.no-permission.go-dashboard") :on-close on-close}] (some? file-id) [:> request-dialog* {:title (tr "not-found.no-permission.file") :content [(tr "not-found.no-permission.you-can-ask.file") (tr "not-found.no-permission.if-approves")] :button-text (tr "not-found.no-permission.ask") :on-button-click on-request-access :cancel-text (tr "not-found.no-permission.go-dashboard") :on-close on-close}] (some? team-id) [:> request-dialog* {:title (tr "not-found.no-permission.project") :content [(tr "not-found.no-permission.you-can-ask.project") (tr "not-found.no-permission.if-approves")] :button-text (tr "not-found.no-permission.ask") :on-button-click on-request-access :cancel-text (tr "not-found.no-permission.go-dashboard") :on-close on-close}]))) (mf/defc not-found* [] [:> error-container* {} [:div {:class (stl/css :main-message)} (tr "labels.not-found.main-message")] [:div {:class (stl/css :desc-message)} (tr "not-found.desc-message.error")] [:div {:class (stl/css :desc-message)} (tr "not-found.desc-message.doesnt-exist")]]) (mf/defc bad-gateway* [] (let [handle-retry (mf/use-fn (fn [] (st/emit! (rt/assign-exception nil))))] [:> error-container* {} [:div {:class (stl/css :main-message)} (tr "labels.bad-gateway.main-message")] [:div {:class (stl/css :desc-message)} (tr "labels.bad-gateway.desc-message")] [:div {:class (stl/css :sign-info)} [:button {:on-click handle-retry} (tr "labels.retry")]]])) (mf/defc service-unavailable* [] (let [on-click (mf/use-fn #(st/emit! (rt/assign-exception nil)))] [:> error-container* {} [:div {:class (stl/css :main-message)} (tr "labels.service-unavailable.main-message")] [:div {:class (stl/css :desc-message)} (tr "labels.service-unavailable.desc-message")] [:div {:class (stl/css :sign-info)} [:button {:on-click on-click} (tr "labels.retry")]]])) (mf/defc webgl-context-lost* [] (let [on-reload (mf/use-fn #(js/location.reload))] [:> error-container* {} [:div {:class (stl/css :main-message)} (tr "errors.webgl-context-lost.main-message")] [:div {:class (stl/css :desc-message)} (tr "errors.webgl-context-lost.desc-message")] [:div {:class (stl/css :buttons-container)} [:> button* {:variant "primary" :on-click on-reload} (tr "labels.reload-page")]]])) (defn- generate-report [data] (try (let [team-id (:current-team-id @st/state) profile-id (:profile-id @st/state) trace (:app.main.errors/trace data) instance (:app.main.errors/instance data)] (with-out-str (println "Hint: " (or (:hint data) (ex-message instance) "--")) (println "Prof ID: " (str (or profile-id "--"))) (println "Team ID: " (str (or team-id "--"))) (println "URI: " cf/public-uri) (when-let [file-id (:file-id data)] (println "File ID:" (str file-id))) (println) (println "Data:") (loop [data data] (-> (d/without-qualified data) (dissoc :explain) (d/update-when :data (constantly "(...)")) (pp/pprint {:level 8 :length 10})) (println) (when-let [explain (:explain data)] (print explain)) (when (and (= :server-error (:type data)) (contains? data :data)) (recur (:data data)))) (println "Trace:") (println trace) (println) (println "Last events:") (pp/pprint @st/last-events {:length 200}) (println))) (catch :default cause (.error js/console "error on generating report.txt" cause) nil))) (mf/defc internal-error* [{:keys [on-reset report] :as props}] (let [report-uri (mf/use-ref nil) on-reset (or on-reset #(st/emit! (rt/assign-exception nil))) support-contact-click (mf/use-fn (mf/deps on-reset report) (fn [] (tm/schedule on-reset) (let [error-report-id (uuid/next) error-href (rt/get-current-href)] (set! errors/last-report {:id error-report-id :content report}) (st/emit! (rt/nav :settings-feedback {:type "issue" :error-report-id error-report-id :error-href error-href}))))) on-download (mf/use-fn (fn [event] (dom/prevent-default event) (when-let [uri (mf/ref-val report-uri)] (dom/trigger-download-uri "report" "text/plain" uri))))] (mf/with-effect [report] (when (some? report) (set! errors/last-report report) (let [report (wapi/create-blob report "text/plain") uri (wapi/create-uri report)] (mf/set-ref-val! report-uri uri) (fn [] (wapi/revoke-uri uri))))) [:> error-container* {} [:div {:class (stl/css :main-message)} (tr "labels.internal-error.main-message")] [:div {:class (stl/css :desc-message)} [:p {:class (stl/css :desc-text)} (tr "labels.internal-error.desc-message-first")] [:p {:class (stl/css :desc-text)} (tr "labels.internal-error.desc-message-second")]] (when (some? report) [:a {:class (stl/css :download-link) :on-click on-download} (tr "labels.download" "report.txt")]) [:div {:class (stl/css :buttons-container)} [:> button* {:variant "secondary" :type "button" :class (stl/css :support-btn) :on-click support-contact-click} (tr "labels.contact-support")] [:> button* {:variant "primary" :type "button" :class (stl/css :retry-btn) :on-click on-reset} (tr "labels.retry")]]])) (defn- load-info "Load exception page info" [path-params] (let [default {:loaded true} stream (cond (:file-id path-params) (->> (rp/cmd! :get-file-info {:id (:file-id path-params)}) (rx/map (fn [info] {:loaded true :file-id (:id info)}))) (:team-id path-params) (->> (rp/cmd! :get-team-info {:id (:team-id path-params)}) (rx/map (fn [info] {:loaded true :team-id (:id info) :team-default (:is-default info)}))) :else (rx/of default))] (->> stream (rx/timeout 3000) (rx/catch (fn [cause] (if (instance? TimeoutError cause) (rx/of default) (rx/throw cause))))))) (mf/defc exception-section* {::mf/private true} [{:keys [data route] :as props}] (let [type (get data :type) report (mf/with-memo [data] (generate-report data)) props (mf/spread-props props {:report report})] (mf/with-effect [data route report] (let [params (:query-params route) params (u/map->query-string params)] (st/emit! (ptk/data-event ::ev/event {::ev/name "exception-page" :type (get data :type :unknown) :hint (get data :hint) :path (get route :path) :report report :params params})))) (case type :not-found [:> not-found* {}] :authentication [:> not-found* {}] :bad-gateway [:> bad-gateway* props] :service-unavailable [:> service-unavailable*] :webgl-context-lost [:> webgl-context-lost*] [:> internal-error* props]))) (mf/defc context-wrapper* [{:keys [is-workspace is-dashboard is-viewer profile children]}] [:* (cond is-workspace [:div {:class (stl/css :workspace)} [:div {:class (stl/css :workspace-left)} deprecated-icon/logo-icon [:div [:div {:class (stl/css :project-name)} (tr "not-found.no-permission.project-name")] [:div {:class (stl/css :file-name)} (tr "not-found.no-permission.penpot-file")]]] [:div {:class (stl/css :workspace-right)}]] is-viewer [:div {:class (stl/css :viewer)} ;; FIXME: the viewer header was never designed to be reused ;; from other parts of the application, and this code looks ;; like a fast workaround reusing it as-is without a proper ;; component adaptation for be able to use it easily it on ;; viewer context or static error page context [:& viewer.header/header {:project {:name (tr "not-found.no-permission.project-name")} :index 0 :file {:name (tr "not-found.no-permission.penpot-file")} :page nil :frame nil :permissions {:is-logged true} :zoom 1 :section :interactions :shown-thumbnails false :interactions-mode nil}]] is-dashboard [:div {:class (stl/css :dashboard)} [:div {:class (stl/css :dashboard-sidebar)} [:> sidebar* {:team nil :projects [] :project (:default-project-id profile) :profile profile :section :dashboard-projects :search-term ""}]]]) children]) (mf/defc exception-page* {::mf/props :obj} [{:keys [data route] :as props}] (let [type (:type data) path (:path route) params (:query-params route) workspace? (str/includes? path "workspace") dashboard? (str/includes? path "dashboard") view? (str/includes? path "view") ;; We store the request access info int this state info* (mf/use-state nil) info (deref info*) profile (mf/deref refs/profile) auth-error? (= type :authentication) not-found? (= type :not-found) authenticated? (is-authenticated? profile) request-access? (and (or workspace? dashboard? view?) (or (some? (:file-id info)) (some? (:team-id info))))] (mf/with-effect [params info] (when-not (:loaded info) (->> (load-info params) (rx/subs! (partial reset! info*) (partial reset! info* {:loaded true}))))) (if (or auth-error? not-found?) (if (not authenticated?) [:> context-wrapper* {:is-workspace workspace? :is-dashboard dashboard? :is-viewer view? :profile profile} [:> login-modal* {}]] (when (get info :loaded false) (if request-access? [:> context-wrapper* {:is-workspace workspace? :is-dashboard dashboard? :is-viewer view? :profile profile} [:> request-access* {:file-id (:file-id info) :team-id (:team-id info) :is-default (:team-default info) :profile profile :is-workspace workspace?}]] [:> exception-section* props]))) [:> exception-section* props])))