1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2026-02-03 17:05:57 +00:00

ui: Add request context for error flash

- webui: Update mailbox, attachment paths
This commit is contained in:
James Hillyerd
2018-12-15 20:16:20 -08:00
parent 6fd13a5215
commit caec5e7c17
12 changed files with 247 additions and 86 deletions

View File

@@ -12,12 +12,12 @@ func SetupRoutes(r *mux.Router) {
web.Handler(RootGreeting)).Name("RootGreeting").Methods("GET") web.Handler(RootGreeting)).Name("RootGreeting").Methods("GET")
r.Path("/status").Handler( r.Path("/status").Handler(
web.Handler(RootStatus)).Name("RootStatus").Methods("GET") web.Handler(RootStatus)).Name("RootStatus").Methods("GET")
r.Path("/m/{name}/{id}").Handler( r.Path("/mailbox/{name}/{id}").Handler(
web.Handler(MailboxMessage)).Name("MailboxMessage").Methods("GET") web.Handler(MailboxMessage)).Name("MailboxMessage").Methods("GET")
r.Path("/m/{name}/{id}/html").Handler( r.Path("/mailbox/{name}/{id}/html").Handler(
web.Handler(MailboxHTML)).Name("MailboxHTML").Methods("GET") web.Handler(MailboxHTML)).Name("MailboxHTML").Methods("GET")
r.Path("/m/{name}/{id}/source").Handler( r.Path("/mailbox/{name}/{id}/source").Handler(
web.Handler(MailboxSource)).Name("MailboxSource").Methods("GET") web.Handler(MailboxSource)).Name("MailboxSource").Methods("GET")
r.Path("/m/attach/{name}/{id}/{num}/{file}").Handler( r.Path("/mailbox/{name}/{id}/attach/{num}/{file}").Handler(
web.Handler(MailboxViewAttach)).Name("MailboxViewAttach").Methods("GET") web.Handler(MailboxViewAttach)).Name("MailboxViewAttach").Methods("GET")
} }

13
ui/package-lock.json generated
View File

@@ -3224,7 +3224,8 @@
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@@ -3639,7 +3640,8 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.1", "version": "5.1.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@@ -3695,6 +3697,7 @@
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@@ -3738,12 +3741,14 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.2", "version": "3.0.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
} }
} }
}, },

View File

