1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-17 17:47:03 +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")
r.Path("/status").Handler(
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")
r.Path("/m/{name}/{id}/html").Handler(
r.Path("/mailbox/{name}/{id}/html").Handler(
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")
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")
}

13
ui/package-lock.json generated
View File

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

View File

@@ -7,6 +7,7 @@ module Api exposing
, getServerMetrics
, markMessageSeen
, purgeMailbox
, serveUrl
)
import Data.Message as Message exposing (Message)
@@ -21,11 +22,11 @@ import Url.Builder
type alias DataResult msg data =
Result Http.Error data -> msg
Result HttpUtil.Error data -> msg
type alias HttpResult msg =
Result Http.Error () -> msg
Result HttpUtil.Error () -> msg
{-| 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 msg mailboxName =
let
context =
{ method = "GET"
, url = apiV1Url [ "mailbox", mailboxName ]
}
in
Http.get
{ url = apiV1Url [ "mailbox", mailboxName ]
, expect = Http.expectJson msg (Decode.list MessageHeader.decoder)
{ url = context.url
, expect = HttpUtil.expectJson context msg (Decode.list MessageHeader.decoder)
}
getGreeting : DataResult msg String -> Cmd msg
getGreeting msg =
let
context =
{ method = "GET"
, url = serveUrl [ "greeting" ]
}
in
Http.get
{ url = serveUrl [ "greeting" ]
, expect = Http.expectString msg
{ url = context.url
, expect = HttpUtil.expectString context msg
}
getMessage : DataResult msg Message -> String -> String -> Cmd msg
getMessage msg mailboxName id =
let
context =
{ method = "GET"
, url = serveUrl [ "mailbox", mailboxName, id ]
}
in
Http.get
{ url = serveUrl [ "m", mailboxName, id ]
, expect = Http.expectJson msg Message.decoder
{ url = context.url
, expect = HttpUtil.expectJson context msg Message.decoder
}
getServerConfig : DataResult msg ServerConfig -> Cmd msg
getServerConfig msg =
let
context =
{ method = "GET"
, url = serveUrl [ "status" ]
}
in
Http.get
{ url = serveUrl [ "status" ]
, expect = Http.expectJson msg ServerConfig.decoder
{ url = context.url
, expect = HttpUtil.expectJson context msg ServerConfig.decoder
}
getServerMetrics : DataResult msg Metrics -> Cmd msg
getServerMetrics msg =
let
context =
{ method = "GET"
, url = Url.Builder.absolute [ "debug", "vars" ] []
}
in
Http.get
{ url = Url.Builder.absolute [ "debug", "vars" ] []
, expect = Http.expectJson msg Metrics.decoder
{ url = context.url
, expect = HttpUtil.expectJson context msg Metrics.decoder
}

View File

@@ -1,5 +1,6 @@
module Data.Session exposing
( Msg(..)
( Flash
, Msg(..)
, Persistent
, Session
, decodeValueWithDefault
@@ -10,6 +11,7 @@ module Data.Session exposing
)
import Browser.Navigation as Nav
import Html exposing (Html)
import Json.Decode as D
import Json.Decode.Pipeline exposing (..)
import Json.Encode as E
@@ -21,13 +23,19 @@ import Url exposing (Url)
type alias Session =
{ key : Nav.Key
, host : String
, flash : String
, flash : Maybe Flash
, routing : Bool
, zone : Time.Zone
, persistent : Persistent
}
type alias Flash =
{ title : String
, table : List ( String, String )
}
type alias Persistent =
{ recentMailboxes : List String
}
@@ -35,7 +43,7 @@ type alias Persistent =
type Msg
= None
| SetFlash String
| SetFlash Flash
| ClearFlash
| DisableRouting
| EnableRouting
@@ -46,7 +54,7 @@ init : Nav.Key -> Url -> Persistent -> Session
init key location persistent =
{ key = key
, host = location.host
, flash = ""
, flash = Nothing
, routing = True
, zone = Time.utc
, persistent = persistent
@@ -62,10 +70,10 @@ update msg session =
session
SetFlash flash ->
{ session | flash = flash }
{ session | flash = Just flash }
ClearFlash ->
{ session | flash = "" }
{ session | flash = Nothing }
DisableRouting ->
{ 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 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 =
let
context =
{ method = "DELETE"
, url = url
}
in
Http.request
{ method = "DELETE"
{ method = context.method
, headers = []
, url = url
, body = Http.emptyBody
, expect = Http.expectWhatever msg
, expect = expectWhatever context msg
, timeout = 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 =
let
context =
{ method = "PATCH"
, url = url
}
in
Http.request
{ method = "PATCH"
{ method = context.method
, headers = []
, url = url
, body = body
, expect = Http.expectWhatever msg
, expect = expectWhatever context msg
, timeout = Nothing
, tracker = Nothing
}
errorString : Http.Error -> String
errorString error =
case error of
Http.BadUrl str ->
"Bad URL: " ++ str
errorFlash : Error -> Session.Flash
errorFlash error =
let
requestContext flash =
{ flash
| table =
flash.table
++ [ ( "Method", error.request.method )
, ( "URL", error.request.url )
]
}
in
requestContext <|
case error.error of
Http.BadUrl str ->
{ title = "Bad URL"
, table = [ ( "URL", str ) ]
}
Http.Timeout ->
"HTTP timeout"
Http.Timeout ->
{ title = "HTTP timeout"
, table = []
}
Http.NetworkError ->
"HTTP Network error"
Http.NetworkError ->
{ title = "HTTP Network error"
, table = []
}
Http.BadStatus res ->
"Bad HTTP status: " ++ String.fromInt res
Http.BadStatus res ->
{ title = "Bad HTTP status"
, table = [ ( "Response Code", String.fromInt res ) ]
}
Http.BadBody msg ->
"Bad HTTP body: " ++ msg
Http.BadBody body ->
{ 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) ->
( model
, Cmd.none
, Session.SetFlash ("Error decoding session:\n" ++ D.errorToString error)
, Session.SetFlash
{ title = "Error decoding session"
, table = [ ( "Error", D.errorToString error ) ]
}
)
TimeZoneLoaded zone ->
@@ -214,7 +217,13 @@ changeRouteTo route model =
( newModel, newCmd, newSession ) =
case route of
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 ->
Home.init

View File

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

View File

@@ -69,7 +69,13 @@ update session msg model =
( { model | messages = header :: model.messages }, Cmd.none, Session.none )
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 ->
( model

View File

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

View File

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

View File

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