Skip to content

Portal - navigate your dataλ︎

Practicalli Portal logo

Portal inspector is a tool for exploration of Clojure data using a browser window to visualise and inspect Clojure, JSON, Transit, Logs, Yaml, etc.

Registered Portal as a tap source and wrap code with (tap> ,,,) to see the results in Portal, providing a more advanced approach to debuging than println.

Send all evaluation results to Portal for a complete history using the portal-wrap nREPL middleware

Add a custom Mulog publisher to send all logs to Portal to help with debugging.

Open Portal from the REPL or configure Portal to open on REPL startup.

Practicalli Project Templates

Clojure projects created with Practicalli Project Templates include Portal configuration to recieve all evaluation results and Mulog event logs.

A custom dev/user.clj file loads dev/portal.clj and dev/mulog-events.clj configurations on REPL startup, when the dev directory is included on the path.

Use the :repl/reloaded for a complete REPL reloaded workflow and tooling on REPL startup

Portal - explore your Clojure data

Clojure 1.10 onward required
tap sources and tap>

tap is a shared, globally accessible system for distributing values (log, debug, function results) to registered tap sources.

add-tap to register a source and receive all values sent. remove-tap to remove a source.

tap> form sends its contents to all registered taps. If no taps are registered, the values sent by tap> are discarded.

