1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-19 02:27:03 +00:00

ui: Re-implement websockets with ports+JS

This commit is contained in:
James Hillyerd
2018-11-13 21:26:37 -08:00
parent ac3a94412d
commit ecd0c124d4
11 changed files with 188 additions and 79 deletions

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="manifest" href="/manifest.json"><link rel="shortcut icon" href="/favicon.png" type="image/png"><title>Inbucket</title><link href="/static/css/main.8d438738.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script type="text/javascript" src="/static/js/main.d6280c5d.js"></script></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="manifest" href="/manifest.json"><link rel="shortcut icon" href="/favicon.png" type="image/png"><title>Inbucket</title><link href="/static/css/main.8d438738.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script type="text/javascript" src="/static/js/main.04b41a91.js"></script></body></html>

View File

@@ -1 +1 @@
"use strict";var precacheConfig=[["/index.html","3beb8060f28c17789ca66e2c6616160f"],["/static/css/main.8d438738.css","8d438738f900913d8f787dc7ef9b05f9"],["/static/js/main.d6280c5d.js","55b008360cb9147787b0a8ed62c0783d"]],cacheName="sw-precache-v3-sw-precache-webpack-plugin-"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,n,r){var a=new URL(e);return r&&a.pathname.match(r)||(a.search+=(a.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),a.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.hash="",n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],r=new URL(t,self.location),a=createCacheKey(r,hashParamName,n,/\.\w{8}\./);return[r.toString(),a]}));function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n)){var r=new Request(n,{credentials:"same-origin"});return fetch(r).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);(t=urlsToCacheKeys.has(n))||(n=addDirectoryIndex(n,"index.html"),t=urlsToCacheKeys.has(n));!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^(?!\\/__).*"],e.request.url)&&(n=new URL("/index.html",self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}); "use strict";var precacheConfig=[["/index.html","ed9e929e14fae109b0cd008ba5adb822"],["/static/css/main.8d438738.css","8d438738f900913d8f787dc7ef9b05f9"],["/static/js/main.04b41a91.js","c9dc7ec55e7e303354fc7423a2afaa38"]],cacheName="sw-precache-v3-sw-precache-webpack-plugin-"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,n,r){var a=new URL(e);return r&&a.pathname.match(r)||(a.search+=(a.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),a.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.hash="",n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],r=new URL(t,self.location),a=createCacheKey(r,hashParamName,n,/\.\w{8}\./);return[r.toString(),a]}));function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n)){var r=new Request(n,{credentials:"same-origin"});return fetch(r).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);(t=urlsToCacheKeys.has(n))||(n=addDirectoryIndex(n,"index.html"),t=urlsToCacheKeys.has(n));!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^(?!\\/__).*"],e.request.url)&&(n=new URL("/index.html",self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"main":{"js":"/static/js/main.d6280c5d.js","css":"/static/css/main.8d438738.css"},"":{"html":"/index.html"}} {"main":{"js":"/static/js/main.04b41a91.js","css":"/static/css/main.8d438738.css"},"":{"html":"/index.html"}}

View File

@@ -27,7 +27,6 @@
"elm-lang/http": "1.0.0 <= v < 2.0.0", "elm-lang/http": "1.0.0 <= v < 2.0.0",
"elm-lang/navigation": "2.1.0 <= v < 3.0.0", "elm-lang/navigation": "2.1.0 <= v < 3.0.0",
"elm-lang/svg": "2.0.0 <= v < 3.0.0", "elm-lang/svg": "2.0.0 <= v < 3.0.0",
"elm-lang/websocket": "1.0.2 <= v < 2.0.0",
"evancz/url-parser": "2.0.1 <= v < 3.0.0", "evancz/url-parser": "2.0.1 <= v < 3.0.0",
"jweir/sparkline": "3.0.0 <= v < 4.0.0", "jweir/sparkline": "3.0.0 <= v < 4.0.0",
"ryannhg/elm-date-format": "2.1.2 <= v < 3.0.0" "ryannhg/elm-date-format": "2.1.2 <= v < 3.0.0"

View File

@@ -180,6 +180,8 @@ updatePage msg model =
setRoute : Route -> Model -> ( Model, Cmd Msg, Session.Msg ) setRoute : Route -> Model -> ( Model, Cmd Msg, Session.Msg )
setRoute route model = setRoute route model =
let
( newModel, newCmd, newSession ) =
case route of case route of
Route.Unknown hash -> Route.Unknown hash ->
( model, Cmd.none, Session.SetFlash ("Unknown route requested: " ++ hash) ) ( model, Cmd.none, Session.SetFlash ("Unknown route requested: " ++ hash) )
@@ -215,8 +217,12 @@ setRoute route model =
) )
Route.Monitor -> Route.Monitor ->
( { model | page = Monitor (Monitor.init model.session.host) } let
, Ports.windowTitle "Inbucket Monitor" ( subModel, subCmd ) =
Monitor.init
in
( { model | page = Monitor subModel }
, Cmd.map MonitorMsg subCmd
, Session.none , Session.none
) )
@@ -228,6 +234,14 @@ setRoute route model =
] ]
, Session.none , Session.none
) )
in
case model.page of
Monitor _ ->
-- Leaving Monitor page, shut down the web socket.
( newModel, Cmd.batch [ Ports.monitorCommand False, newCmd ], newSession )
_ ->
( newModel, newCmd, newSession )
applySession : ( Model, Cmd Msg, Session.Msg ) -> ( Model, Cmd Msg ) applySession : ( Model, Cmd Msg, Session.Msg ) -> ( Model, Cmd Msg )

