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:
130
ui/src/Api.elm
130
ui/src/Api.elm
@@ -6,6 +6,7 @@ module Api exposing
|
||||
, getServerConfig
|
||||
, getServerMetrics
|
||||
, markMessageSeen
|
||||
, monitorUri
|
||||
, purgeMailbox
|
||||
, serveUrl
|
||||
)
|
||||
@@ -14,10 +15,12 @@ import Data.Message as Message exposing (Message)
|
||||
import Data.MessageHeader as MessageHeader exposing (MessageHeader)
|
||||
import Data.Metrics as Metrics exposing (Metrics)
|
||||
import Data.ServerConfig as ServerConfig exposing (ServerConfig)
|
||||
import Data.Session exposing (Session)
|
||||
import Http
|
||||
import HttpUtil
|
||||
import Json.Decode as Decode
|
||||
import Json.Encode as Encode
|
||||
import String
|
||||
import Url.Builder
|
||||
|
||||
|
||||
@@ -29,31 +32,17 @@ type alias HttpResult msg =
|
||||
Result HttpUtil.Error () -> msg
|
||||
|
||||
|
||||
{-| Builds a public REST API URL (see wiki).
|
||||
-}
|
||||
apiV1Url : List String -> String
|
||||
apiV1Url elements =
|
||||
Url.Builder.absolute ([ "api", "v1" ] ++ elements) []
|
||||
deleteMessage : Session -> HttpResult msg -> String -> String -> Cmd msg
|
||||
deleteMessage session msg mailboxName id =
|
||||
HttpUtil.delete msg (apiV1Url session [ "mailbox", mailboxName, id ])
|
||||
|
||||
|
||||
{-| Builds an internal `serve` REST API URL; only used by this UI.
|
||||
-}
|
||||
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 =
|
||||
getHeaderList : Session -> DataResult msg (List MessageHeader) -> String -> Cmd msg
|
||||
getHeaderList session msg mailboxName =
|
||||
let
|
||||
context =
|
||||
{ method = "GET"
|
||||
, url = apiV1Url [ "mailbox", mailboxName ]
|
||||
, url = apiV1Url session [ "mailbox", mailboxName ]
|
||||
}
|
||||
in
|
||||
Http.get
|
||||
@@ -62,12 +51,12 @@ getHeaderList msg mailboxName =
|
||||
}
|
||||
|
||||
|
||||
getGreeting : DataResult msg String -> Cmd msg
|
||||
getGreeting msg =
|
||||
getGreeting : Session -> DataResult msg String -> Cmd msg
|
||||
getGreeting session msg =
|
||||
let
|
||||
context =
|
||||
{ method = "GET"
|
||||
, url = serveUrl [ "greeting" ]
|
||||
, url = serveUrl session [ "greeting" ]
|
||||
}
|
||||
in
|
||||
Http.get
|
||||
@@ -76,12 +65,12 @@ getGreeting msg =
|
||||
}
|
||||
|
||||
|
||||
getMessage : DataResult msg Message -> String -> String -> Cmd msg
|
||||
getMessage msg mailboxName id =
|
||||
getMessage : Session -> DataResult msg Message -> String -> String -> Cmd msg
|
||||
getMessage session msg mailboxName id =
|
||||
let
|
||||
context =
|
||||
{ method = "GET"
|
||||
, url = serveUrl [ "mailbox", mailboxName, id ]
|
||||
, url = serveUrl session [ "mailbox", mailboxName, id ]
|
||||
}
|
||||
in
|
||||
Http.get
|
||||
@@ -90,12 +79,12 @@ getMessage msg mailboxName id =
|
||||
}
|
||||
|
||||
|
||||
getServerConfig : DataResult msg ServerConfig -> Cmd msg
|
||||
getServerConfig msg =
|
||||
getServerConfig : Session -> DataResult msg ServerConfig -> Cmd msg
|
||||
getServerConfig session msg =
|
||||
let
|
||||
context =
|
||||
{ method = "GET"
|
||||
, url = serveUrl [ "status" ]
|
||||
, url = serveUrl session [ "status" ]
|
||||
}
|
||||
in
|
||||
Http.get
|
||||
@@ -104,12 +93,19 @@ getServerConfig msg =
|
||||
}
|
||||
|
||||
|
||||
getServerMetrics : DataResult msg Metrics -> Cmd msg
|
||||
getServerMetrics msg =
|
||||
getServerMetrics : Session -> DataResult msg Metrics -> Cmd msg
|
||||
getServerMetrics session msg =
|
||||
let
|
||||
context =
|
||||
{ method = "GET"
|
||||
, url = Url.Builder.absolute [ "debug", "vars" ] []
|
||||
, url =
|
||||
Url.Builder.absolute
|
||||
(splitBasePath session.config.basePath
|
||||
++ [ "debug"
|
||||
, "vars"
|
||||
]
|
||||
)
|
||||
[]
|
||||
}
|
||||
in
|
||||
Http.get
|
||||
@@ -118,15 +114,73 @@ getServerMetrics msg =
|
||||
}
|
||||
|
||||
|
||||
markMessageSeen : HttpResult msg -> String -> String -> Cmd msg
|
||||
markMessageSeen msg mailboxName id =
|
||||
markMessageSeen : Session -> HttpResult msg -> String -> String -> Cmd msg
|
||||
markMessageSeen session msg mailboxName id =
|
||||
-- The URL tells the API which message ID to update, so we only need to indicate the
|
||||
-- desired change in the body.
|
||||
Encode.object [ ( "seen", Encode.bool True ) ]
|
||||
|> Http.jsonBody
|
||||
|> HttpUtil.patch msg (apiV1Url [ "mailbox", mailboxName, id ])
|
||||
|> HttpUtil.patch msg (apiV1Url session [ "mailbox", mailboxName, id ])
|
||||
|
||||
|
||||
purgeMailbox : HttpResult msg -> String -> Cmd msg
|
||||
purgeMailbox msg mailboxName =
|
||||
HttpUtil.delete msg (apiV1Url [ "mailbox", mailboxName ])
|
||||
monitorUri : Session -> String
|
||||
monitorUri session =
|
||||
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 =
|
||||
{ monitorVisible : Bool
|
||||
{ basePath : String
|
||||
, monitorVisible : Bool
|
||||
}
|
||||
|
||||
|
||||
decoder : D.Decoder AppConfig
|
||||
decoder =
|
||||
D.succeed AppConfig
|
||||
|> P.optional "base-path" D.string ""
|
||||
|> P.required "monitor-visible" D.bool
|
||||
|
||||
|
||||
default : AppConfig
|
||||
default =
|
||||
AppConfig True
|
||||
AppConfig "" True
|
||||
|
||||
@@ -18,6 +18,7 @@ import Data.AppConfig as AppConfig exposing (AppConfig)
|
||||
import Json.Decode as D
|
||||
import Json.Decode.Pipeline exposing (optional)
|
||||
import Json.Encode as E
|
||||
import Route exposing (Router)
|
||||
import Time
|
||||
import Url exposing (Url)
|
||||
|
||||
@@ -27,6 +28,7 @@ type alias Session =
|
||||
, host : String
|
||||
, flash : Maybe Flash
|
||||
, routing : Bool
|
||||
, router : Router
|
||||
, zone : Time.Zone
|
||||
, config : AppConfig
|
||||
, persistent : Persistent
|
||||
@@ -50,6 +52,7 @@ init key location config persistent =
|
||||
, host = location.host
|
||||
, flash = Nothing
|
||||
, routing = True
|
||||
, router = Route.newRouter config.basePath
|
||||
, zone = Time.utc
|
||||
, config = config
|
||||
, persistent = persistent
|
||||
@@ -62,6 +65,7 @@ initError key location error =
|
||||
, host = location.host
|
||||
, flash = Just (Flash "Initialization failed" [ ( "Error", error ) ])
|
||||
, routing = True
|
||||
, router = Route.newRouter ""
|
||||
, zone = Time.utc
|
||||
, config = AppConfig.default
|
||||
, persistent = Persistent []
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module Layout exposing (Model, Msg, Page(..), frame, init, reset, update)
|
||||
|
||||
import Browser.Navigation as Nav
|
||||
import Data.Session as Session exposing (Session)
|
||||
import Html
|
||||
exposing
|
||||
@@ -132,7 +133,9 @@ update msg model session =
|
||||
else
|
||||
( model
|
||||
, session
|
||||
, Route.pushUrl session.key (Route.Mailbox model.mailboxName)
|
||||
, Route.Mailbox model.mailboxName
|
||||
|> session.router.toPath
|
||||
|> Nav.pushUrl session.key
|
||||
)
|
||||
|
||||
RecentMenuMouseOver ->
|
||||
@@ -195,14 +198,14 @@ frame { model, session, activePage, activeMailbox, modal, content } =
|
||||
[ button [ class "navbar-toggle", Events.onClick (MainMenuToggled |> model.mapMsg) ]
|
||||
[ i [ class "fas fa-bars" ] [] ]
|
||||
, 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 ) ] ]
|
||||
[ if session.config.monitorVisible then
|
||||
navbarLink Monitor Route.Monitor [ text "Monitor" ] activePage
|
||||
navbarLink Monitor (session.router.toPath Route.Monitor) [ text "Monitor" ] activePage
|
||||
|
||||
else
|
||||
text ""
|
||||
, navbarLink Status Route.Status [ text "Status" ] activePage
|
||||
, navbarLink Status (session.router.toPath Route.Status) [ text "Status" ] activePage
|
||||
, navbarRecent activePage activeMailbox model session
|
||||
, li [ class "navbar-mailbox" ]
|
||||
[ form [ Events.onSubmit (OpenMailbox |> model.mapMsg) ]
|
||||
@@ -260,10 +263,10 @@ externalLink url title =
|
||||
a [ href url, target "_blank", rel "noopener" ] [ text title ]
|
||||
|
||||
|
||||
navbarLink : Page -> Route -> List (Html a) -> Page -> Html a
|
||||
navbarLink page route linkContent activePage =
|
||||
navbarLink : Page -> String -> List (Html a) -> Page -> Html a
|
||||
navbarLink page url linkContent 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.
|
||||
@@ -292,7 +295,7 @@ navbarRecent page activeMailbox model session =
|
||||
session.persistent.recentMailboxes
|
||||
|
||||
recentLink mailbox =
|
||||
a [ Route.href (Route.Mailbox mailbox) ] [ text mailbox ]
|
||||
a [ href <| session.router.toPath <| Route.Mailbox mailbox ] [ text mailbox ]
|
||||
in
|
||||
li
|
||||
[ class "navbar-dropdown-container"
|
||||
|
||||
@@ -66,7 +66,7 @@ init configValue location key =
|
||||
}
|
||||
|
||||
route =
|
||||
Route.fromUrl location
|
||||
session.router.fromUrl location
|
||||
|
||||
( model, cmd ) =
|
||||
changeRouteTo route initModel
|
||||
@@ -167,7 +167,7 @@ updateMain msg model session =
|
||||
UrlChanged url ->
|
||||
-- Responds to new browser URL.
|
||||
if session.routing then
|
||||
changeRouteTo (Route.fromUrl url) model
|
||||
changeRouteTo (session.router.fromUrl url) model
|
||||
|
||||
else
|
||||
-- Skip once, but re-enable routing.
|
||||
|
||||
@@ -20,7 +20,7 @@ type alias Model =
|
||||
|
||||
init : Session -> ( Model, Cmd Msg )
|
||||
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)
|
||||
|
||||
import Api
|
||||
import Browser.Navigation as Nav
|
||||
import Data.Message as Message exposing (Message)
|
||||
import Data.MessageHeader exposing (MessageHeader)
|
||||
import Data.Session as Session exposing (Session)
|
||||
@@ -113,15 +114,15 @@ init session mailboxName selection =
|
||||
, markSeenTimer = Timer.empty
|
||||
, now = Time.millisToPosix 0
|
||||
}
|
||||
, load mailboxName
|
||||
, load session mailboxName
|
||||
)
|
||||
|
||||
|
||||
load : String -> Cmd Msg
|
||||
load mailboxName =
|
||||
load : Session -> String -> Cmd Msg
|
||||
load session mailboxName =
|
||||
Cmd.batch
|
||||
[ 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
|
||||
, Cmd.batch
|
||||
[ -- Update browser location.
|
||||
Route.replaceUrl model.session.key (Route.Message model.mailboxName id)
|
||||
, Api.getMessage MessageLoaded model.mailboxName id
|
||||
Route.Message 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
|
||||
cmd =
|
||||
Cmd.batch
|
||||
[ Route.replaceUrl model.session.key (Route.Mailbox model.mailboxName)
|
||||
, Api.purgeMailbox PurgedMailbox model.mailboxName
|
||||
[ Route.Mailbox model.mailboxName
|
||||
|> model.session.router.toPath
|
||||
|> Nav.replaceUrl model.session.key
|
||||
, Api.purgeMailbox model.session PurgedMailbox model.mailboxName
|
||||
]
|
||||
in
|
||||
case model.state of
|
||||
@@ -405,8 +410,10 @@ updateDeleteMessage model message =
|
||||
ShowingList (filter (\x -> x.id /= message.id) list) NoMessage
|
||||
}
|
||||
, Cmd.batch
|
||||
[ Api.deleteMessage DeletedMessage message.mailbox message.id
|
||||
, Route.replaceUrl model.session.key (Route.Mailbox model.mailboxName)
|
||||
[ Api.deleteMessage model.session DeletedMessage message.mailbox message.id
|
||||
, Route.Mailbox model.mailboxName
|
||||
|> model.session.router.toPath
|
||||
|> Nav.replaceUrl model.session.key
|
||||
]
|
||||
)
|
||||
|
||||
@@ -435,7 +442,7 @@ updateMarkMessageSeen model =
|
||||
| state =
|
||||
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 }
|
||||
in
|
||||
( 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) ->
|
||||
viewMessage model.session.zone message model.bodyMode
|
||||
viewMessage model.session model.session.zone message model.bodyMode
|
||||
|
||||
ShowingList _ (Transitioning message) ->
|
||||
viewMessage model.session.zone message model.bodyMode
|
||||
viewMessage model.session model.session.zone message model.bodyMode
|
||||
|
||||
_ ->
|
||||
text ""
|
||||
@@ -564,14 +571,14 @@ messageChip model selected message =
|
||||
]
|
||||
|
||||
|
||||
viewMessage : Time.Zone -> Message -> Body -> Html Msg
|
||||
viewMessage zone message bodyMode =
|
||||
viewMessage : Session -> Time.Zone -> Message -> Body -> Html Msg
|
||||
viewMessage session zone message bodyMode =
|
||||
let
|
||||
htmlUrl =
|
||||
Api.serveUrl [ "mailbox", message.mailbox, message.id, "html" ]
|
||||
Api.serveUrl session [ "mailbox", message.mailbox, message.id, "html" ]
|
||||
|
||||
sourceUrl =
|
||||
Api.serveUrl [ "mailbox", message.mailbox, message.id, "source" ]
|
||||
Api.serveUrl session [ "mailbox", message.mailbox, message.id, "source" ]
|
||||
|
||||
htmlButton =
|
||||
if message.html == "" then
|
||||
@@ -602,7 +609,7 @@ viewMessage zone message bodyMode =
|
||||
]
|
||||
, messageErrors message
|
||||
, messageBody message bodyMode
|
||||
, attachments message
|
||||
, attachments session message
|
||||
]
|
||||
|
||||
|
||||
@@ -665,20 +672,20 @@ messageBody message bodyMode =
|
||||
]
|
||||
|
||||
|
||||
attachments : Message -> Html Msg
|
||||
attachments message =
|
||||
attachments : Session -> Message -> Html Msg
|
||||
attachments session message =
|
||||
if List.isEmpty message.attachments then
|
||||
div [] []
|
||||
|
||||
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 message attach =
|
||||
attachmentRow : Session -> Message -> Message.Attachment -> Html Msg
|
||||
attachmentRow session message attach =
|
||||
let
|
||||
url =
|
||||
Api.serveUrl
|
||||
Api.serveUrl session
|
||||
[ "mailbox"
|
||||
, message.mailbox
|
||||
, message.id
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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.Session as Session exposing (Session)
|
||||
import DateFormat as DF
|
||||
@@ -21,7 +23,7 @@ import Html
|
||||
, thead
|
||||
, tr
|
||||
)
|
||||
import Html.Attributes exposing (class, tabindex)
|
||||
import Html.Attributes exposing (class, src, tabindex)
|
||||
import Html.Events as Events
|
||||
import Json.Decode as D
|
||||
import Route
|
||||
@@ -101,7 +103,9 @@ update msg model =
|
||||
openMessage : MessageHeader -> Model -> ( Model, Cmd Msg )
|
||||
openMessage header 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" ]
|
||||
]
|
||||
]
|
||||
|
||||
-- monitor-messages maintains a websocket connection to the Inbucket daemon at the path
|
||||
-- specified by `src`.
|
||||
, 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)
|
||||
]
|
||||
[]
|
||||
|
||||
@@ -84,7 +84,7 @@ init session =
|
||||
}
|
||||
, Cmd.batch
|
||||
[ Task.perform Tick Time.now
|
||||
, Api.getServerConfig ServerConfigLoaded
|
||||
, Api.getServerConfig session ServerConfigLoaded
|
||||
]
|
||||
)
|
||||
|
||||
@@ -134,7 +134,7 @@ update msg model =
|
||||
)
|
||||
|
||||
Tick time ->
|
||||
( { model | now = time }, Api.getServerMetrics MetricsReceived )
|
||||
( { model | now = time }, Api.getServerMetrics model.session MetricsReceived )
|
||||
|
||||
|
||||
{-| 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.Builder as Builder
|
||||
import Url.Parser as Parser exposing ((</>), Parser, map, oneOf, s, string, top)
|
||||
@@ -17,6 +14,25 @@ type Route
|
||||
| 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 : 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.
|
||||
-}
|
||||
routeToPath : Route -> String
|
||||
routeToPath page =
|
||||
toPath : String -> Route -> String
|
||||
toPath basePath page =
|
||||
let
|
||||
pieces =
|
||||
case page of
|
||||
@@ -54,35 +86,32 @@ routeToPath page =
|
||||
Status ->
|
||||
[ "status" ]
|
||||
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
|
||||
fromUrl location =
|
||||
case Parser.parse (oneOf routes) location of
|
||||
Nothing ->
|
||||
Unknown location.path
|
||||
prepareBasePath : String -> String
|
||||
prepareBasePath path =
|
||||
let
|
||||
stripSlashes str =
|
||||
if String.startsWith "/" str then
|
||||
stripSlashes (String.dropLeft 1 str)
|
||||
|
||||
Just route ->
|
||||
route
|
||||
else if String.endsWith "/" str then
|
||||
stripSlashes (String.dropRight 1 str)
|
||||
|
||||
else
|
||||
str
|
||||
|
||||
newPath =
|
||||
stripSlashes path
|
||||
in
|
||||
if newPath == "" then
|
||||
""
|
||||
|
||||
else
|
||||
"/" ++ newPath
|
||||
|
||||
@@ -3,22 +3,55 @@
|
||||
customElements.define(
|
||||
'monitor-messages',
|
||||
class MonitorMessages extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return [ 'src' ]
|
||||
}
|
||||
|
||||
constructor() {
|
||||
const self = super()
|
||||
// TODO make URI/URL configurable.
|
||||
var uri = '/api/v1/monitor/messages'
|
||||
self._url = ((window.location.protocol === 'https:') ? 'wss://' : 'ws://') + window.location.host + uri
|
||||
self._socket = null
|
||||
super()
|
||||
this._url = null // Current websocket URL.
|
||||
this._socket = null // Currently open WebSocket.
|
||||
}
|
||||
|
||||
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
|
||||
self._socket = new WebSocket(self._url)
|
||||
var ws = self._socket
|
||||
ws.addEventListener('open', function (e) {
|
||||
ws.addEventListener('open', function (_e) {
|
||||
self.dispatchEvent(new CustomEvent('connected', { detail: true }))
|
||||
})
|
||||
ws.addEventListener('close', function (e) {
|
||||
ws.addEventListener('close', function (_e) {
|
||||
self.dispatchEvent(new CustomEvent('connected', { detail: false }))
|
||||
})
|
||||
ws.addEventListener('message', function (e) {
|
||||
@@ -28,11 +61,20 @@ customElements.define(
|
||||
})
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
var ws = this._socket
|
||||
// Closes WebSocket connection.
|
||||
wsClose() {
|
||||
const ws = this._socket
|
||||
if (ws) {
|
||||
ws.close()
|
||||
}
|
||||
}
|
||||
|
||||
get src() {
|
||||
return this.getAttribute('src')
|
||||
}
|
||||
|
||||
set src(value) {
|
||||
this.setAttribute('src', value)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user