From c57260349b5102fb08d36cb19d22a3c448d9aba3 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Mon, 31 Dec 2018 11:46:29 -0800 Subject: [PATCH] web + ui: Pass init cookie from server to client --- pkg/server/web/app_json.go | 5 +++++ pkg/server/web/handlers.go | 10 +++++++++ pkg/server/web/server.go | 21 ++++++++++++++++++- ui/src/Data/AppConfig.elm | 15 ++++++++++++++ ui/src/Data/Session.elm | 18 ++++++++++------ ui/src/Main.elm | 23 ++++++++++++++++++--- ui/src/index.js | 42 ++++++++++++++++++++++++++++++++++++-- 7 files changed, 122 insertions(+), 12 deletions(-) create mode 100644 pkg/server/web/app_json.go create mode 100644 ui/src/Data/AppConfig.elm diff --git a/pkg/server/web/app_json.go b/pkg/server/web/app_json.go new file mode 100644 index 0000000..c80331c --- /dev/null +++ b/pkg/server/web/app_json.go @@ -0,0 +1,5 @@ +package web + +type jsonAppConfig struct { + MonitorVisible bool `json:"monitor-visible"` +} diff --git a/pkg/server/web/handlers.go b/pkg/server/web/handlers.go index feb9a5b..15c65ec 100644 --- a/pkg/server/web/handlers.go +++ b/pkg/server/web/handlers.go @@ -31,6 +31,16 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { } } +// cookieHandler injects an HTTP cookie into the response. +func cookieHandler(cookie *http.Cookie, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + log.Debug().Str("module", "web").Str("remote", req.RemoteAddr).Str("proto", req.Proto). + Str("method", req.Method).Str("path", req.RequestURI).Msg("Injecting cookie") + http.SetCookie(w, cookie) + next.ServeHTTP(w, req) + }) +} + // fileHandler creates a handler that sends the named file regardless of the requested URL. func fileHandler(name string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { diff --git a/pkg/server/web/server.go b/pkg/server/web/server.go index 957b7f7..d6e0b99 100644 --- a/pkg/server/web/server.go +++ b/pkg/server/web/server.go @@ -3,10 +3,12 @@ package web import ( "context" + "encoding/json" "expvar" "net" "net/http" "net/http/pprof" + "net/url" "path/filepath" "time" @@ -75,7 +77,8 @@ func Initialize( fileHandler(filepath.Join(conf.Web.UIDir, "favicon.png"))) // SPA managed paths. - spaHandler := fileHandler(filepath.Join(conf.Web.UIDir, "index.html")) + spaHandler := cookieHandler(appConfigCookie(conf.Web), + fileHandler(filepath.Join(conf.Web.UIDir, "index.html"))) Router.Path("/").Handler(spaHandler) Router.Path("/monitor").Handler(spaHandler) Router.Path("/status").Handler(spaHandler) @@ -126,6 +129,22 @@ func Start(ctx context.Context) { } } +func appConfigCookie(webConfig config.Web) *http.Cookie { + o := &jsonAppConfig{ + MonitorVisible: webConfig.MonitorVisible, + } + b, err := json.Marshal(o) + if err != nil { + log.Error().Str("module", "web").Str("phase", "startup").Err(err). + Msg("Failed to convert app-config to JSON") + } + return &http.Cookie{ + Name: "app-config", + Value: url.PathEscape(string(b)), + Path: "/", + } +} + // serve begins serving HTTP requests func serve(ctx context.Context) { // server.Serve blocks until we close the listener diff --git a/ui/src/Data/AppConfig.elm b/ui/src/Data/AppConfig.elm new file mode 100644 index 0000000..d86dbe9 --- /dev/null +++ b/ui/src/Data/AppConfig.elm @@ -0,0 +1,15 @@ +module Data.AppConfig exposing (AppConfig, decoder) + +import Json.Decode as D +import Json.Decode.Pipeline as P + + +type alias AppConfig = + { monitorVisible : Bool + } + + +decoder : D.Decoder AppConfig +decoder = + D.succeed AppConfig + |> P.required "monitor-visible" D.bool diff --git a/ui/src/Data/Session.elm b/ui/src/Data/Session.elm index 3821292..4e2d72b 100644 --- a/ui/src/Data/Session.elm +++ b/ui/src/Data/Session.elm @@ -4,11 +4,11 @@ module Data.Session exposing , Session , addRecent , clearFlash - , decodeValueWithDefault , decoder , disableRouting , enableRouting , init + , initError , showFlash ) @@ -54,6 +54,17 @@ init key location persistent = } +initError : Nav.Key -> Url -> String -> Session +initError key location error = + { key = key + , host = location.host + , flash = Just (Flash "Initialization failed" [ ( "Error", error ) ]) + , routing = True + , zone = Time.utc + , persistent = Persistent [] + } + + addRecent : String -> Session -> Session addRecent mailbox session = if List.head session.persistent.recentMailboxes == Just mailbox then @@ -99,11 +110,6 @@ decoder = |> optional "recentMailboxes" (D.list D.string) [] -decodeValueWithDefault : D.Value -> Persistent -decodeValueWithDefault = - D.decodeValue decoder >> Result.withDefault { recentMailboxes = [] } - - encode : Persistent -> E.Value encode persistent = E.object diff --git a/ui/src/Main.elm b/ui/src/Main.elm index 30c207a..7bf5b04 100644 --- a/ui/src/Main.elm +++ b/ui/src/Main.elm @@ -2,7 +2,8 @@ module Main exposing (main) import Browser exposing (Document, UrlRequest) import Browser.Navigation as Nav -import Data.Session as Session exposing (Session, decoder) +import Data.AppConfig as AppConfig exposing (AppConfig) +import Data.Session as Session exposing (Session) import Html exposing (..) import Json.Decode as D exposing (Value) import Page.Home as Home @@ -34,11 +35,27 @@ type alias Model = } +type alias InitConfig = + { appConfig : AppConfig + , session : Session.Persistent + } + + init : Value -> Url -> Nav.Key -> ( Model, Cmd Msg ) -init sessionValue location key = +init configValue location key = let + configDecoder = + D.map2 InitConfig + (D.field "app-config" AppConfig.decoder) + (D.field "session" Session.decoder) + session = - Session.init key location (Session.decodeValueWithDefault sessionValue) + case D.decodeValue configDecoder configValue of + Ok config -> + Session.init key location config.session + + Err error -> + Session.initError key location (D.errorToString error) ( subModel, _ ) = Home.init session diff --git a/ui/src/index.js b/ui/src/index.js index 6d5551e..85ded58 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -4,10 +4,16 @@ import { Elm } from './Main.elm' import registerMonitorPorts from './registerMonitor' import './renderedHtml' +// Initial configuration from Inbucket server to Elm App. +var flags = { + "app-config": appConfig(), + "session": sessionObject(), +} + // App startup. var app = Elm.Main.init({ node: document.getElementById('root'), - flags: sessionObject() + flags: flags, }) // Message monitor. @@ -24,9 +30,24 @@ window.addEventListener("storage", function (event) { } }, false) +// Decode the JSON value of the app-config cookie, then delete it. +function appConfig() { + var name = "app-config" + var c = getCookie(name) + if (c) { + deleteCookie(name) + return JSON.parse(decodeURIComponent(c)) + } + console.warn("Inbucket " + name + " cookie not found, running with defaults.") + return { + "monitor-visible": true, + } +} + +// Grab peristent session data out of local storage. function sessionObject() { - var s = localStorage.session try { + var s = localStorage.session if (s) { return JSON.parse(s) } @@ -35,3 +56,20 @@ function sessionObject() { } return null } + +function getCookie(cookieName) { + var name = cookieName + "=" + var cookies = decodeURIComponent(document.cookie).split(';') + for (var i=0; i