View File

@@ -15,25 +15,28 @@ import DateFormat
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events as Events import Html.Events as Events
import Json.Decode exposing (decodeString) import Json.Decode as D
import Ports
import Route import Route
import WebSocket
-- MODEL -- MODEL
type alias Model = type alias Model =
{ wsUrl : String { connected : Bool
, messages : List MessageHeader , messages : List MessageHeader
} }
init : String -> Model init : ( Model, Cmd Msg )
init host = init =
{ wsUrl = "ws://" ++ host ++ "/api/v1/monitor/messages" ( Model False []
, messages = [] , Cmd.batch
} [ Ports.windowTitle "Inbucket Monitor"
, Ports.monitorCommand True
]
)
@@ -42,7 +45,16 @@ init host =
subscriptions : Model -> Sub Msg subscriptions : Model -> Sub Msg
subscriptions model = subscriptions model =
WebSocket.listen model.wsUrl (decodeString MessageHeader.decoder >> NewMessage) let
monitorMessage =
D.oneOf
[ D.map Message MessageHeader.decoder
, D.map Connected D.bool
]
|> D.decodeValue
|> Ports.monitorMessage
in
Sub.map MonitorResult monitorMessage
@@ -50,17 +62,25 @@ subscriptions model =
type Msg type Msg
= NewMessage (Result String MessageHeader) = MonitorResult (Result String MonitorMessage)
| OpenMessage MessageHeader | OpenMessage MessageHeader
type MonitorMessage
= Connected Bool
| Message MessageHeader
update : Session -> Msg -> Model -> ( Model, Cmd Msg, Session.Msg ) update : Session -> Msg -> Model -> ( Model, Cmd Msg, Session.Msg )
update session msg model = update session msg model =
case msg of case msg of
NewMessage (Ok msg) -> MonitorResult (Ok (Connected status)) ->
( { model | connected = status }, Cmd.none, Session.none )
MonitorResult (Ok (Message msg)) ->
( { model | messages = msg :: model.messages }, Cmd.none, Session.none ) ( { model | messages = msg :: model.messages }, Cmd.none, Session.none )
NewMessage (Err err) -> MonitorResult (Err err) ->
( model, Cmd.none, Session.SetFlash err ) ( model, Cmd.none, Session.SetFlash err )
OpenMessage msg -> OpenMessage msg ->
@@ -78,7 +98,17 @@ view : Session -> Model -> Html Msg
view session model = view session model =
div [ id "page" ] div [ id "page" ]
[ h1 [] [ text "Monitor" ] [ h1 [] [ text "Monitor" ]
, p [] [ text "Messages will be listed here shortly after delivery." ] , p []
[ text "Messages will be listed here shortly after delivery. "
, em []
[ text
(if model.connected then
"Connected."
else
"Disconnected!"
)
]
]
, table [ id "monitor" ] , table [ id "monitor" ]
[ thead [] [ thead []
[ th [] [ text "Date" ] [ th [] [ text "Date" ]

View File

@@ -1,9 +1,22 @@
port module Ports exposing (onSessionChange, storeSession, windowTitle) port module Ports
exposing
( monitorCommand
, monitorMessage
, onSessionChange
, storeSession
, windowTitle
)
import Data.Session exposing (Persistent) import Data.Session exposing (Persistent)
import Json.Encode exposing (Value) import Json.Encode exposing (Value)
port monitorCommand : Bool -> Cmd msg
port monitorMessage : (Value -> msg) -> Sub msg
port onSessionChange : (Value -> msg) -> Sub msg port onSessionChange : (Value -> msg) -> Sub msg

View File

@@ -1,33 +1,40 @@
import './main.css'; import './main.css'
import { Main } from './Main.elm'; import { Main } from './Main.elm'
import registerServiceWorker from './registerServiceWorker'; import registerServiceWorker from './registerServiceWorker'
import registerMonitorPorts from './registerMonitor'
var app = Main.embed(document.getElementById('root'), sessionObject()); // App startup.
var app = Main.embed(document.getElementById('root'), sessionObject())
// Message monitor.
registerMonitorPorts(app)
// Session storage.
app.ports.storeSession.subscribe(function (session) { app.ports.storeSession.subscribe(function (session) {
localStorage.session = JSON.stringify(session); localStorage.session = JSON.stringify(session)
}); })
app.ports.windowTitle.subscribe(function (title) {
document.title = title;
});
window.addEventListener("storage", function (event) { window.addEventListener("storage", function (event) {
if (event.storageArea === localStorage && event.key === "session") { if (event.storageArea === localStorage && event.key === "session") {
app.ports.onSessionChange.send(sessionObject()); app.ports.onSessionChange.send(sessionObject())
} }
}, false); }, false)
function sessionObject() { function sessionObject() {
var s = localStorage.session; var s = localStorage.session
try { try {
if (s) { if (s) {
return JSON.parse(s); return JSON.parse(s)
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error)
} }
return null; return null
} }
registerServiceWorker(); // Window title.
app.ports.windowTitle.subscribe(function (title) {
document.title = title
})
registerServiceWorker()

46
ui/src/registerMonitor.js Normal file
View File

@@ -0,0 +1,46 @@
// Register the websocket listeners for the monitor API.
export default function registerMonitorPorts(app) {
var uri = '/api/v1/monitor/messages'
var url = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + uri
// Current handler.
var handler = null
app.ports.monitorCommand.subscribe(function (cmd) {
if (handler != null) {
handler.down()
handler = null
}
if (cmd) {
// Command is up.
handler = websocketHandler(url, app.ports.monitorMessage)
handler.up()
}
})
}
// Creates a handler responsible for connecting, disconnecting from web socket.
function websocketHandler(url, port) {
var ws = null
return {
up: () => {
ws = new WebSocket(url)
ws.addEventListener('open', function (e) {
port.send(true)
})
ws.addEventListener('close', function (e) {
port.send(false)
})
ws.addEventListener('message', function (e) {
var msg = JSON.parse(e.data)
port.send(msg)
})
},
down: () => {
ws.close()
}
}
}