mirror of
https://github.com/logseq/logseq.git
synced 2026-04-24 14:14:55 +00:00
211 lines
10 KiB
Clojure
211 lines
10 KiB
Clojure
(ns frontend.components.server
|
|
(:require
|
|
[clojure.string :as string]
|
|
[electron.ipc :as ipc]
|
|
[frontend.handler.notification :as notification]
|
|
[frontend.state :as state]
|
|
[frontend.ui :as ui]
|
|
[frontend.util :as util]
|
|
[logseq.shui.hooks :as hooks]
|
|
[logseq.shui.ui :as shui]
|
|
[medley.core :as medley]
|
|
[promesa.core :as p]
|
|
[rum.core :as rum]))
|
|
|
|
(rum/defcs panel-of-tokens
|
|
< rum/reactive
|
|
(rum/local nil ::tokens)
|
|
{:will-mount
|
|
(fn [s]
|
|
(let [*tokens (s ::tokens)]
|
|
(reset! *tokens (get-in @state/state [:electron/server :tokens])) s))}
|
|
[_state close-panel]
|
|
|
|
(let [server-state (state/sub :electron/server)
|
|
*tokens (::tokens _state)
|
|
changed? (not= @*tokens (:tokens server-state))]
|
|
[:div.cp__server-tokens-panel.pt-6
|
|
[:h2.text-3xl.-translate-y-4 "Authorization tokens"]
|
|
;; items
|
|
(let [update-value! (fn [idx k v] (swap! *tokens assoc-in [idx k] v))]
|
|
(for [[idx {:keys [value name]}] (medley/indexed @*tokens)]
|
|
[:div.item.py-2.flex.space-x-2.items-center
|
|
{:key idx}
|
|
[:input.form-input.basis-36
|
|
{:auto-focus true
|
|
:placeholder "name"
|
|
:value name
|
|
:on-change #(let [value (.-value (.-target %))]
|
|
(update-value! idx :name value))}]
|
|
[:input.form-input
|
|
{:placeholder "value"
|
|
:value value
|
|
:on-change #(let [value (.-value (.-target %))]
|
|
(update-value! idx :value value))}]
|
|
|
|
[:button.px-2.opacity-50.hover:opacity-90.active:opacity-100
|
|
{:on-click #(let [new-token (util/unique-id)
|
|
^js input-el (some-> (.-target %) (.closest ".item") (.querySelector "input.form-input:nth-child(2)"))]
|
|
(update-value! idx :value new-token)
|
|
(when input-el
|
|
(js/setTimeout (fn [] (.select input-el)) 64)))
|
|
:title "Regenerate token value"}
|
|
[:span.flex.items-center (ui/icon "refresh")]]
|
|
[:button.px-2.opacity-50.hover:opacity-90.active:opacity-100
|
|
{:on-click #(reset! *tokens (into [] (medley/remove-nth idx @*tokens)))}
|
|
[:span.flex.items-center (ui/icon "trash-x")]]]))
|
|
|
|
[:p.flex.justify-end.pt-6.space-x-3
|
|
(ui/button "+ Add new token"
|
|
:on-click #(swap! *tokens conj {})
|
|
:variant :outline)
|
|
(ui/button "Save"
|
|
:on-click (fn [] (-> (ipc/ipc :server/set-config {:tokens @*tokens})
|
|
(p/then #(notification/show! "Update tokens successfully!" :success))
|
|
(p/catch #(js/console.error %))
|
|
(p/finally #(close-panel))))
|
|
:disabled (not changed?))]]))
|
|
|
|
(rum/defcs panel-of-configs
|
|
< rum/reactive
|
|
(rum/local nil ::configs)
|
|
{:will-mount
|
|
(fn [s]
|
|
(let [*configs (s ::configs)]
|
|
(reset! *configs (:electron/server @state/state)) s))}
|
|
[_state close-panel]
|
|
|
|
(let [server-state (state/sub :electron/server)
|
|
*configs (::configs _state)
|
|
{:keys [host port autostart]} @*configs
|
|
hp-changed? (or (not= host (:host server-state))
|
|
(not= (util/safe-parse-int (or port 0))
|
|
(util/safe-parse-int (or (:port server-state) 0))))
|
|
changed? (or hp-changed? (->> [autostart (:autostart server-state)]
|
|
(mapv #(cond-> % (nil? %) boolean))
|
|
(apply not=)))]
|
|
|
|
[:div.cp__server-configs-panel.pt-5
|
|
[:h2.text-3xl.-translate-y-4 "Server configurations"]
|
|
|
|
[:div.item.flex.items-center.space-x-3
|
|
[:label.basis-96
|
|
[:strong "Host"]
|
|
[:input.form-input
|
|
{:value host
|
|
:on-change #(let [value (.-value (.-target %))]
|
|
(swap! *configs assoc :host value))}]]
|
|
|
|
[:label
|
|
[:strong "Port (1 ~ 65535)"]
|
|
[:input.form-input
|
|
{:auto-focus true
|
|
:value port
|
|
:min "1"
|
|
:max "65535"
|
|
:type "number"
|
|
:on-change #(let [value (.-value (.-target %))]
|
|
(swap! *configs assoc :port value))}]]]
|
|
|
|
[:p.py-3.px-1
|
|
[:label.flex.space-x-2.items-center
|
|
(ui/checkbox
|
|
{:on-change #(let [checked (.-checked (.-target %))]
|
|
(swap! *configs assoc :autostart checked))
|
|
:checked (not (false? autostart))})
|
|
|
|
[:strong.select-none "Auto start server with the app launched"]]]
|
|
|
|
[:p.flex.justify-end.pt-6.space-x-3
|
|
(ui/button "Reset" :variant :outline
|
|
:on-click #(reset! *configs (select-keys server-state [:host :port :autostart])))
|
|
(ui/button "Save & Apply"
|
|
:disabled (not changed?)
|
|
:on-click (fn []
|
|
(let [configs (select-keys @*configs [:host :port :autostart])]
|
|
(-> (ipc/ipc :server/set-config configs)
|
|
(p/then #(p/let [_ (close-panel)
|
|
_ (p/delay 1000)]
|
|
(when hp-changed?
|
|
(ipc/ipc :server/do :restart))))
|
|
(p/catch #(notification/show! (str %) :error))))))]]))
|
|
|
|
(rum/defc server-indicator
|
|
[server-state]
|
|
|
|
(hooks/use-effect!
|
|
(fn []
|
|
(p/let [_ (p/delay 1000)
|
|
_ (ipc/ipc :server/load-state)]
|
|
(let [t (js/setTimeout #(when (state/sub [:electron/server :autostart])
|
|
(ipc/ipc :server/do :restart)) 1000)]
|
|
#(js/clearTimeout t))))
|
|
[])
|
|
|
|
(let [{:keys [status error mcp-enabled?]} server-state
|
|
status (keyword (util/safe-lower-case status))
|
|
running? (= :running status)
|
|
href (and running? (str "http://" (:host server-state) ":" (:port server-state)))]
|
|
|
|
(hooks/use-effect!
|
|
#(when error
|
|
(notification/show! (str "[Server] " error) :error))
|
|
[error])
|
|
|
|
[:div.cp__server-indicator
|
|
(shui/button-ghost-icon (if running? "api" "api-off")
|
|
{:on-click (fn [^js e]
|
|
(shui/popup-show!
|
|
(.-target e)
|
|
(fn [{:keys [_close]}]
|
|
(let [items [{:hr? true}
|
|
|
|
(cond
|
|
running?
|
|
{:title "Stop server"
|
|
:options {:on-click #(ipc/ipc :server/do :stop)}
|
|
:icon [:span.text-red-500.flex.items-center (ui/icon "player-stop")]}
|
|
|
|
:else
|
|
{:title "Start server"
|
|
:options {:on-click #(ipc/ipc :server/do :restart)}
|
|
:icon [:span.text-green-500.flex.items-center (ui/icon "player-play")]})
|
|
|
|
{:title "Authorization tokens"
|
|
:options {:on-click #(shui/dialog-open!
|
|
(fn []
|
|
(panel-of-tokens shui/dialog-close!)))}
|
|
:icon (ui/icon "key")}
|
|
|
|
{:title "Server configurations"
|
|
:options {:on-click #(shui/dialog-open!
|
|
(fn []
|
|
(panel-of-configs shui/dialog-close!)))}
|
|
:icon (ui/icon "server-cog")}]]
|
|
|
|
(cons
|
|
[:div.links-header.flex.justify-center.py-2
|
|
[:span.ml-1.text-sm.opacity-70
|
|
(if-not running?
|
|
(string/upper-case (or (:status server-state) "stopped"))
|
|
[:span.flex.flex-col.gap-1.text-xs.font-mono
|
|
[:a.hover:underline.flex.items-center {:href href}
|
|
href (shui/tabler-icon "external-link" {:size 12 :class "inline-block ml-1 pt-[1px]"})]
|
|
(when mcp-enabled?
|
|
[:a.hover:underline.flex.items-center
|
|
{:on-click (fn []
|
|
(util/copy-to-clipboard! (str href "/mcp"))
|
|
(notification/show! "MCP URL copied to clipboard!" :success))}
|
|
(str href "/mcp")
|
|
(shui/tabler-icon "copy" {:size 12 :class "inline-block ml-1 mt-[1px]"})])])]]
|
|
(for [{:keys [hr? title options icon]} items]
|
|
(cond
|
|
hr?
|
|
(shui/dropdown-menu-separator)
|
|
|
|
:else
|
|
(shui/dropdown-menu-item options
|
|
[:span.flex.items-center icon [:span.pl-1 title]]))))))
|
|
{:as-dropdown? true
|
|
:content-props {:onClick #(shui/popup-hide!)}}))})]))
|