@@ -7,6 +7,7 @@ module Api exposing
, getServerMetrics , getServerMetrics
, markMessageSeen , markMessageSeen
, purgeMailbox , purgeMailbox
, serveUrl
) )
import Data.Message as Message exposing (Message) import Data.Message as Message exposing (Message)
@@ -21,11 +22,11 @@ import Url.Builder
type alias DataResult msg data = type alias DataResult msg data =
Result Http.Error data -> msg Result HttpUtil.Error data -> msg
type alias HttpResult msg = type alias HttpResult msg =
Result Http.Error () -> msg Result HttpUtil.Error () -> msg
{-| Builds a public REST API URL (see wiki). {-| Builds a public REST API URL (see wiki).
@@ -49,41 +50,71 @@ deleteMessage msg mailboxName id =
getHeaderList : DataResult msg (List MessageHeader) -> String -> Cmd msg getHeaderList : DataResult msg (List MessageHeader) -> String -> Cmd msg
getHeaderList msg mailboxName = getHeaderList msg mailboxName =
let
context =
{ method = "GET"
, url = apiV1Url [ "mailbox", mailboxName ]
}
in
Http.get Http.get
{ url = apiV1Url [ "mailbox", mailboxName ] { url = context.url
, expect = Http.expectJson msg (Decode.list MessageHeader.decoder) , expect = HttpUtil.expectJson context msg (Decode.list MessageHeader.decoder)
} }
getGreeting : DataResult msg String -> Cmd msg getGreeting : DataResult msg String -> Cmd msg
getGreeting msg = getGreeting msg =
let
context =
{ method = "GET"
, url = serveUrl [ "greeting" ]
}
in
Http.get Http.get
{ url = serveUrl [ "greeting" ] { url = context.url
, expect = Http.expectString msg , expect = HttpUtil.expectString context msg
} }
getMessage : DataResult msg Message -> String -> String -> Cmd msg getMessage : DataResult msg Message -> String -> String -> Cmd msg
getMessage msg mailboxName id = getMessage msg mailboxName id =
let
context =
{ method = "GET"
, url = serveUrl [ "mailbox", mailboxName, id ]
}
in
Http.get Http.get
{ url = serveUrl [ "m", mailboxName, id ] { url = context.url
, expect = Http.expectJson msg Message.decoder , expect = HttpUtil.expectJson context msg Message.decoder
} }
getServerConfig : DataResult msg ServerConfig -> Cmd msg getServerConfig : DataResult msg ServerConfig -> Cmd msg
getServerConfig msg = getServerConfig msg =
let
context =
{ method = "GET"
, url = serveUrl [ "status" ]
}
in
Http.get Http.get
{ url = serveUrl [ "status" ] { url = context.url
, expect = Http.expectJson msg ServerConfig.decoder , expect = HttpUtil.expectJson context msg ServerConfig.decoder
} }
getServerMetrics : DataResult msg Metrics -> Cmd msg getServerMetrics : DataResult msg Metrics -> Cmd msg
getServerMetrics msg = getServerMetrics msg =
let
context =
{ method = "GET"
, url = Url.Builder.absolute [ "debug", "vars" ] []
}
in
Http.get Http.get
{ url = Url.Builder.absolute [ "debug", "vars" ] [] { url = context.url
, expect = Http.expectJson msg Metrics.decoder , expect = HttpUtil.expectJson context msg Metrics.decoder
} }

View File

@@ -1,5 +1,6 @@
module Data.Session exposing module Data.Session exposing
( Msg(..) ( Flash
, Msg(..)
, Persistent , Persistent
, Session , Session
, decodeValueWithDefault , decodeValueWithDefault
@@ -10,6 +11,7 @@ module Data.Session exposing
) )
import Browser.Navigation as Nav import Browser.Navigation as Nav
import Html exposing (Html)
import Json.Decode as D import Json.Decode as D
import Json.Decode.Pipeline exposing (..) import Json.Decode.Pipeline exposing (..)
import Json.Encode as E import Json.Encode as E
@@ -21,13 +23,19 @@ import Url exposing (Url)
type alias Session = type alias Session =
{ key : Nav.Key { key : Nav.Key
, host : String , host : String
, flash : String , flash : Maybe Flash
, routing : Bool , routing : Bool
, zone : Time.Zone , zone : Time.Zone
, persistent : Persistent , persistent : Persistent
} }
type alias Flash =
{ title : String
, table : List ( String, String )
}
type alias Persistent = type alias Persistent =
{ recentMailboxes : List String { recentMailboxes : List String
} }
@@ -35,7 +43,7 @@ type alias Persistent =
type Msg type Msg
= None = None
| SetFlash String | SetFlash Flash
| ClearFlash | ClearFlash
| DisableRouting | DisableRouting
| EnableRouting | EnableRouting
@@ -46,7 +54,7 @@ init : Nav.Key -> Url -> Persistent -> Session
init key location persistent = init key location persistent =
{ key = key { key = key
, host = location.host , host = location.host
, flash = "" , flash = Nothing
, routing = True , routing = True
, zone = Time.utc , zone = Time.utc
, persistent = persistent , persistent = persistent
@@ -62,10 +70,10 @@ update msg session =
session session
SetFlash flash -> SetFlash flash ->
{ session | flash = flash } { session | flash = Just flash }
ClearFlash -> ClearFlash ->
{ session | flash = "" } { session | flash = Nothing }
DisableRouting -> DisableRouting ->
{ session | routing = False } { session | routing = False }

View File

@@ -1,48 +1,133 @@
module HttpUtil exposing (delete, errorString, patch) module HttpUtil exposing (Error, RequestContext, delete, errorFlash, expectJson, expectString, patch)
import Data.Session as Session
import Html exposing (Html, div, text)
import Http import Http
import Json.Decode as Decode
delete : (Result Http.Error () -> msg) -> String -> Cmd msg type alias Error =
{ error : Http.Error
, request : RequestContext
}
type alias RequestContext =
{ method : String
, url : String
}
delete : (Result Error () -> msg) -> String -> Cmd msg
delete msg url = delete msg url =
Http.request let
context =
{ method = "DELETE" { method = "DELETE"
, url = url
}
in
Http.request
{ method = context.method
, headers = [] , headers = []
, url = url , url = url
, body = Http.emptyBody , body = Http.emptyBody
, expect = Http.expectWhatever msg , expect = expectWhatever context msg
, timeout = Nothing , timeout = Nothing
, tracker = Nothing , tracker = Nothing
} }
patch : (Result Http.Error () -> msg) -> String -> Http.Body -> Cmd msg patch : (Result Error () -> msg) -> String -> Http.Body -> Cmd msg
patch msg url body = patch msg url body =
Http.request let
context =
{ method = "PATCH" { method = "PATCH"
, url = url
}
in
Http.request
{ method = context.method
, headers = [] , headers = []
, url = url , url = url
, body = body , body = body
, expect = Http.expectWhatever msg , expect = expectWhatever context msg
, timeout = Nothing , timeout = Nothing
, tracker = Nothing , tracker = Nothing
} }
errorString : Http.Error -> String errorFlash : Error -> Session.Flash
errorString error = errorFlash error =
case error of let
requestContext flash =
{ flash
| table =
flash.table
++ [ ( "Method", error.request.method )
, ( "URL", error.request.url )
]
}
in
requestContext <|
case error.error of
Http.BadUrl str -> Http.BadUrl str ->
"Bad URL: " ++ str { title = "Bad URL"
, table = [ ( "URL", str ) ]
}
Http.Timeout -> Http.Timeout ->
"HTTP timeout" { title = "HTTP timeout"
, table = []
}
Http.NetworkError -> Http.NetworkError ->
"HTTP Network error" { title = "HTTP Network error"
, table = []
}
Http.BadStatus res -> Http.BadStatus res ->
"Bad HTTP status: " ++ String.fromInt res { title = "Bad HTTP status"
, table = [ ( "Response Code", String.fromInt res ) ]
}
Http.BadBody msg -> Http.BadBody body ->
"Bad HTTP body: " ++ msg { title = "Bad HTTP body"
, table = [ ( "Body", body ) ]
}
expectJson : RequestContext -> (Result Error a -> msg) -> Decode.Decoder a -> Http.Expect msg
expectJson context toMsg decoder =
Http.expectStringResponse toMsg <|
resolve context <|
\string ->
Result.mapError Decode.errorToString (Decode.decodeString decoder string)
expectString : RequestContext -> (Result Error String -> msg) -> Http.Expect msg
expectString context toMsg =
Http.expectStringResponse toMsg (resolve context Ok)
expectWhatever : RequestContext -> (Result Error () -> msg) -> Http.Expect msg
expectWhatever context toMsg =
Http.expectBytesResponse toMsg (resolve context (\_ -> Ok ()))
resolve : RequestContext -> (body -> Result String a) -> Http.Response body -> Result Error a
resolve context toResult response =
case response of
Http.BadUrl_ url ->
Err (Error (Http.BadUrl url) context)
Http.Timeout_ ->
Err (Error Http.Timeout context)
Http.NetworkError_ ->
Err (Error Http.NetworkError context)
Http.BadStatus_ metadata _ ->
Err (Error (Http.BadStatus metadata.statusCode) context)
Http.GoodStatus_ _ body ->
Result.mapError (\x -> Error (Http.BadBody x) context) (toResult body)

View File

@@ -156,7 +156,10 @@ update msg model =
SessionUpdated (Err error) -> SessionUpdated (Err error) ->
( model ( model
, Cmd.none , Cmd.none
, Session.SetFlash ("Error decoding session:\n" ++ D.errorToString error) , Session.SetFlash
{ title = "Error decoding session"
, table = [ ( "Error", D.errorToString error ) ]
}
) )
TimeZoneLoaded zone -> TimeZoneLoaded zone ->
@@ -214,7 +217,13 @@ changeRouteTo route model =
( newModel, newCmd, newSession ) = ( newModel, newCmd, newSession ) =
case route of case route of
Route.Unknown path -> Route.Unknown path ->
( model, Cmd.none, Session.SetFlash ("Unknown route requested: " ++ path) ) ( model
, Cmd.none
, Session.SetFlash
{ title = "Unknown route requested"
, table = [ ( "Path", path ) ]
}
)
Route.Home -> Route.Home ->
Home.init Home.init

View File

@@ -28,7 +28,7 @@ init =
type Msg type Msg
= GreetingLoaded (Result Http.Error String) = GreetingLoaded (Result HttpUtil.Error String)
update : Session -> Msg -> Model -> ( Model, Cmd Msg, Session.Msg ) update : Session -> Msg -> Model -> ( Model, Cmd Msg, Session.Msg )
@@ -38,7 +38,7 @@ update session msg model =
( Model greeting, Cmd.none, Session.none ) ( Model greeting, Cmd.none, Session.none )
GreetingLoaded (Err err) -> GreetingLoaded (Err err) ->
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) ) ( model, Cmd.none, Session.SetFlash (HttpUtil.errorFlash err) )

View File

@@ -125,20 +125,20 @@ subscriptions model =
type Msg type Msg
= ListLoaded (Result Http.Error (List MessageHeader)) = ListLoaded (Result HttpUtil.Error (List MessageHeader))
| ClickMessage MessageID | ClickMessage MessageID
| OpenMessage MessageID | OpenMessage MessageID
| MessageLoaded (Result Http.Error Message) | MessageLoaded (Result HttpUtil.Error Message)
| MessageBody Body | MessageBody Body
| OpenedTime Posix | OpenedTime Posix
| MarkSeenTick Posix | MarkSeenTick Posix
| MarkedSeen (Result Http.Error ()) | MarkedSeen (Result HttpUtil.Error ())
| DeleteMessage Message | DeleteMessage Message
| DeletedMessage (Result Http.Error ()) | DeletedMessage (Result HttpUtil.Error ())
| PurgeMailboxPrompt | PurgeMailboxPrompt
| PurgeMailboxCanceled | PurgeMailboxCanceled
| PurgeMailboxConfirmed | PurgeMailboxConfirmed
| PurgedMailbox (Result Http.Error ()) | PurgedMailbox (Result HttpUtil.Error ())
| OnSearchInput String | OnSearchInput String
| Tick Posix | Tick Posix
@@ -166,7 +166,7 @@ update session msg model =
( model, Cmd.none, Session.none ) ( model, Cmd.none, Session.none )
DeletedMessage (Err err) -> DeletedMessage (Err err) ->
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) ) ( model, Cmd.none, Session.SetFlash (HttpUtil.errorFlash err) )
ListLoaded (Ok headers) -> ListLoaded (Ok headers) ->
case model.state of case model.state of
@@ -188,19 +188,19 @@ update session msg model =
( model, Cmd.none, Session.none ) ( model, Cmd.none, Session.none )
ListLoaded (Err err) -> ListLoaded (Err err) ->
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) ) ( model, Cmd.none, Session.SetFlash (HttpUtil.errorFlash err) )
MarkedSeen (Ok _) -> MarkedSeen (Ok _) ->
( model, Cmd.none, Session.none ) ( model, Cmd.none, Session.none )
MarkedSeen (Err err) -> MarkedSeen (Err err) ->
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) ) ( model, Cmd.none, Session.SetFlash (HttpUtil.errorFlash err) )
MessageLoaded (Ok message) -> MessageLoaded (Ok message) ->
updateMessageResult model message updateMessageResult model message
MessageLoaded (Err err) -> MessageLoaded (Err err) ->
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) ) ( model, Cmd.none, Session.SetFlash (HttpUtil.errorFlash err) )
MessageBody bodyMode -> MessageBody bodyMode ->
( { model | bodyMode = bodyMode }, Cmd.none, Session.none ) ( { model | bodyMode = bodyMode }, Cmd.none, Session.none )
@@ -249,7 +249,7 @@ update session msg model =
( model, Cmd.none, Session.none ) ( model, Cmd.none, Session.none )
PurgedMailbox (Err err) -> PurgedMailbox (Err err) ->
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) ) ( model, Cmd.none, Session.SetFlash (HttpUtil.errorFlash err) )
MarkSeenTick now -> MarkSeenTick now ->
case model.state of case model.state of
@@ -533,7 +533,7 @@ viewMessage : Time.Zone -> Message -> Body -> Html Msg
viewMessage zone message bodyMode = viewMessage zone message bodyMode =
let let
sourceUrl = sourceUrl =
"/serve/m/" ++ message.mailbox ++ "/" ++ message.id ++ "/source" Api.serveUrl [ "mailbox", message.mailbox, message.id, "source" ]
in in
div [] div []
[ div [ class "button-bar" ] [ div [ class "button-bar" ]
@@ -596,22 +596,25 @@ messageBody message bodyMode =
attachments : Message -> Html Msg attachments : Message -> Html Msg
attachments message = attachments message =
let
baseUrl =
"/serve/m/attach/" ++ message.mailbox ++ "/" ++ message.id ++ "/"
in
if List.isEmpty message.attachments then if List.isEmpty message.attachments then
div [] [] div [] []
else else
table [ class "attachments well" ] (List.map (attachmentRow baseUrl) message.attachments) table [ class "attachments well" ] (List.map (attachmentRow message) message.attachments)
attachmentRow : String -> Message.Attachment -> Html Msg attachmentRow : Message -> Message.Attachment -> Html Msg
attachmentRow baseUrl attach = attachmentRow message attach =
let let
url = url =
baseUrl ++ attach.id ++ "/" ++ attach.fileName Api.serveUrl
[ "mailbox"
, message.mailbox
, message.id
, "attach"
, attach.id
, attach.fileName
]
in in
tr [] tr []
[ td [] [ td []

View File

@@ -69,7 +69,13 @@ update session msg model =
( { model | messages = header :: model.messages }, Cmd.none, Session.none ) ( { model | messages = header :: model.messages }, Cmd.none, Session.none )
MessageReceived (Err err) -> MessageReceived (Err err) ->
( model, Cmd.none, Session.SetFlash (D.errorToString err) ) ( model
, Cmd.none
, Session.SetFlash
{ title = "Decoding failed"
, table = [ ( "Error", D.errorToString err ) ]
}
)
OpenMessage header -> OpenMessage header ->
( model ( model

View File

@@ -101,8 +101,8 @@ subscriptions model =
type Msg type Msg
= MetricsReceived (Result Http.Error Metrics) = MetricsReceived (Result HttpUtil.Error Metrics)
| ServerConfigLoaded (Result Http.Error ServerConfig) | ServerConfigLoaded (Result HttpUtil.Error ServerConfig)
| Tick Posix | Tick Posix
@@ -113,13 +113,13 @@ update session msg model =
( updateMetrics metrics model, Cmd.none, Session.none ) ( updateMetrics metrics model, Cmd.none, Session.none )
MetricsReceived (Err err) -> MetricsReceived (Err err) ->
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) ) ( model, Cmd.none, Session.SetFlash (HttpUtil.errorFlash err) )
ServerConfigLoaded (Ok config) -> ServerConfigLoaded (Ok config) ->
( { model | config = Just config }, Cmd.none, Session.none ) ( { model | config = Just config }, Cmd.none, Session.none )
ServerConfigLoaded (Err err) -> ServerConfigLoaded (Err err) ->
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) ) ( model, Cmd.none, Session.SetFlash (HttpUtil.errorFlash err) )
Tick time -> Tick time ->
( { model | now = time }, Api.getServerMetrics MetricsReceived, Session.none ) ( { model | now = time }, Api.getServerMetrics MetricsReceived, Session.none )

View File

@@ -87,18 +87,27 @@ frameModal maybeModal =
text "" text ""
errorFlash : FrameControls msg -> String -> Html msg errorFlash : FrameControls msg -> Maybe Session.Flash -> Html msg
errorFlash controls message = errorFlash controls maybeFlash =
if message == "" then let
row ( heading, message ) =
pre []
[ text heading
, text ": "
, text message
]
in
case maybeFlash of
Nothing ->
text "" text ""
else Just flash ->
div [ class "error" ] div [ class "error" ]
[ div [ class "flash-header" ] [ div [ class "flash-header" ]
[ h2 [] [ text "Error" ] [ h2 [] [ text flash.title ]
, a [ href "#", Events.onClick controls.clearFlash ] [ text "Close" ] , a [ href "#", Events.onClick controls.clearFlash ] [ text "Close" ]
] ]
, pre [] [ text message ] , div [ class "flash-table" ] (List.map row flash.table)
] ]

View File

@@ -158,6 +158,11 @@ h1 {
justify-content: space-between; justify-content: space-between;
} }
.flash-table {
max-width: 90vw;
overflow: auto;
}
.greeting { .greeting {
max-width: 1000px; max-width: 1000px;
} }