mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
Add configurable base path for reverse proxy use (#169)
* ui: Refactor routing functions into Router record * ui: Store base URI in AppConfig * ui: Use basePath in Router functions * backend: Add Web.BasePath config option and update routes * Tweaks to get SPA to bootstrap basePath configured * ui: basePath support for apis/serve * ui: basePath support for message monitor * web: Redirect requests to / when basePath configured * doc: add basepath to config.md * Closes #107
This commit is contained in:
@@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/inbucket/inbucket/pkg/storage"
|
"github.com/inbucket/inbucket/pkg/storage"
|
||||||
"github.com/inbucket/inbucket/pkg/storage/file"
|
"github.com/inbucket/inbucket/pkg/storage/file"
|
||||||
"github.com/inbucket/inbucket/pkg/storage/mem"
|
"github.com/inbucket/inbucket/pkg/storage/mem"
|
||||||
|
"github.com/inbucket/inbucket/pkg/stringutil"
|
||||||
"github.com/inbucket/inbucket/pkg/webui"
|
"github.com/inbucket/inbucket/pkg/webui"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -129,9 +130,10 @@ func main() {
|
|||||||
retentionScanner := storage.NewRetentionScanner(conf.Storage, store, shutdownChan)
|
retentionScanner := storage.NewRetentionScanner(conf.Storage, store, shutdownChan)
|
||||||
retentionScanner.Start()
|
retentionScanner.Start()
|
||||||
|
|
||||||
// Start HTTP server.
|
// Configure routes and start HTTP server.
|
||||||
webui.SetupRoutes(web.Router.PathPrefix("/serve/").Subrouter())
|
prefix := stringutil.MakePathPrefixer(conf.Web.BasePath)
|
||||||
rest.SetupRoutes(web.Router.PathPrefix("/api/").Subrouter())
|
webui.SetupRoutes(web.Router.PathPrefix(prefix("/serve/")).Subrouter())
|
||||||
|
rest.SetupRoutes(web.Router.PathPrefix(prefix("/api/")).Subrouter())
|
||||||
web.Initialize(conf, shutdownChan, mmanager, msgHub)
|
web.Initialize(conf, shutdownChan, mmanager, msgHub)
|
||||||
go web.Start(rootCtx)
|
go web.Start(rootCtx)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ variables it supports:
|
|||||||
INBUCKET_POP3_DOMAIN inbucket HELLO domain
|
INBUCKET_POP3_DOMAIN inbucket HELLO domain
|
||||||
INBUCKET_POP3_TIMEOUT 600s Idle network timeout
|
INBUCKET_POP3_TIMEOUT 600s Idle network timeout
|
||||||
INBUCKET_WEB_ADDR 0.0.0.0:9000 Web server IP4 host:port
|
INBUCKET_WEB_ADDR 0.0.0.0:9000 Web server IP4 host:port
|
||||||
|
INBUCKET_WEB_BASEPATH Base path prefix for UI and API URLs
|
||||||
INBUCKET_WEB_UIDIR ui/dist User interface dir
|
INBUCKET_WEB_UIDIR ui/dist User interface dir
|
||||||
INBUCKET_WEB_GREETINGFILE ui/greeting.html Home page greeting HTML
|
INBUCKET_WEB_GREETINGFILE ui/greeting.html Home page greeting HTML
|
||||||
INBUCKET_WEB_MONITORVISIBLE true Show monitor tab in UI?
|
INBUCKET_WEB_MONITORVISIBLE true Show monitor tab in UI?
|
||||||
@@ -290,6 +291,24 @@ Inbucket to listen on all available network interfaces.
|
|||||||
|
|
||||||
- Default: `0.0.0.0:9000`
|
- Default: `0.0.0.0:9000`
|
||||||
|
|
||||||
|
### Base Path
|
||||||
|
|
||||||
|
`INBUCKET_WEB_BASEPATH`
|
||||||
|
|
||||||
|
Base path prefix for UI and API URLs. This option is used when you wish to
|
||||||
|
root all Inbucket URLs to a specific path when placing it behind a
|
||||||
|
reverse-proxy.
|
||||||
|
|
||||||
|
For example, setting the base path to `prefix` will move:
|
||||||
|
- the Inbucket status page from `/status` to `/prefix/status`,
|
||||||
|
- Bob's mailbox from `/m/bob` to `/prefix/m/bob`, and
|
||||||
|
- the REST API from `/api/v1/*` to `/prefix/api/v1/*`.
|
||||||
|
|
||||||
|
*Note:* This setting will not work correctly when running Inbucket via the npm
|
||||||
|
development server.
|
||||||
|
|
||||||
|
- Default: None
|
||||||
|
|
||||||
### UI Directory
|
### UI Directory
|
||||||
|
|
||||||
`INBUCKET_WEB_UIDIR`
|
`INBUCKET_WEB_UIDIR`
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export INBUCKET_WEB_TEMPLATECACHE="false"
|
|||||||
export INBUCKET_WEB_COOKIEAUTHKEY="not-secret"
|
export INBUCKET_WEB_COOKIEAUTHKEY="not-secret"
|
||||||
export INBUCKET_WEB_UIDIR="ui/dist"
|
export INBUCKET_WEB_UIDIR="ui/dist"
|
||||||
#export INBUCKET_WEB_MONITORVISIBLE="false"
|
#export INBUCKET_WEB_MONITORVISIBLE="false"
|
||||||
|
#export INBUCKET_WEB_BASEPATH="prefix"
|
||||||
export INBUCKET_STORAGE_TYPE="file"
|
export INBUCKET_STORAGE_TYPE="file"
|
||||||
export INBUCKET_STORAGE_PARAMS="path:/tmp/inbucket"
|
export INBUCKET_STORAGE_PARAMS="path:/tmp/inbucket"
|
||||||
export INBUCKET_STORAGE_RETENTIONPERIOD="3h"
|
export INBUCKET_STORAGE_RETENTIONPERIOD="3h"
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ type POP3 struct {
|
|||||||
// Web contains the HTTP server configuration.
|
// Web contains the HTTP server configuration.
|
||||||
type Web struct {
|
type Web struct {
|
||||||
Addr string `required:"true" default:"0.0.0.0:9000" desc:"Web server IP4 host:port"`
|
Addr string `required:"true" default:"0.0.0.0:9000" desc:"Web server IP4 host:port"`
|
||||||
|
BasePath string `default:"" desc:"Base path prefix for UI and API URLs"`
|
||||||
UIDir string `required:"true" default:"ui/dist" desc:"User interface dir"`
|
UIDir string `required:"true" default:"ui/dist" desc:"User interface dir"`
|
||||||
GreetingFile string `required:"true" default:"ui/greeting.html" desc:"Home page greeting HTML"`
|
GreetingFile string `required:"true" default:"ui/greeting.html" desc:"Home page greeting HTML"`
|
||||||
MonitorVisible bool `required:"true" default:"true" desc:"Show monitor tab in UI?"`
|
MonitorVisible bool `required:"true" default:"true" desc:"Show monitor tab in UI?"`
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
type jsonAppConfig struct {
|
type jsonAppConfig struct {
|
||||||
MonitorVisible bool `json:"monitor-visible"`
|
BasePath string `json:"base-path"`
|
||||||
|
MonitorVisible bool `json:"monitor-visible"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/inbucket/inbucket/pkg/config"
|
"github.com/inbucket/inbucket/pkg/config"
|
||||||
"github.com/inbucket/inbucket/pkg/message"
|
"github.com/inbucket/inbucket/pkg/message"
|
||||||
"github.com/inbucket/inbucket/pkg/msghub"
|
"github.com/inbucket/inbucket/pkg/msghub"
|
||||||
|
"github.com/inbucket/inbucket/pkg/stringutil"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,33 +57,42 @@ func Initialize(
|
|||||||
msgHub = mh
|
msgHub = mh
|
||||||
manager = mm
|
manager = mm
|
||||||
|
|
||||||
|
// Redirect requests to / if there is a base path configured.
|
||||||
|
prefix := stringutil.MakePathPrefixer(conf.Web.BasePath)
|
||||||
|
redirectBase := prefix("/")
|
||||||
|
if redirectBase != "/" {
|
||||||
|
log.Info().Str("module", "web").Str("phase", "startup").Str("path", redirectBase).
|
||||||
|
Msg("Base path configured")
|
||||||
|
Router.Path("/").Handler(http.RedirectHandler(redirectBase, http.StatusFound))
|
||||||
|
}
|
||||||
|
|
||||||
// Dynamic paths.
|
// Dynamic paths.
|
||||||
log.Info().Str("module", "web").Str("phase", "startup").Str("path", conf.Web.UIDir).
|
log.Info().Str("module", "web").Str("phase", "startup").Str("path", conf.Web.UIDir).
|
||||||
Msg("Web UI content mapped")
|
Msg("Web UI content mapped")
|
||||||
Router.Handle("/debug/vars", expvar.Handler())
|
Router.Handle(prefix("/debug/vars"), expvar.Handler())
|
||||||
if conf.Web.PProf {
|
if conf.Web.PProf {
|
||||||
Router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
Router.HandleFunc(prefix("/debug/pprof/cmdline"), pprof.Cmdline)
|
||||||
Router.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
Router.HandleFunc(prefix("/debug/pprof/profile"), pprof.Profile)
|
||||||
Router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
Router.HandleFunc(prefix("/debug/pprof/symbol"), pprof.Symbol)
|
||||||
Router.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
Router.HandleFunc(prefix("/debug/pprof/trace"), pprof.Trace)
|
||||||
Router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
|
Router.PathPrefix(prefix("/debug/pprof/")).HandlerFunc(pprof.Index)
|
||||||
log.Warn().Str("module", "web").Str("phase", "startup").
|
log.Warn().Str("module", "web").Str("phase", "startup").
|
||||||
Msg("Go pprof tools installed to /debug/pprof")
|
Msg("Go pprof tools installed to " + prefix("/debug/pprof"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static paths.
|
// Static paths.
|
||||||
Router.PathPrefix("/static").Handler(
|
Router.PathPrefix(prefix("/static")).Handler(
|
||||||
http.StripPrefix("/", http.FileServer(http.Dir(conf.Web.UIDir))))
|
http.StripPrefix(prefix("/"), http.FileServer(http.Dir(conf.Web.UIDir))))
|
||||||
Router.Path("/favicon.png").Handler(
|
Router.Path(prefix("/favicon.png")).Handler(
|
||||||
fileHandler(filepath.Join(conf.Web.UIDir, "favicon.png")))
|
fileHandler(filepath.Join(conf.Web.UIDir, "favicon.png")))
|
||||||
|
|
||||||
// SPA managed paths.
|
// SPA managed paths.
|
||||||
spaHandler := cookieHandler(appConfigCookie(conf.Web),
|
spaHandler := cookieHandler(appConfigCookie(conf.Web),
|
||||||
fileHandler(filepath.Join(conf.Web.UIDir, "index.html")))
|
fileHandler(filepath.Join(conf.Web.UIDir, "index.html")))
|
||||||
Router.Path("/").Handler(spaHandler)
|
Router.Path(prefix("/")).Handler(spaHandler)
|
||||||
Router.Path("/monitor").Handler(spaHandler)
|
Router.Path(prefix("/monitor")).Handler(spaHandler)
|
||||||
Router.Path("/status").Handler(spaHandler)
|
Router.Path(prefix("/status")).Handler(spaHandler)
|
||||||
Router.PathPrefix("/m/").Handler(spaHandler)
|
Router.PathPrefix(prefix("/m/")).Handler(spaHandler)
|
||||||
|
|
||||||
// Error handlers.
|
// Error handlers.
|
||||||
Router.NotFoundHandler = noMatchHandler(
|
Router.NotFoundHandler = noMatchHandler(
|
||||||
@@ -131,6 +141,7 @@ func Start(ctx context.Context) {
|
|||||||
|
|
||||||
func appConfigCookie(webConfig config.Web) *http.Cookie {
|
func appConfigCookie(webConfig config.Web) *http.Cookie {
|
||||||
o := &jsonAppConfig{
|
o := &jsonAppConfig{
|
||||||
|
BasePath: webConfig.BasePath,
|
||||||
MonitorVisible: webConfig.MonitorVisible,
|
MonitorVisible: webConfig.MonitorVisible,
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(o)
|
b, err := json.Marshal(o)
|
||||||
|
|||||||
@@ -61,3 +61,16 @@ func SliceToLower(slice []string) {
|
|||||||
slice[i] = strings.ToLower(s)
|
slice[i] = strings.ToLower(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MakePathPrefixer returns a function that will add the specified prefix (base) to URI strings.
|
||||||
|
// The returned prefixer expects all provided paths to start with /.
|
||||||
|
func MakePathPrefixer(prefix string) func(string) string {
|
||||||
|
prefix = strings.Trim(prefix, "/")
|
||||||
|
if prefix != "" {
|
||||||
|
prefix = "/" + prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(path string) string {
|
||||||
|
return prefix + path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package stringutil_test
|
package stringutil_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -35,3 +36,43 @@ func TestStringAddressList(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMakePathPrefixer(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
prefix, path, want string
|
||||||
|
}{
|
||||||
|
{prefix: "", path: "", want: ""},
|
||||||
|
{prefix: "", path: "relative", want: "relative"},
|
||||||
|
{prefix: "", path: "/qualified", want: "/qualified"},
|
||||||
|
{prefix: "", path: "/many/path/segments", want: "/many/path/segments"},
|
||||||
|
{prefix: "pfx", path: "", want: "/pfx"},
|
||||||
|
{prefix: "pfx", path: "/", want: "/pfx/"},
|
||||||
|
{prefix: "pfx", path: "relative", want: "/pfxrelative"},
|
||||||
|
{prefix: "pfx", path: "/qualified", want: "/pfx/qualified"},
|
||||||
|
{prefix: "pfx", path: "/many/path/segments", want: "/pfx/many/path/segments"},
|
||||||
|
{prefix: "/pfx/", path: "", want: "/pfx"},
|
||||||
|
{prefix: "/pfx/", path: "/", want: "/pfx/"},
|
||||||
|
{prefix: "/pfx/", path: "relative", want: "/pfxrelative"},
|
||||||
|
{prefix: "/pfx/", path: "/qualified", want: "/pfx/qualified"},
|
||||||
|
{prefix: "/pfx/", path: "/many/path/segments", want: "/pfx/many/path/segments"},
|
||||||
|
{prefix: "a/b/c", path: "", want: "/a/b/c"},
|
||||||
|
{prefix: "a/b/c", path: "/", want: "/a/b/c/"},
|
||||||
|
{prefix: "a/b/c", path: "relative", want: "/a/b/crelative"},
|
||||||
|
{prefix: "a/b/c", path: "/qualified", want: "/a/b/c/qualified"},
|
||||||
|
{prefix: "a/b/c", path: "/many/path/segments", want: "/a/b/c/many/path/segments"},
|
||||||
|
{prefix: "/a/b/c/", path: "", want: "/a/b/c"},
|
||||||
|
{prefix: "/a/b/c/", path: "/", want: "/a/b/c/"},
|
||||||
|
{prefix: "/a/b/c/", path: "relative", want: "/a/b/crelative"},
|
||||||
|
{prefix: "/a/b/c/", path: "/qualified", want: "/a/b/c/qualified"},
|
||||||
|
{prefix: "/a/b/c/", path: "/many/path/segments", want: "/a/b/c/many/path/segments"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(fmt.Sprintf("prefix %s for path %s", tc.prefix, tc.path), func(t *testing.T) {
|
||||||
|
prefixer := stringutil.MakePathPrefixer(tc.prefix)
|
||||||
|
got := prefixer(tc.path)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("Got: %q, want: %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
130
ui/src/Api.elm
130
ui/src/Api.elm
@@ -6,6 +6,7 @@ module Api exposing
|
|||||||
, getServerConfig
|
, getServerConfig
|
||||||
, getServerMetrics
|
, getServerMetrics
|
||||||
, markMessageSeen
|
, markMessageSeen
|
||||||
|
, monitorUri
|
||||||
, purgeMailbox
|
, purgeMailbox
|
||||||
, serveUrl
|
, serveUrl
|
||||||
)
|
)
|
||||||
@@ -14,10 +15,12 @@ import Data.Message as Message exposing (Message)
|
|||||||
import Data.MessageHeader as MessageHeader exposing (MessageHeader)
|
import Data.MessageHeader as MessageHeader exposing (MessageHeader)
|
||||||
import Data.Metrics as Metrics exposing (Metrics)
|
import Data.Metrics as Metrics exposing (Metrics)
|
||||||
import Data.ServerConfig as ServerConfig exposing (ServerConfig)
|
import Data.ServerConfig as ServerConfig exposing (ServerConfig)
|
||||||
|
import Data.Session exposing (Session)
|
||||||
import Http
|
import Http
|
||||||
import HttpUtil
|
import HttpUtil
|
||||||
import Json.Decode as Decode
|
import Json.Decode as Decode
|
||||||
import Json.Encode as Encode
|
import Json.Encode as Encode
|
||||||
|
import String
|
||||||
import Url.Builder
|
import Url.Builder
|
||||||
|
|
||||||
|
|
||||||
@@ -29,31 +32,17 @@ type alias HttpResult msg =
|
|||||||
Result HttpUtil.Error () -> msg
|
Result HttpUtil.Error () -> msg
|
||||||
|
|
||||||
|
|
||||||
{-| Builds a public REST API URL (see wiki).
|
deleteMessage : Session -> HttpResult msg -> String -> String -> Cmd msg
|
||||||
-}
|
deleteMessage session msg mailboxName id =
|
||||||
apiV1Url : List String -> String
|
HttpUtil.delete msg (apiV1Url session [ "mailbox", mailboxName, id ])
|
||||||
apiV1Url elements =
|
|
||||||
Url.Builder.absolute ([ "api", "v1" ] ++ elements) []
|
|
||||||
|
|
||||||
|
|
||||||
{-| Builds an internal `serve` REST API URL; only used by this UI.
|
getHeaderList : Session -> DataResult msg (List MessageHeader) -> String -> Cmd msg
|
||||||
-}
|
getHeaderList session msg mailboxName =
|
||||||
serveUrl : List String -> String
|
|
||||||
serveUrl elements =
|
|
||||||
Url.Builder.absolute ("serve" :: elements) []
|
|
||||||
|
|
||||||
|
|
||||||
deleteMessage : HttpResult msg -> String -> String -> Cmd msg
|
|
||||||
deleteMessage msg mailboxName id =
|
|
||||||
HttpUtil.delete msg (apiV1Url [ "mailbox", mailboxName, id ])
|
|
||||||
|
|
||||||
|
|
||||||
getHeaderList : DataResult msg (List MessageHeader) -> String -> Cmd msg
|
|
||||||
getHeaderList msg mailboxName =
|
|
||||||
let
|
let
|
||||||
context =
|
context =
|
||||||
{ method = "GET"
|
{ method = "GET"
|
||||||
, url = apiV1Url [ "mailbox", mailboxName ]
|
, url = apiV1Url session [ "mailbox", mailboxName ]
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
Http.get
|
Http.get
|
||||||
@@ -62,12 +51,12 @@ getHeaderList msg mailboxName =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getGreeting : DataResult msg String -> Cmd msg
|
getGreeting : Session -> DataResult msg String -> Cmd msg
|
||||||
getGreeting msg =
|
getGreeting session msg =
|
||||||
let
|
let
|
||||||
context =
|
context =
|
||||||
{ method = "GET"
|
{ method = "GET"
|
||||||
, url = serveUrl [ "greeting" ]
|
, url = serveUrl session [ "greeting" ]
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
Http.get
|
Http.get
|
||||||
@@ -76,12 +65,12 @@ getGreeting msg =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getMessage : DataResult msg Message -> String -> String -> Cmd msg
|
getMessage : Session -> DataResult msg Message -> String -> String -> Cmd msg
|
||||||
getMessage msg mailboxName id =
|
getMessage session msg mailboxName id =
|
||||||
let
|
let
|
||||||
context =
|
context =
|
||||||
{ method = "GET"
|
{ method = "GET"
|
||||||
, url = serveUrl [ "mailbox", mailboxName, id ]
|
, url = serveUrl session [ "mailbox", mailboxName, id ]
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
Http.get
|
Http.get
|
||||||
@@ -90,12 +79,12 @@ getMessage msg mailboxName id =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getServerConfig : DataResult msg ServerConfig -> Cmd msg
|
getServerConfig : Session -> DataResult msg ServerConfig -> Cmd msg
|
||||||
getServerConfig msg =
|
getServerConfig session msg =
|
||||||
let
|
let
|
||||||
context =
|
context =
|
||||||
{ method = "GET"
|
{ method = "GET"
|
||||||
, url = serveUrl [ "status" ]
|
, url = serveUrl session [ "status" ]
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
Http.get
|
Http.get
|
||||||
@@ -104,12 +93,19 @@ getServerConfig msg =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getServerMetrics : DataResult msg Metrics -> Cmd msg
|
getServerMetrics : Session -> DataResult msg Metrics -> Cmd msg
|
||||||
getServerMetrics msg =
|
getServerMetrics session msg =
|
||||||
let
|
let
|
||||||
context =
|
context =
|
||||||
{ method = "GET"
|
{ method = "GET"
|
||||||
, url = Url.Builder.absolute [ "debug", "vars" ] []
|
, url =
|
||||||
|
Url.Builder.absolute
|
||||||
|
(splitBasePath session.config.basePath
|
||||||
|
++ [ "debug"
|
||||||
|
, "vars"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
[]
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
Http.get
|
Http.get
|
||||||
@@ -118,15 +114,73 @@ getServerMetrics msg =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
markMessageSeen : HttpResult msg -> String -> String -> Cmd msg
|
markMessageSeen : Session -> HttpResult msg -> String -> String -> Cmd msg
|
||||||
markMessageSeen msg mailboxName id =
|
markMessageSeen session msg mailboxName id =
|
||||||
-- The URL tells the API which message ID to update, so we only need to indicate the
|
-- The URL tells the API which message ID to update, so we only need to indicate the
|
||||||
-- desired change in the body.
|
-- desired change in the body.
|
||||||
Encode.object [ ( "seen", Encode.bool True ) ]
|
Encode.object [ ( "seen", Encode.bool True ) ]
|
||||||
|> Http.jsonBody
|
|> Http.jsonBody
|
||||||
|> HttpUtil.patch msg (apiV1Url [ "mailbox", mailboxName, id ])
|
|> HttpUtil.patch msg (apiV1Url session [ "mailbox", mailboxName, id ])
|
||||||
|
|
||||||
|
|
||||||
purgeMailbox : HttpResult msg -> String -> Cmd msg
|
monitorUri : Session -> String
|
||||||
purgeMailbox msg mailboxName =
|
monitorUri session =
|
||||||
HttpUtil.delete msg (apiV1Url [ "mailbox", mailboxName ])
|
apiV1Url session [ "monitor", "messages" ]
|
||||||
|
|
||||||
|
|
||||||
|
purgeMailbox : Session -> HttpResult msg -> String -> Cmd msg
|
||||||
|
purgeMailbox session msg mailboxName =
|
||||||
|
HttpUtil.delete msg (apiV1Url session [ "mailbox", mailboxName ])
|
||||||
|
|
||||||
|
|
||||||
|
{-| Builds a public REST API URL (see wiki).
|
||||||
|
-}
|
||||||
|
apiV1Url : Session -> List String -> String
|
||||||
|
apiV1Url session elements =
|
||||||
|
Url.Builder.absolute
|
||||||
|
(List.concat
|
||||||
|
[ splitBasePath session.config.basePath
|
||||||
|
, [ "api", "v1" ]
|
||||||
|
, elements
|
||||||
|
]
|
||||||
|
)
|
||||||
|
[]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Builds an internal `serve` REST API URL; only used by this UI.
|
||||||
|
-}
|
||||||
|
serveUrl : Session -> List String -> String
|
||||||
|
serveUrl session elements =
|
||||||
|
Url.Builder.absolute
|
||||||
|
(List.concat
|
||||||
|
[ splitBasePath session.config.basePath
|
||||||
|
, [ "serve" ]
|
||||||
|
, elements
|
||||||
|
]
|
||||||
|
)
|
||||||
|
[]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Converts base path into a list of path elements.
|
||||||
|
-}
|
||||||
|
splitBasePath : String -> List String
|
||||||
|
splitBasePath path =
|
||||||
|
if path == "" then
|
||||||
|
[]
|
||||||
|
|
||||||
|
else
|
||||||
|
let
|
||||||
|
stripSlashes str =
|
||||||
|
if String.startsWith "/" str then
|
||||||
|
stripSlashes (String.dropLeft 1 str)
|
||||||
|
|
||||||
|
else if String.endsWith "/" str then
|
||||||
|
stripSlashes (String.dropRight 1 str)
|
||||||
|
|
||||||
|
else
|
||||||
|
str
|
||||||
|
|
||||||
|
newPath =
|
||||||
|
stripSlashes path
|
||||||
|
in
|
||||||
|
String.split "/" newPath
|
||||||
|
|||||||
@@ -5,16 +5,18 @@ import Json.Decode.Pipeline as P
|
|||||||
|
|
||||||
|
|
||||||
type alias AppConfig =
|
type alias AppConfig =
|
||||||
{ monitorVisible : Bool
|
{ basePath : String
|
||||||
|
, monitorVisible : Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
decoder : D.Decoder AppConfig
|
decoder : D.Decoder AppConfig
|
||||||
decoder =
|
decoder =
|
||||||
D.succeed AppConfig
|
D.succeed AppConfig
|
||||||
|
|> P.optional "base-path" D.string ""
|
||||||
|> P.required "monitor-visible" D.bool
|
|> P.required "monitor-visible" D.bool
|
||||||
|
|
||||||
|
|
||||||
default : AppConfig
|
default : AppConfig
|
||||||
default =
|
default =
|
||||||
AppConfig True
|
AppConfig "" True
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import Data.AppConfig as AppConfig exposing (AppConfig)
|
|||||||
import Json.Decode as D
|
import Json.Decode as D
|
||||||
import Json.Decode.Pipeline exposing (optional)
|
import Json.Decode.Pipeline exposing (optional)
|
||||||
import Json.Encode as E
|
import Json.Encode as E
|
||||||
|
import Route exposing (Router)
|
||||||
import Time
|
import Time
|
||||||
import Url exposing (Url)
|
import Url exposing (Url)
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ type alias Session =
|
|||||||
, host : String
|
, host : String
|
||||||
, flash : Maybe Flash
|
, flash : Maybe Flash
|
||||||
, routing : Bool
|
, routing : Bool
|
||||||
|
, router : Router
|
||||||
, zone : Time.Zone
|
, zone : Time.Zone
|
||||||
, config : AppConfig
|
, config : AppConfig
|
||||||
, persistent : Persistent
|
, persistent : Persistent
|
||||||
@@ -50,6 +52,7 @@ init key location config persistent =
|
|||||||
, host = location.host
|
, host = location.host
|
||||||
, flash = Nothing
|
, flash = Nothing
|
||||||
, routing = True
|
, routing = True
|
||||||
|
, router = Route.newRouter config.basePath
|
||||||
, zone = Time.utc
|
, zone = Time.utc
|
||||||
, config = config
|
, config = config
|
||||||
, persistent = persistent
|
, persistent = persistent
|
||||||
@@ -62,6 +65,7 @@ initError key location error =
|
|||||||
, host = location.host
|
, host = location.host
|
||||||
, flash = Just (Flash "Initialization failed" [ ( "Error", error ) ])
|
, flash = Just (Flash "Initialization failed" [ ( "Error", error ) ])
|
||||||
, routing = True
|
, routing = True
|
||||||
|
, router = Route.newRouter ""
|
||||||
, zone = Time.utc
|
, zone = Time.utc
|
||||||
, config = AppConfig.default
|
, config = AppConfig.default
|
||||||
, persistent = Persistent []
|
, persistent = Persistent []
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
module Layout exposing (Model, Msg, Page(..), frame, init, reset, update)
|
module Layout exposing (Model, Msg, Page(..), frame, init, reset, update)
|
||||||
|
|
||||||
|
import Browser.Navigation as Nav
|
||||||
import Data.Session as Session exposing (Session)
|
import Data.Session as Session exposing (Session)
|
||||||
import Html
|
import Html
|
||||||
exposing
|
exposing
|
||||||
@@ -132,7 +133,9 @@ update msg model session =
|
|||||||
else
|
else
|
||||||
( model
|
( model
|
||||||
, session
|
, session
|
||||||
, Route.pushUrl session.key (Route.Mailbox model.mailboxName)
|
, Route.Mailbox model.mailboxName
|
||||||
|
|> session.router.toPath
|
||||||
|
|> Nav.pushUrl session.key
|
||||||
)
|
)
|
||||||
|
|
||||||
RecentMenuMouseOver ->
|
RecentMenuMouseOver ->
|
||||||
@@ -195,14 +198,14 @@ frame { model, session, activePage, activeMailbox, modal, content } =
|
|||||||
[ button [ class "navbar-toggle", Events.onClick (MainMenuToggled |> model.mapMsg) ]
|
[ button [ class "navbar-toggle", Events.onClick (MainMenuToggled |> model.mapMsg) ]
|
||||||
[ i [ class "fas fa-bars" ] [] ]
|
[ i [ class "fas fa-bars" ] [] ]
|
||||||
, span [ class "navbar-brand" ]
|
, span [ class "navbar-brand" ]
|
||||||
[ a [ Route.href Route.Home ] [ text "@ inbucket" ] ]
|
[ a [ href <| session.router.toPath Route.Home ] [ text "@ inbucket" ] ]
|
||||||
, ul [ class "main-nav", classList [ ( "active", model.mainMenuVisible ) ] ]
|
, ul [ class "main-nav", classList [ ( "active", model.mainMenuVisible ) ] ]
|
||||||
[ if session.config.monitorVisible then
|
[ if session.config.monitorVisible then
|
||||||
navbarLink Monitor Route.Monitor [ text "Monitor" ] activePage
|
navbarLink Monitor (session.router.toPath Route.Monitor) [ text "Monitor" ] activePage
|
||||||
|
|
||||||
else
|
else
|
||||||
text ""
|
text ""
|
||||||
, navbarLink Status Route.Status [ text "Status" ] activePage
|
, navbarLink Status (session.router.toPath Route.Status) [ text "Status" ] activePage
|
||||||
, navbarRecent activePage activeMailbox model session
|
, navbarRecent activePage activeMailbox model session
|
||||||
, li [ class "navbar-mailbox" ]
|
, li [ class "navbar-mailbox" ]
|
||||||
[ form [ Events.onSubmit (OpenMailbox |> model.mapMsg) ]
|
[ form [ Events.onSubmit (OpenMailbox |> model.mapMsg) ]
|
||||||
@@ -260,10 +263,10 @@ externalLink url title =
|
|||||||
a [ href url, target "_blank", rel "noopener" ] [ text title ]
|
a [ href url, target "_blank", rel "noopener" ] [ text title ]
|
||||||
|
|
||||||
|
|
||||||
navbarLink : Page -> Route -> List (Html a) -> Page -> Html a
|
navbarLink : Page -> String -> List (Html a) -> Page -> Html a
|
||||||
navbarLink page route linkContent activePage =
|
navbarLink page url linkContent activePage =
|
||||||
li [ classList [ ( "navbar-active", page == activePage ) ] ]
|
li [ classList [ ( "navbar-active", page == activePage ) ] ]
|
||||||
[ a [ Route.href route ] linkContent ]
|
[ a [ href url ] linkContent ]
|
||||||
|
|
||||||
|
|
||||||
{-| Renders list of recent mailboxes, selecting the currently active mailbox.
|
{-| Renders list of recent mailboxes, selecting the currently active mailbox.
|
||||||
@@ -292,7 +295,7 @@ navbarRecent page activeMailbox model session =
|
|||||||
session.persistent.recentMailboxes
|
session.persistent.recentMailboxes
|
||||||
|
|
||||||
recentLink mailbox =
|
recentLink mailbox =
|
||||||
a [ Route.href (Route.Mailbox mailbox) ] [ text mailbox ]
|
a [ href <| session.router.toPath <| Route.Mailbox mailbox ] [ text mailbox ]
|
||||||
in
|
in
|
||||||
li
|
li
|
||||||
[ class "navbar-dropdown-container"
|
[ class "navbar-dropdown-container"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ init configValue location key =
|
|||||||
}
|
}
|
||||||
|
|
||||||
route =
|
route =
|
||||||
Route.fromUrl location
|
session.router.fromUrl location
|
||||||
|
|
||||||
( model, cmd ) =
|
( model, cmd ) =
|
||||||
changeRouteTo route initModel
|
changeRouteTo route initModel
|
||||||
@@ -167,7 +167,7 @@ updateMain msg model session =
|
|||||||
UrlChanged url ->
|
UrlChanged url ->
|
||||||
-- Responds to new browser URL.
|
-- Responds to new browser URL.
|
||||||
if session.routing then
|
if session.routing then
|
||||||
changeRouteTo (Route.fromUrl url) model
|
changeRouteTo (session.router.fromUrl url) model
|
||||||
|
|
||||||
else
|
else
|
||||||
-- Skip once, but re-enable routing.
|
-- Skip once, but re-enable routing.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type alias Model =
|
|||||||
|
|
||||||
init : Session -> ( Model, Cmd Msg )
|
init : Session -> ( Model, Cmd Msg )
|
||||||
init session =
|
init session =
|
||||||
( Model session "", Api.getGreeting GreetingLoaded )
|
( Model session "", Api.getGreeting session GreetingLoaded )
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
module Page.Mailbox exposing (Model, Msg, init, load, subscriptions, update, view)
|
module Page.Mailbox exposing (Model, Msg, init, load, subscriptions, update, view)
|
||||||
|
|
||||||
import Api
|
import Api
|
||||||
|
import Browser.Navigation as Nav
|
||||||
import Data.Message as Message exposing (Message)
|
import Data.Message as Message exposing (Message)
|
||||||
import Data.MessageHeader exposing (MessageHeader)
|
import Data.MessageHeader exposing (MessageHeader)
|
||||||
import Data.Session as Session exposing (Session)
|
import Data.Session as Session exposing (Session)
|
||||||
@@ -113,15 +114,15 @@ init session mailboxName selection =
|
|||||||
, markSeenTimer = Timer.empty
|
, markSeenTimer = Timer.empty
|
||||||
, now = Time.millisToPosix 0
|
, now = Time.millisToPosix 0
|
||||||
}
|
}
|
||||||
, load mailboxName
|
, load session mailboxName
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
load : String -> Cmd Msg
|
load : Session -> String -> Cmd Msg
|
||||||
load mailboxName =
|
load session mailboxName =
|
||||||
Cmd.batch
|
Cmd.batch
|
||||||
[ Task.perform Tick Time.now
|
[ Task.perform Tick Time.now
|
||||||
, Api.getHeaderList ListLoaded mailboxName
|
, Api.getHeaderList session ListLoaded mailboxName
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -165,8 +166,10 @@ update msg model =
|
|||||||
( updateSelected { model | session = Session.disableRouting model.session } id
|
( updateSelected { model | session = Session.disableRouting model.session } id
|
||||||
, Cmd.batch
|
, Cmd.batch
|
||||||
[ -- Update browser location.
|
[ -- Update browser location.
|
||||||
Route.replaceUrl model.session.key (Route.Message model.mailboxName id)
|
Route.Message model.mailboxName id
|
||||||
, Api.getMessage MessageLoaded model.mailboxName id
|
|> model.session.router.toPath
|
||||||
|
|> Nav.replaceUrl model.session.key
|
||||||
|
, Api.getMessage model.session MessageLoaded model.mailboxName id
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -322,8 +325,10 @@ updateTriggerPurge model =
|
|||||||
let
|
let
|
||||||
cmd =
|
cmd =
|
||||||
Cmd.batch
|
Cmd.batch
|
||||||
[ Route.replaceUrl model.session.key (Route.Mailbox model.mailboxName)
|
[ Route.Mailbox model.mailboxName
|
||||||
, Api.purgeMailbox PurgedMailbox model.mailboxName
|
|> model.session.router.toPath
|
||||||
|
|> Nav.replaceUrl model.session.key
|
||||||
|
, Api.purgeMailbox model.session PurgedMailbox model.mailboxName
|
||||||
]
|
]
|
||||||
in
|
in
|
||||||
case model.state of
|
case model.state of
|
||||||
@@ -405,8 +410,10 @@ updateDeleteMessage model message =
|
|||||||
ShowingList (filter (\x -> x.id /= message.id) list) NoMessage
|
ShowingList (filter (\x -> x.id /= message.id) list) NoMessage
|
||||||
}
|
}
|
||||||
, Cmd.batch
|
, Cmd.batch
|
||||||
[ Api.deleteMessage DeletedMessage message.mailbox message.id
|
[ Api.deleteMessage model.session DeletedMessage message.mailbox message.id
|
||||||
, Route.replaceUrl model.session.key (Route.Mailbox model.mailboxName)
|
, Route.Mailbox model.mailboxName
|
||||||
|
|> model.session.router.toPath
|
||||||
|
|> Nav.replaceUrl model.session.key
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -435,7 +442,7 @@ updateMarkMessageSeen model =
|
|||||||
| state =
|
| state =
|
||||||
ShowingList newMessages (ShowingMessage { visibleMessage | seen = True })
|
ShowingList newMessages (ShowingMessage { visibleMessage | seen = True })
|
||||||
}
|
}
|
||||||
, Api.markMessageSeen MarkSeenLoaded visibleMessage.mailbox visibleMessage.id
|
, Api.markMessageSeen model.session MarkSeenLoaded visibleMessage.mailbox visibleMessage.id
|
||||||
)
|
)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
@@ -449,7 +456,7 @@ updateOpenMessage model id =
|
|||||||
{ model | session = Session.addRecent model.mailboxName model.session }
|
{ model | session = Session.addRecent model.mailboxName model.session }
|
||||||
in
|
in
|
||||||
( updateSelected newModel id
|
( updateSelected newModel id
|
||||||
, Api.getMessage MessageLoaded model.mailboxName id
|
, Api.getMessage model.session MessageLoaded model.mailboxName id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -503,10 +510,10 @@ view model =
|
|||||||
)
|
)
|
||||||
|
|
||||||
ShowingList _ (ShowingMessage message) ->
|
ShowingList _ (ShowingMessage message) ->
|
||||||
viewMessage model.session.zone message model.bodyMode
|
viewMessage model.session model.session.zone message model.bodyMode
|
||||||
|
|
||||||
ShowingList _ (Transitioning message) ->
|
ShowingList _ (Transitioning message) ->
|
||||||
viewMessage model.session.zone message model.bodyMode
|
viewMessage model.session model.session.zone message model.bodyMode
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
text ""
|
text ""
|
||||||
@@ -564,14 +571,14 @@ messageChip model selected message =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
viewMessage : Time.Zone -> Message -> Body -> Html Msg
|
viewMessage : Session -> Time.Zone -> Message -> Body -> Html Msg
|
||||||
viewMessage zone message bodyMode =
|
viewMessage session zone message bodyMode =
|
||||||
let
|
let
|
||||||
htmlUrl =
|
htmlUrl =
|
||||||
Api.serveUrl [ "mailbox", message.mailbox, message.id, "html" ]
|
Api.serveUrl session [ "mailbox", message.mailbox, message.id, "html" ]
|
||||||
|
|
||||||
sourceUrl =
|
sourceUrl =
|
||||||
Api.serveUrl [ "mailbox", message.mailbox, message.id, "source" ]
|
Api.serveUrl session [ "mailbox", message.mailbox, message.id, "source" ]
|
||||||
|
|
||||||
htmlButton =
|
htmlButton =
|
||||||
if message.html == "" then
|
if message.html == "" then
|
||||||
@@ -602,7 +609,7 @@ viewMessage zone message bodyMode =
|
|||||||
]
|
]
|
||||||
, messageErrors message
|
, messageErrors message
|
||||||
, messageBody message bodyMode
|
, messageBody message bodyMode
|
||||||
, attachments message
|
, attachments session message
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -665,20 +672,20 @@ messageBody message bodyMode =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
attachments : Message -> Html Msg
|
attachments : Session -> Message -> Html Msg
|
||||||
attachments message =
|
attachments session message =
|
||||||
if List.isEmpty message.attachments then
|
if List.isEmpty message.attachments then
|
||||||
div [] []
|
div [] []
|
||||||
|
|
||||||
else
|
else
|
||||||
table [ class "attachments well" ] (List.map (attachmentRow message) message.attachments)
|
table [ class "attachments well" ] (List.map (attachmentRow session message) message.attachments)
|
||||||
|
|
||||||
|
|
||||||
attachmentRow : Message -> Message.Attachment -> Html Msg
|
attachmentRow : Session -> Message -> Message.Attachment -> Html Msg
|
||||||
attachmentRow message attach =
|
attachmentRow session message attach =
|
||||||
let
|
let
|
||||||
url =
|
url =
|
||||||
Api.serveUrl
|
Api.serveUrl session
|
||||||
[ "mailbox"
|
[ "mailbox"
|
||||||
, message.mailbox
|
, message.mailbox
|
||||||
, message.id
|
, message.id
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
module Page.Monitor exposing (Model, Msg, init, update, view)
|
module Page.Monitor exposing (Model, Msg, init, update, view)
|
||||||
|
|
||||||
|
import Api
|
||||||
|
import Browser.Navigation as Nav
|
||||||
import Data.MessageHeader as MessageHeader exposing (MessageHeader)
|
import Data.MessageHeader as MessageHeader exposing (MessageHeader)
|
||||||
import Data.Session as Session exposing (Session)
|
import Data.Session as Session exposing (Session)
|
||||||
import DateFormat as DF
|
import DateFormat as DF
|
||||||
@@ -21,7 +23,7 @@ import Html
|
|||||||
, thead
|
, thead
|
||||||
, tr
|
, tr
|
||||||
)
|
)
|
||||||
import Html.Attributes exposing (class, tabindex)
|
import Html.Attributes exposing (class, src, tabindex)
|
||||||
import Html.Events as Events
|
import Html.Events as Events
|
||||||
import Json.Decode as D
|
import Json.Decode as D
|
||||||
import Route
|
import Route
|
||||||
@@ -101,7 +103,9 @@ update msg model =
|
|||||||
openMessage : MessageHeader -> Model -> ( Model, Cmd Msg )
|
openMessage : MessageHeader -> Model -> ( Model, Cmd Msg )
|
||||||
openMessage header model =
|
openMessage header model =
|
||||||
( model
|
( model
|
||||||
, Route.pushUrl model.session.key (Route.Message header.mailbox header.id)
|
, Route.Message header.mailbox header.id
|
||||||
|
|> model.session.router.toPath
|
||||||
|
|> Nav.replaceUrl model.session.key
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -132,8 +136,12 @@ view model =
|
|||||||
[ button [ Events.onClick Clear ] [ text "Clear" ]
|
[ button [ Events.onClick Clear ] [ text "Clear" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
-- monitor-messages maintains a websocket connection to the Inbucket daemon at the path
|
||||||
|
-- specified by `src`.
|
||||||
, node "monitor-messages"
|
, node "monitor-messages"
|
||||||
[ Events.on "connected" (D.map Connected <| D.at [ "detail" ] <| D.bool)
|
[ src (Api.monitorUri model.session)
|
||||||
|
, Events.on "connected" (D.map Connected <| D.at [ "detail" ] <| D.bool)
|
||||||
, Events.on "message" (D.map MessageReceived D.value)
|
, Events.on "message" (D.map MessageReceived D.value)
|
||||||
]
|
]
|
||||||
[]
|
[]
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ init session =
|
|||||||
}
|
}
|
||||||
, Cmd.batch
|
, Cmd.batch
|
||||||
[ Task.perform Tick Time.now
|
[ Task.perform Tick Time.now
|
||||||
, Api.getServerConfig ServerConfigLoaded
|
, Api.getServerConfig session ServerConfigLoaded
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ update msg model =
|
|||||||
)
|
)
|
||||||
|
|
||||||
Tick time ->
|
Tick time ->
|
||||||
( { model | now = time }, Api.getServerMetrics MetricsReceived )
|
( { model | now = time }, Api.getServerMetrics model.session MetricsReceived )
|
||||||
|
|
||||||
|
|
||||||
{-| Update all metrics in Model; increment xCounter.
|
{-| Update all metrics in Model; increment xCounter.
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
module Route exposing (Route(..), fromUrl, href, pushUrl, replaceUrl)
|
module Route exposing (Route(..), Router, newRouter)
|
||||||
|
|
||||||
import Browser.Navigation as Navigation exposing (Key)
|
|
||||||
import Html exposing (Attribute)
|
|
||||||
import Html.Attributes as Attr
|
|
||||||
import Url exposing (Url)
|
import Url exposing (Url)
|
||||||
import Url.Builder as Builder
|
import Url.Builder as Builder
|
||||||
import Url.Parser as Parser exposing ((</>), Parser, map, oneOf, s, string, top)
|
import Url.Parser as Parser exposing ((</>), Parser, map, oneOf, s, string, top)
|
||||||
@@ -17,6 +14,25 @@ type Route
|
|||||||
| Status
|
| Status
|
||||||
|
|
||||||
|
|
||||||
|
type alias Router =
|
||||||
|
{ fromUrl : Url -> Route
|
||||||
|
, toPath : Route -> String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Returns a configured Router.
|
||||||
|
-}
|
||||||
|
newRouter : String -> Router
|
||||||
|
newRouter basePath =
|
||||||
|
let
|
||||||
|
newPath =
|
||||||
|
prepareBasePath basePath
|
||||||
|
in
|
||||||
|
{ fromUrl = fromUrl newPath
|
||||||
|
, toPath = toPath newPath
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
{-| Routes our application handles.
|
{-| Routes our application handles.
|
||||||
-}
|
-}
|
||||||
routes : List (Parser (Route -> a) a)
|
routes : List (Parser (Route -> a) a)
|
||||||
@@ -29,10 +45,26 @@ routes =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Returns the Route for a given URL.
|
||||||
|
-}
|
||||||
|
fromUrl : String -> Url -> Route
|
||||||
|
fromUrl basePath url =
|
||||||
|
let
|
||||||
|
relative =
|
||||||
|
{ url | path = String.replace basePath "" url.path }
|
||||||
|
in
|
||||||
|
case Parser.parse (oneOf routes) relative of
|
||||||
|
Nothing ->
|
||||||
|
Unknown url.path
|
||||||
|
|
||||||
|
Just route ->
|
||||||
|
route
|
||||||
|
|
||||||
|
|
||||||
{-| Convert route to a URI.
|
{-| Convert route to a URI.
|
||||||
-}
|
-}
|
||||||
routeToPath : Route -> String
|
toPath : String -> Route -> String
|
||||||
routeToPath page =
|
toPath basePath page =
|
||||||
let
|
let
|
||||||
pieces =
|
pieces =
|
||||||
case page of
|
case page of
|
||||||
@@ -54,35 +86,32 @@ routeToPath page =
|
|||||||
Status ->
|
Status ->
|
||||||
[ "status" ]
|
[ "status" ]
|
||||||
in
|
in
|
||||||
Builder.absolute pieces []
|
basePath ++ Builder.absolute pieces []
|
||||||
|
|
||||||
|
|
||||||
|
{-| Make sure basePath starts with a slash and does not have trailing slashes.
|
||||||
|
|
||||||
-- PUBLIC HELPERS
|
"inbucket/" becomes "/inbucket", "" remains ""
|
||||||
|
|
||||||
|
|
||||||
href : Route -> Attribute msg
|
|
||||||
href route =
|
|
||||||
Attr.href (routeToPath route)
|
|
||||||
|
|
||||||
|
|
||||||
replaceUrl : Key -> Route -> Cmd msg
|
|
||||||
replaceUrl key =
|
|
||||||
routeToPath >> Navigation.replaceUrl key
|
|
||||||
|
|
||||||
|
|
||||||
pushUrl : Key -> Route -> Cmd msg
|
|
||||||
pushUrl key =
|
|
||||||
routeToPath >> Navigation.pushUrl key
|
|
||||||
|
|
||||||
|
|
||||||
{-| Returns the Route for a given URL.
|
|
||||||
-}
|
-}
|
||||||
fromUrl : Url -> Route
|
prepareBasePath : String -> String
|
||||||
fromUrl location =
|
prepareBasePath path =
|
||||||
case Parser.parse (oneOf routes) location of
|
let
|
||||||
Nothing ->
|
stripSlashes str =
|
||||||
Unknown location.path
|
if String.startsWith "/" str then
|
||||||
|
stripSlashes (String.dropLeft 1 str)
|
||||||
|
|
||||||
Just route ->
|
else if String.endsWith "/" str then
|
||||||
route
|
stripSlashes (String.dropRight 1 str)
|
||||||
|
|
||||||
|
else
|
||||||
|
str
|
||||||
|
|
||||||
|
newPath =
|
||||||
|
stripSlashes path
|
||||||
|
in
|
||||||
|
if newPath == "" then
|
||||||
|
""
|
||||||
|
|
||||||
|
else
|
||||||
|
"/" ++ newPath
|
||||||
|
|||||||
@@ -3,22 +3,55 @@
|
|||||||
customElements.define(
|
customElements.define(
|
||||||
'monitor-messages',
|
'monitor-messages',
|
||||||
class MonitorMessages extends HTMLElement {
|
class MonitorMessages extends HTMLElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return [ 'src' ]
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const self = super()
|
super()
|
||||||
// TODO make URI/URL configurable.
|
this._url = null // Current websocket URL.
|
||||||
var uri = '/api/v1/monitor/messages'
|
this._socket = null // Currently open WebSocket.
|
||||||
self._url = ((window.location.protocol === 'https:') ? 'wss://' : 'ws://') + window.location.host + uri
|
|
||||||
self._socket = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
if (this.hasAttribute('src')) {
|
||||||
|
this.wsOpen(this.getAttribute('src'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback() {
|
||||||
|
// Checking _socket prevents connection attempts prior to connectedCallback().
|
||||||
|
if (this._socket && this.hasAttribute('src')) {
|
||||||
|
this.wsOpen(this.getAttribute('src'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.wsClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connects to WebSocket and registers event listeners.
|
||||||
|
wsOpen(uri) {
|
||||||
|
const url =
|
||||||
|
((window.location.protocol === 'https:') ? 'wss://' : 'ws://') +
|
||||||
|
window.location.host + uri
|
||||||
|
if (this._socket && url === this._url) {
|
||||||
|
// Already connected to same URL.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.wsClose()
|
||||||
|
this._url = url
|
||||||
|
|
||||||
|
console.info("Connecting to WebSocket", url)
|
||||||
|
const ws = new WebSocket(url)
|
||||||
|
this._socket = ws
|
||||||
|
|
||||||
|
// Register event listeners.
|
||||||
const self = this
|
const self = this
|
||||||
self._socket = new WebSocket(self._url)
|
ws.addEventListener('open', function (_e) {
|
||||||
var ws = self._socket
|
|
||||||
ws.addEventListener('open', function (e) {
|
|
||||||
self.dispatchEvent(new CustomEvent('connected', { detail: true }))
|
self.dispatchEvent(new CustomEvent('connected', { detail: true }))
|
||||||
})
|
})
|
||||||
ws.addEventListener('close', function (e) {
|
ws.addEventListener('close', function (_e) {
|
||||||
self.dispatchEvent(new CustomEvent('connected', { detail: false }))
|
self.dispatchEvent(new CustomEvent('connected', { detail: false }))
|
||||||
})
|
})
|
||||||
ws.addEventListener('message', function (e) {
|
ws.addEventListener('message', function (e) {
|
||||||
@@ -28,11 +61,20 @@ customElements.define(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
// Closes WebSocket connection.
|
||||||
var ws = this._socket
|
wsClose() {
|
||||||
|
const ws = this._socket
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.close()
|
ws.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get src() {
|
||||||
|
return this.getAttribute('src')
|
||||||
|
}
|
||||||
|
|
||||||
|
set src(value) {
|
||||||
|
this.setAttribute('src', value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module.exports = (env, argv) => {
|
|||||||
const config = {
|
const config = {
|
||||||
output: {
|
output: {
|
||||||
filename: 'static/[name].[hash:8].js',
|
filename: 'static/[name].[hash:8].js',
|
||||||
publicPath: '/',
|
publicPath: '',
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
|||||||
Reference in New Issue
Block a user