(deref (deref #'clojure.core/tapset)) will show the tap sources. tapset is a Clojure set defined as private var and not meant to be accessed directly.

Online Portal demo

Add Portalλ︎

Clojure CLI user configuration aliases enable Portal to be used with any Clojure or ClojureScript project.

Practicalli Clojure CLI Config contains several aliases that support Portal, either to start a REPL process that can send all Clojure evaluated code to Portal or simply adding Portal as a library for manual use.

Run a REPL with portal and portal.nrepl/wrap-portal to send every REPL evaluation to Portal over an nREPL connection

  • :repl/reloaded - starts a rich terminal UI REPL with Portal nREPL middleware, including REPL Reloaded tools
  • :repl/inspect - starts a basic REPL with Portal nREPL middleware.

Or include the portal library in clojure commands or when starting a REPL via an editor

  • dev/reloaded - Portal, including REPL Reloaded tools
  • inspect/portal-cli - Clojure CLI (simplest approach)
  • inspect/portal-web - Web ClojureScript REPL
  • inspect/portal-node - node ClojureScript REPL

Create portal aliases to include the portal libraries for the Clojure, ClojureScript Web browser and ClojureScript Node server libraries

Portal aliases in Clojure CLI user configuration

:inspect/portal-cli
{:extra-deps {djblue/portal {:mvn/version "0.34.2"}
              clj-commons/clj-yaml         {:mvn/version "0.7.0"}}}

:inspect/portal-web
{:extra-deps {djblue/portal             {:mvn/version "0.34.2"}
              org.clojure/clojurescript {:mvn/version "1.10.844"}}
 :main-opts  ["-m" "cljs.main"]}

:inspect/portal-node
{:extra-deps {djblue/portal             {:mvn/version "0.34.2"}
              org.clojure/clojurescript {:mvn/version "1.10.844"}}
 :main-opts  ["-m" "cljs.main" "-re" "node"]}

:repl/inspect
{:extra-deps
 {cider/cider-nrepl {:mvn/version "0.28.5"}
  djblue/portal     {:mvn/version "0.33.0"}
  clj-commons/clj-yaml         {:mvn/version "0.7.0"}}
 :main-opts ["-m" "nrepl.cmdline"
             "--middleware"
             "[cider.nrepl/cider-middleware,portal.nrepl/wrap-portal]"]}

Practicalli Clojure CLI Config contains several aliases that support Portal.

YAML support for Portal - Clojure only

clj-commons/clj-yaml adds YAML support to Portal for Clojure on the JVM

REPL Reloaded Aliases

REPL Reloaded section includes the :repl/reloaded and :dev/reloaded ailas definitions

Start REPL with Portalλ︎

Run a REPL in a terminal and include the Portal library, using the Clojure CLI tools

Start a REPL with namespace reloading, hotload libraries and portal data inspector

clojure -M:repl/reloaded

Or start the REPL with only portal

clojure -M:inspect/portal:repl/rebel

Add cider-clojure-cli-aliases to a .dir-locals.el in the root of the Clojure project with an alias used to add portal

.dir-locals.el

((clojure-mode . ((cider-preferred-build-tool . clojure-cli)
                  (cider-clojure-cli-aliases . ":dev/reloaded"))))

Or include an alias with only portal data inspector

.dir-locals.el

((clojure-mode . ((cider-preferred-build-tool . clojure-cli)
                  (cider-clojure-cli-aliases . ":inspect/portal-cli"))))

Set cider-clojure-cli-aliases to the alias used to add portal, e.g. inspect/portal

Example

(setq cider-clojure-cli-aliases ":inspect/portal")

Spacemacs: add to dotspacemacs/user-config in the Spacemacs configuration file. Doom Emacs: add to config.el Doom configuration file.

Open Portalλ︎

(require '[portal.api :as inspect]) once the REPL starts.

For inspector-portal-web use (require '[portal.web :as inspect]) instead

(inspect/open) to open the Portal inspector window in a browser (see portal themes)

(add-tap #'portal/submit) to add portal as a tap target

Use Portal from REPLλ︎

Portal functions can be called from the REPL prompt. When using Portal regularly, include code in a file, e.g. dev/user.clj namespace to start a portal and add a tap source. Use a rich comment form, (comment ,,,) to wrap the portal function calls if Portal should be launched manually by the developer.

user namespace and REPL commands

(ns user
  (:require [portal.api :as inspect]))

(comment
  ;; Open a portal inspector window
  (inspect/open)
  ;; Add portal as a tap> target over nREPL connection
  (add-tap portal.api/submit)
  ;; Clear all values in the portal inspector window
  (inspect/clear)
  ;; Close the inspector
  (inspect/close)
  ) ;; End of rich comment block

Open Portal on REPL startupλ︎

Start the Portal inspector as soon as the REPL is started. This works for a terminal REPL as well as clojure aware editors.

Create a dev/user.clj source code file which requires the portal.api library, opens the inspector window and adds portal as a tap source.

When using namespace reloading tools (clojure tools.namespace.repl, Integrant, etc.) it is advisable to exclude dev directory from the path to avoid launching multiple instances of Portal.

Example

(ns user
  (:require
   [portal.api :as inspect]
   [clojure.tools.namespace.repl :as namespace]))

(println "Set REPL refresh directories to " (namespace/set-refresh-dirs "src" "resources"))

As a further precaution, check the Portal API sessions value to ensure Portal is not already running, preventing Portal running multiple times

Example

(def portal-instance
  (or (first (inspect/sessions))
      (inspect/open {:portal.colors/theme :portal.colors/gruvbox})))

Example

(ns user
  (:require [portal.api :as inspect]))

;; ---------------------------------------------------------
;; Open Portal window in browser with dark theme
(inspect/open {:portal.colors/theme :portal.colors/gruvbox})
;; Add portal as a tap> target over nREPL connection
(add-tap #'portal.api/submit)
;; ---------------------------------------------------------
(comment
  (inspect/clear)  ; Clear all values in the portal inspector window
  (inspect/close)  ; Close the inspector
  ) ; End of rich comment block

Start a REPL using the :repl/reloaded or :dev/reloaded alias from Practicalli Clojure CLI Config to include the dev directory on the path and the portal library.

Basic useλ︎

The tap> function sends data to Portal to be shown on the inspector window.

(tap> {:accounts
        [{:name "jen" :email "jen@jen.com"}
        {:name "sara" :email "sara@sara.com"}]})

Use portal to navigate and inspect the details of the data sent to it via tap>.

(inspect/clear) to clear all values from the portal inspector window.

(inspect/close) to close the inspector window.

Editor Commandsλ︎

Control Portal from a Clojure Editor by wrapping the portal commands.

Add helper functions to the Emacs configuration and add key bindings to call them.

Emacs Configuration

;; def portal to the dev namespace to allow dereferencing via @dev/portal
(defun portal.api/open ()
  (interactive)
  (cider-nrepl-sync-request:eval
    "(do (ns dev)
         (def portal ((requiring-resolve 'portal.api/open)))
         (add-tap (requiring-resolve 'portal.api/submit)))"))

(defun portal.api/clear ()
  (interactive)
  (cider-nrepl-sync-request:eval "(portal.api/clear)"))

(defun portal.api/close ()
  (interactive)
  (cider-nrepl-sync-request:eval "(portal.api/close)"))
  • Spacemacs: add to dotspacemacs/user-config in the Spacemacs configuration file.
  • Doom emacs: add to config.el Doom configuration file.

Add key bindings to call the helper functions, ideally from the Clojure major mode menu.

Add key bindings specifically for Clojure mode, available via the , d p debug portal menu when a Clojure file (clj, edn, cljc, cljs) is open in the current buffer.

Spacemacs Key bindings for Portal

Add key bindings to Clojure major mode, e.g. , d p c to clear values from Portal

Spacemacs Configuration - dotspacemacs/user-config
(spacemacs/declare-prefix-for-mode 'clojure-mode "dp" "Portal")
(spacemacs/set-leader-keys-for-major-mode 'clojure-mode "dpp" 'portal.api/open)
(spacemacs/set-leader-keys-for-major-mode 'clojure-mode "dpc" 'portal.api/clear)
(spacemacs/set-leader-keys-for-major-mode 'clojure-mode "dpD" 'portal.api/close)

Or add user key bindings to user menu, SPC o avoiding potential clash with Spacemacs Clojure layer key bindings. e.g. Space o p c to clear values from Portal

Spacemacs Configuration - dotspacemacs/user-config
(spacemacs/declare-prefix "op" "Clojure Portal")
(spacemacs/set-leader-keys "opp" 'portal.api/open)
(spacemacs/set-leader-keys "opc" 'portal.api/clear)
(spacemacs/set-leader-keys "opD" 'portal.api/close)

Use the map! macro to add keys to the clojure-mode-map, using :after to ensure cider is loaded before binding the keys

Doom Configuration

(map! :map clojure-mode-map
      :n "s-o" #'portal.api/open
      :n "C-l" #'portal.api/clear)

Practicalli Doom Emacs configuration

Practicalli Doom Emacs config includes Portal key bindings in the Clojure major mode menu, under the debug menu. * , d p o to open portal * , d p c to clear results from portal

(map! :after cider
      :map clojure-mode-map
      :localleader
      :desc "REPL session" "'" #'sesman-start

      ;; Debug Clojure
      (:prefix ("d" . "debug/inspect")
       :desc "debug" "d" #'cider-debug-defun-at-point
       (:prefix ("i" . "inspect")
        :desc "last expression" "e" #'cider-inspect-last-sexp
        :desc "expression" "f" #'cider-inspect-defun-at-point
        :desc "inspector" "i" #'cider-inspect
        :desc "last result" "l" #'cider-inspect-last-result
        (:prefix ("p" . "portal")
         :desc "Clear" "c" #'portal.api/clear
         :desc "Open" "D" #'portal.api/close
         :desc "Open" "p" #'portal.api/open)
        :desc "value" "v" #'cider-inspect-expr))

        ; truncated...
        )

Practicalli Doom Emacs Config - +clojure.el

Portal Documentation - Editors

Editor nREPL middlewareλ︎

portal.nrepl/wrap-portal sends every REPL evaluation to Portal over an nREPL connection, avoiding the need to wrap expressions with tap>.

Start a REPL that includes the Portal nREPL middleware to send the result of every evaluation to portal.

  • :repl/reloaded - rich terminal UI with portal and REPL Reloaded tools
  • :repl/inspect - basic terminal UI with portal

:repl/inspect to start a terminal REPL with nREPL support for Clojure editor connection and portal libraries and middleware that will send all evaluations to portal once added as a tap source. !!!! EXAMPLE "User deps.edn"

:repl/inspect
{:extra-deps
 {nrepl/nrepl          {:mvn/version "1.0.0"}
  cider/cider-nrepl    {:mvn/version "0.30.0"}
  djblue/portal        {:mvn/version "0.40.0"}
  clj-commons/clj-yaml {:mvn/version "0.7.0"}}
 :main-opts ["-m" "nrepl.cmdline"
             "--middleware"
             "[cider.nrepl/cider-middleware,portal.nrepl/wrap-portal]"]}

Start a REPL with :repl/reloaded or 'repl/inspect'

clojure -M:repl/reloaded

Start Portal User Interface and add portal as a tap target using the portal.api/submit function to send all evaluated code to Portal

Clear results to keep history manageable

Use the Portal API clear function to remove all existing results in Portal

Tap Logs to Portalλ︎

Using a custom mulog publisher, all event logs can be automatically sent to portal.

mulog tap publisher

;; ---------------------------------------------------------
;; Mulog Custom Publishers
;; - tap publisher for use with Portal and other tap sources
;; ---------------------------------------------------------
(ns mulog-publisher
  (:require
   ;; [com.brunobonacci.mulog :as mulog]
   [com.brunobonacci.mulog.buffer :as mulog-buffer]
   [portal.api :as p]))

(deftype TapPublisher [buffer transform]
  com.brunobonacci.mulog.publisher.PPublisher
  (agent-buffer [_] buffer)
  (publish-delay [_] 200)
  (publish [_ buffer]
    (doseq [item (transform (map second (mulog-buffer/items buffer)))]
      (tap> item))
    (mulog-buffer/clear buffer)))

(defn tap
  [{:keys [transform] :as _config}]
  (TapPublisher. (mulog-buffer/agent-buffer 10000) (or transform identity)))

Require the mulog-publisher namespace and mulog library in the user ns expression

Require mulog-publisher namespace

(ns user
  "Tools for REPL Driven Development"
  (:require
   [com.brunobonacci.mulog :as mulog]
   [mulog-publisher]))

Start the publisher, optionally setting a global context for events first

Set values for all mulog events and start custom mulog publisher

;; ---------------------------------------------------------
;; Mulog events and publishing

;; set event global context - information added to every event for REPL workflow
(mulog/set-global-context! {:app-name "Practicalli Service",
                            :version "0.1.0", :env "dev"})

(def mulog-tap-publisher
  "Start mulog custom tap publisher to send all events to Portal
  and other tap sources
  `mulog-tap-publisher` to stop publisher"
  (mulog/start-publisher!
    {:type :custom, :fqn-function "mulog-publisher/tap"}))
;; ---------------------------------------------------------

Mulog events are now sent to portal when evaluated

    (mulog/log ::repl-state ::ns (ns-publics *ns*))

Stop the mulog publisher by calling the reference it returns, i.e. mulog-tap-publisher

Function to stop mulog tap publisher

(defn mulog-tap-stop
 "Stop mulog tap publisher to ensure multiple publishers are not started
 Recommended before using `(restart)` or evaluating the `user` namespace"
  [] (mulog-tap-publisher))

Referencesλ︎

Portal Documentation - clj-docs